package main import ( "html" "html/template" "io/ioutil" "log" "net/url" "os" "sort" "strings" "time" "git.sr.ht/~sircmpwn/getopt" "github.com/SlyMarbo/rss" "github.com/mattn/go-runewidth" "github.com/microcosm-cc/bluemonday" ) type urlSlice []*url.URL func (us *urlSlice) String() string { var str []string for _, u := range *us { str = append(str, u.String()) } return strings.Join(str, ", ") } func (us *urlSlice) Set(val string) error { u, err := url.Parse(val) if err != nil { return err } *us = append(*us, u) return nil } type Article struct { Date time.Time Link string SourceLink string SourceTitle string Summary template.HTML Title string } func main() { var ( narticles = getopt.Int("n", 3, "article count") perSource = getopt.Int("p", 1, "articles to take from each source") summaryLen = getopt.Int("l", 256, "length of summaries") sources []*url.URL ) getopt.Var((*urlSlice)(&sources), "s", "list of sources") getopt.Usage = func() { log.Fatalf("Usage: %s [-s https://source.rss...] < in.html > out.html", os.Args[0]) } err := getopt.Parse() if err != nil { panic(err) } 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 } items := feed.Items if len(items) > *perSource { items = items[:*perSource] } base, err := url.Parse(feed.UpdateURL) if err != nil { log.Fatal("failed parsing update URL of the feed") } feedLink, _ := url.Parse(feed.Link) if err != nil { log.Fatal("failed parsing canonical feed URL of the feed") } for _, item := range items { raw_summary := item.Summary if len(raw_summary) == 0 { raw_summary = html.UnescapeString(item.Content) } summary := runewidth.Truncate( policy.Sanitize(raw_summary), *summaryLen, "…") itemLink, _ := url.Parse(item.Link) if err != nil { log.Fatal("failed parsing article URL of the feed item") } articles = append(articles, &Article{ Date: item.Date, SourceLink: base.ResolveReference(feedLink).String(), SourceTitle: feed.Title, Summary: template.HTML(summary), Title: item.Title, Link: base.ResolveReference(itemLink).String(), }) } } sort.Slice(articles, func(i, j int) bool { return articles[i].Date.After(articles[j].Date) }) if len(articles) < *narticles { *narticles = len(articles) } articles = articles[:*narticles] err = tmpl.Execute(os.Stdout, struct { Articles []*Article }{ Articles: articles, }) if err != nil { panic(err) } }