diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed65116 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +openring diff --git a/README.md b/README.md index a1f368c..0182727 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ # openring-it -Italian adaptation of Drew Devault’s openring \ No newline at end of file +This is a tool for generating a webring from RSS feeds, so you can link to +other blogs you like on your own blog. It's designed to be fairly simple and +integrate with any static site generator. The basic usage is: + +``` +openring \ + -s https://drewdevault.com/feed.xml \ + -s https://emersion.fr/blog/rss.xml \ + -s https://danluu.com/atom.xml \ + < in.html \ + > out.html +``` + +This will read the template at in.html (an example is provided, but feel free to +adjust it to suit your needs), fetch the latest 5 articles from among your +sources, and pass them to the template and write the output to out.html. Then +you can include this file with your static site generator's normal file include +mechanism. + +## Contributing + +Want to help? [Send patches](https://git-send-email.io) to the original openring's [mailing +list](https://lists.sr.ht/~sircmpwn/public-inbox): +[`~sircmpwn/public-inbox@lists.sr.ht`](mailto:~sircmpwn/public-inbox@lists.sr.ht). +Thanks! diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fc1fcc1 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module git.sr.ht/~sircmpwn/openring + +go 1.12 + +require ( + git.sr.ht/~sircmpwn/getopt v0.0.0-20190621174457-292febf82fd0 + github.com/SlyMarbo/rss v1.0.1 + github.com/mattn/go-runewidth v0.0.4 + github.com/microcosm-cc/bluemonday v1.0.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b6cc966 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +git.sr.ht/~sircmpwn/getopt v0.0.0-20190214165041-9a4f886f9fc7 h1:xTFH5S/3ltiRvAtETLLDFWm5nVIouT5GeCPHm8UaVEU= +git.sr.ht/~sircmpwn/getopt v0.0.0-20190214165041-9a4f886f9fc7/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= +git.sr.ht/~sircmpwn/getopt v0.0.0-20190621174457-292febf82fd0 h1:gUeOEsT0mhoCKxKYJk8HeYtUZME686xs70eG2l80W5U= +git.sr.ht/~sircmpwn/getopt v0.0.0-20190621174457-292febf82fd0/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= +github.com/SlyMarbo/rss v1.0.1 h1:fiaIU5UhcXauVOniHOIocWG7uj8Ej6pHNarMGPJilzA= +github.com/SlyMarbo/rss v1.0.1/go.mod h1:JNF+T33oj4m5WLCQXpBTCgO+SxRbYVgdiiimHNgzcbA= +github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ= +github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/in.html b/in.html new file mode 100644 index 0000000..ab2410e --- /dev/null +++ b/in.html @@ -0,0 +1,21 @@ +
+

Articoli da altri blog

+
+ {{range .Articles}} +
+

+ {{.Title}} +

+

{{.Summary}}

+ + da {{.SourceTitle}} + + {{.Date | datef "2 January 2006"}} +
+ {{end}} +
+

+ Generato con + openring +

+
diff --git a/openring.go b/openring.go new file mode 100644 index 0000000..839cb5a --- /dev/null +++ b/openring.go @@ -0,0 +1,205 @@ +// License-Id: GPL-3.0-only +// Copyright: 2019 Drew DeVault +// Copyright: 2019 Haelwenn (lanodan) Monnier +// Copyright: 2019 Jeff Kaufman +// Copyright: 2019 Nate Dobbins +// Copyright: 2019 Noah Loomans +// Copyright: 2019 Philip K +// Copyright: 2019 Simon Ser +// Copyright: 2020 Drew DeVault +// Copyright: 2020 skuzzymiglet +// Copyright: 2021 Gianluca Arbezzano +// Copyright: 2021 sourque +package main + +import ( + "bufio" + "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", 5, "article count") + perSource = getopt.Int("p", 1, "articles to take from each source") + summaryLen = getopt.Int("l", 256, "length of summaries") + urlsFile = getopt.String("S", "", "file with URLs of sources") + 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) + } + + if *urlsFile != "" { + file, err := os.Open(*urlsFile) + if err != nil { + panic(err) + } + sc := bufio.NewScanner(file) + for sc.Scan() { + (*urlSlice)(&sources).Set(sc.Text()) + } + file.Close() + } + + input, err := ioutil.ReadAll(os.Stdin) + if err != nil { + panic(err) + } + + rimpiazzo := strings.NewReplacer( + "January", "gennaio", + "February", "febbraio", + "March", "marzo", + "April", "aprile", + "May", "maggio", + "June", "giugno", + "July", "luglio", + "August", "agosto", + "September", "settembre", + "October", "ottobre", + "November", "novembre", + "December", "dicembre", ) + + tmpl, err := template. + New("template"). + Funcs(map[string]interface{}{ + "date": func(t time.Time) string { + return t.Format("January 2, 2006") + }, + "datef": func(fmt string, t time.Time) string { + return rimpiazzo.Replace(t.Format(fmt)) + }, + }). + 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 + } + if feed.Title == "" { + log.Printf("Warning: feed from %s has no title", source.Host) + feed.Title = source.Host + } + 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, err := 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, err := 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) + } +}