You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
208 lines
4.8 KiB
208 lines
4.8 KiB
package main |
|
|
|
import ( |
|
"encoding/xml" |
|
"log" |
|
"strings" |
|
"time" |
|
|
|
"github.com/timshannon/bolthold" |
|
"go.etcd.io/bbolt" |
|
) |
|
|
|
const ( |
|
rssPre = `<?xml version="1.0" encoding="UTF-8" ?> |
|
` |
|
rfc2822 = "Mon, 02 Jan 2006 15:04:05 +0700" |
|
) |
|
|
|
type queueItem struct { |
|
URL string `boltholdKey:"URL"` |
|
Date time.Time |
|
Votes int |
|
} |
|
|
|
type feedItem struct { |
|
Date time.Time `boltholdKey:"date"` |
|
URL string `boltholdIndex:"URL"` |
|
Title string |
|
Excerpt string |
|
Authors []string |
|
WordCount int |
|
ImageURL string |
|
} |
|
|
|
type feed struct { |
|
db *bolthold.Store |
|
fetcher contentFetcher |
|
} |
|
|
|
func newFeed(dbPath string, fetcher contentFetcher) (*feed, error) { |
|
db, err := bolthold.Open(dbPath, 0660, nil) |
|
if err != nil { |
|
return nil, err |
|
} |
|
f := feed{ |
|
db: db, |
|
fetcher: fetcher, |
|
} |
|
go f.feeder() |
|
return &f, nil |
|
} |
|
|
|
func (f *feed) close() error { |
|
return f.db.Close() |
|
} |
|
|
|
func (f *feed) feeder() { |
|
for { |
|
last4Hours := time.Now().Add(time.Hour * -4) |
|
items4Hours, err := f.db.Count(&feedItem{}, bolthold.Where("Date").Gt(last4Hours)) |
|
if err != nil { |
|
log.Printf("Error getting last update: %v", err) |
|
} else if items4Hours == 0 { |
|
f.publish() |
|
} |
|
time.Sleep(time.Minute * 10) |
|
} |
|
} |
|
|
|
func (f *feed) publish() { |
|
var item queueItem |
|
err := f.db.FindOne(&item, bolthold.Where("Votes").Ge(1).SortBy("Votes", "Date").Limit(1)) |
|
if err != nil { |
|
if err != bolthold.ErrNotFound { |
|
log.Printf("Error fetching an item to publish from the queue: %v", err) |
|
} |
|
return |
|
} |
|
|
|
content, err := f.fetcher.fetchContent(item.URL) |
|
if err != nil { |
|
log.Printf("Error fetching content from pocket: %v", err) |
|
return |
|
} |
|
publishItem := feedItem{ |
|
Date: time.Now(), |
|
URL: item.URL, |
|
Title: content.title, |
|
Excerpt: content.excerpt, |
|
Authors: content.authors, |
|
ImageURL: content.imageURL, |
|
} |
|
err = f.db.Insert(publishItem.Date, &publishItem) |
|
if err != nil { |
|
log.Printf("Error inserting %s: %v", item.URL, err) |
|
return |
|
} |
|
err = f.db.Delete(item.URL, &item) |
|
if err != nil { |
|
log.Printf("Error deleting %s from queue: %v", item.URL, err) |
|
} |
|
} |
|
|
|
func (f *feed) add(url string) error { |
|
published, err := f.db.Count(&feedItem{}, bolthold.Where("URL").Eq(url)) |
|
if err != nil { |
|
log.Printf("Error checking if already published %s: %v", url, err) |
|
} |
|
if published != 0 { |
|
log.Printf("Already published %s", url) |
|
return nil |
|
} |
|
|
|
return f.db.Bolt().Update(func(tx *bbolt.Tx) error { |
|
var item queueItem |
|
err := f.db.TxGet(tx, url, &item) |
|
if err != nil { |
|
if err != bolthold.ErrNotFound { |
|
return err |
|
} |
|
|
|
item.URL = url |
|
item.Date = time.Now() |
|
} |
|
item.Votes++ |
|
return f.db.TxUpsert(tx, url, &item) |
|
}) |
|
} |
|
|
|
type rss struct { |
|
Channel Channel `xml:"channel"` |
|
Version string `xml:"version,attr"` |
|
DC string `xml:"xmlns:dc,attr"` |
|
} |
|
|
|
type Channel struct { |
|
Title string `xml:"title"` |
|
Link string `xml:"link"` |
|
Description string `xml:"description"` |
|
LastBuildDate string `xml:"lastBuildDate"` |
|
Image ChannelImage `xml:"image"` |
|
Item []Item `xml:"item"` |
|
} |
|
|
|
type ChannelImage struct { |
|
URL string `xml:"url"` |
|
Title string `xml:"title"` |
|
Link string `xml:"link"` |
|
} |
|
|
|
type Item struct { |
|
Title string `xml:"title"` |
|
DCCreator []string `xml:"dc:creator"` |
|
Description string `xml:"description"` |
|
Link string `xml:"link"` |
|
PubDate string `xml:"pubDate"` |
|
GUID string `xml:"guid"` |
|
} |
|
|
|
func (f feed) items() []feedItem { |
|
var items []feedItem |
|
err := f.db.Find(&items, bolthold.Where("Date").Gt(time.Unix(0, 0)).SortBy("Date").Reverse()) |
|
if err != nil { |
|
log.Printf("Error reading feed db bucket: %v", err) |
|
} |
|
return items |
|
} |
|
|
|
func (f feed) rss() string { |
|
channel := Channel{ |
|
Title: "Farenheit 2577", |
|
Link: "https://sindominio.net/2577", |
|
Description: "News from the distopian present", |
|
Image: ChannelImage{ |
|
URL: "https://sindominio.net/2577/2577.png", |
|
Title: "Farenheit 2577", |
|
Link: "https://sindominio.net/2577", |
|
}, |
|
LastBuildDate: time.Now().Format(rfc2822), |
|
} |
|
err := f.db.ForEach(bolthold.Where("Date").Gt(time.Unix(0, 0)).SortBy("Date").Reverse(), |
|
func(item *feedItem) error { |
|
description := "" |
|
if item.ImageURL != "" { |
|
description = "<img src=\"" + item.ImageURL + "\" alt=\"header\">" |
|
} |
|
description += "<p>" + strings.Replace(item.Excerpt, "\n", "</p><p>", -1) + "</p>" |
|
channel.Item = append(channel.Item, Item{ |
|
Title: item.Title, |
|
DCCreator: item.Authors, |
|
Description: description, |
|
Link: item.URL, |
|
GUID: item.URL, |
|
PubDate: item.Date.Format(rfc2822), |
|
}) |
|
return nil |
|
}) |
|
if err != nil { |
|
log.Printf("Can't read the feed database: %v", err) |
|
return "" |
|
} |
|
buff, err := xml.MarshalIndent(rss{channel, "2.0", "http://purl.org/dc/elements/1.1/"}, "", " ") |
|
if err != nil { |
|
log.Printf("Can't marshal rss: %v", err) |
|
return "" |
|
} |
|
return rssPre + string(buff) |
|
}
|
|
|