2019-06-03 18:27:44 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-06-19 02:29:31 +00:00
|
|
|
"html"
|
2019-06-03 18:27:44 +00:00
|
|
|
"html/template"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/SlyMarbo/rss"
|
|
|
|
"github.com/mattn/go-runewidth"
|
|
|
|
"github.com/microcosm-cc/bluemonday"
|
|
|
|
"git.sr.ht/~sircmpwn/getopt"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Article struct {
|
|
|
|
Date time.Time
|
|
|
|
Link string
|
|
|
|
SourceLink string
|
|
|
|
SourceTitle string
|
|
|
|
Summary template.HTML
|
|
|
|
Title string
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
var (
|
2019-06-17 18:22:12 +00:00
|
|
|
narticles int = 3
|
|
|
|
perSource int = 1
|
|
|
|
summaryLen int = 256
|
2019-06-03 18:27:44 +00:00
|
|
|
sources []*url.URL
|
|
|
|
)
|
|
|
|
|
2019-06-17 18:22:12 +00:00
|
|
|
opts, optind, err := getopt.Getopts(os.Args[1:], "l:n:p:s:")
|
2019-06-03 18:27:44 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
|
|
switch opt.Option {
|
|
|
|
case 'l':
|
|
|
|
summaryLen, err = strconv.Atoi(opt.Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
case 'n':
|
|
|
|
narticles, err = strconv.Atoi(opt.Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2019-06-17 18:22:12 +00:00
|
|
|
case 'p':
|
|
|
|
perSource, err = strconv.Atoi(opt.Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2019-06-03 18:27:44 +00:00
|
|
|
case 's':
|
|
|
|
u, err := url.Parse(opt.Value)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
sources = append(sources, u)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(os.Args[optind+1:]) != 0 {
|
|
|
|
log.Fatalf(
|
|
|
|
"Usage: %s [-s https://source.rss...] < in.html > out.html",
|
|
|
|
os.Args[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
input, err := ioutil.ReadAll(os.Stdin)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
tmpl, err := template.
|
|
|
|
New("template").
|
|
|
|
Funcs(map[string]interface{}{
|
|
|
|
"date": func(t time.Time) string {
|
|
|
|
return t.Format("January 2, 2006")
|
|
|
|
},
|
|
|
|
}).
|
|
|
|
Parse(string(input))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Println("Fetching feeds...")
|
|
|
|
var feeds []*rss.Feed
|
|
|
|
for _, source := range sources {
|
|
|
|
feed, err := rss.Fetch(source.String())
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Error fetching %s: %s", source.String(), err.Error())
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
feeds = append(feeds, feed)
|
|
|
|
log.Printf("Fetched %s", feed.Title)
|
|
|
|
}
|
|
|
|
if len(feeds) == 0 {
|
|
|
|
log.Fatal("Expected at least one feed to successfully fetch")
|
|
|
|
}
|
|
|
|
|
|
|
|
policy := bluemonday.StrictPolicy()
|
|
|
|
|
|
|
|
var articles []*Article
|
|
|
|
for _, feed := range feeds {
|
|
|
|
if len(feed.Items) == 0 {
|
|
|
|
log.Printf("Warning: feed %s has no items", feed.Title)
|
|
|
|
continue
|
|
|
|
}
|
2019-06-17 18:22:12 +00:00
|
|
|
items := feed.Items
|
|
|
|
if len(items) > perSource {
|
|
|
|
items = items[:perSource]
|
|
|
|
}
|
|
|
|
for _, item := range items {
|
2019-06-19 02:29:31 +00:00
|
|
|
raw_summary := item.Summary
|
|
|
|
if len(raw_summary) == 0 {
|
|
|
|
raw_summary = html.UnescapeString(item.Content)
|
|
|
|
}
|
2019-06-17 18:22:12 +00:00
|
|
|
summary := runewidth.Truncate(
|
2019-06-19 02:29:31 +00:00
|
|
|
policy.Sanitize(raw_summary), summaryLen, "…")
|
2019-06-17 18:22:12 +00:00
|
|
|
articles = append(articles, &Article{
|
|
|
|
Date: item.Date,
|
|
|
|
SourceLink: feed.Link,
|
|
|
|
SourceTitle: feed.Title,
|
|
|
|
Summary: template.HTML(summary),
|
|
|
|
Title: item.Title,
|
|
|
|
Link: item.Link,
|
|
|
|
})
|
|
|
|
}
|
2019-06-03 18:27:44 +00:00
|
|
|
}
|
|
|
|
sort.Slice(articles, func(i, j int) bool {
|
|
|
|
return articles[i].Date.After(articles[j].Date)
|
|
|
|
})
|
2019-06-14 15:56:11 +00:00
|
|
|
if len(articles) < narticles {
|
|
|
|
narticles = len(articles)
|
|
|
|
}
|
2019-06-03 18:27:44 +00:00
|
|
|
articles = articles[:narticles]
|
|
|
|
err = tmpl.Execute(os.Stdout, struct{
|
|
|
|
Articles []*Article
|
|
|
|
}{
|
|
|
|
Articles: articles,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|