Add Gemfeed support
alex wennerberg alex@alexwennerberg.com
Tue, 15 Dec 2020 18:05:49 -0800
6 files changed,
120 insertions(+),
2 deletions(-)
A
gemfeed.go
@@ -0,0 +1,79 @@
+// Parses Gemfeed according to the companion spec: gemini://gemini.circumlunar.space/docs/companion/subscription.gmi +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "time" +) + +type Gemfeed struct { + Title string + Entries []*FeedEntry +} + +type FeedEntry struct { + Title string + Url string + Date time.Time + FeedTitle string + DateString string +} + +// TODO definitely cache this function -- it reads EVERY gemini file on flounder. +func getAllGemfeedEntries() ([]*FeedEntry, error) { + var feedEntries []*FeedEntry + err := filepath.Walk(c.FilesDirectory, func(thepath string, info os.FileInfo, err error) error { + if isGemini(info.Name()) { + f, err := os.Open(thepath) + feed, err := ParseGemfeed(f) + if err == nil { + feedEntries = append(feedEntries, feed.Entries...) + } + } + return nil + }) + if err != nil { + return nil, err + } else { + sort.Slice(feedEntries, func(i, j int) bool { + return feedEntries[i].Date.After(feedEntries[j].Date) + }) + return feedEntries, nil + } +} + +// Parsed Gemfeed text Returns error if not a gemfeed +// Doesn't sort output +// Doesn't get posts dated in the future +func ParseGemfeed(text io.Reader) (*Gemfeed, error) { + scanner := bufio.NewScanner(text) + gf := Gemfeed{} + for scanner.Scan() { + line := scanner.Text() + if gf.Title == "" && strings.HasPrefix(line, "#") && !strings.HasPrefix(line, "##") { + gf.Title = strings.Trim(line[1:], " \t") + } else if strings.HasPrefix(line, "=>") { + link := strings.Trim(line[2:], " \t") + splits := strings.SplitN(link, " ", 2) + if len(splits) == 2 && len(splits[1]) >= 10 { + dateString := splits[1][:10] + date, err := time.Parse("2006-01-02", dateString) + if err == nil && time.Now().After(date) { + title := strings.Trim(splits[1][10:], " -\t") + fe := FeedEntry{title, splits[0], date, gf.Title, dateString} + gf.Entries = append(gf.Entries, &fe) + } + } + } + } + if len(gf.Entries) == 0 { + return nil, fmt.Errorf("No Gemfeed entries found") + } + return &gf, nil +}
M
http.go
→
http.go
@@ -78,6 +78,24 @@ panic(err)
} } +func feedHandler(w http.ResponseWriter, r *http.Request) { + user := newGetAuthUser(r) + feedEntries, err := getAllGemfeedEntries() + if err != nil { + panic(err) + } + data := struct { + Host string + PageTitle string + FeedEntries []*FeedEntry + AuthUser AuthUser + }{c.Host, c.SiteTitle, feedEntries, user} + err = t.ExecuteTemplate(w, "feed.html", data) + if err != nil { + panic(err) + } +} + func editFileHandler(w http.ResponseWriter, r *http.Request) { user := newGetAuthUser(r) if !user.LoggedIn {@@ -533,10 +551,9 @@ return
} // Dumb content negotiation - extension := path.Ext(fileName) _, raw := r.URL.Query()["raw"] acceptsGemini := strings.Contains(r.Header.Get("Accept"), "text/gemini") - if !raw && !acceptsGemini && (extension == ".gmi" || extension == ".gemini") { + if !raw && !acceptsGemini && isGemini(fileName) { file, _ := os.Open(fileName) htmlString := textToHTML(gmi.ParseText(file)) favicon := getFavicon(userName)@@ -668,6 +685,7 @@ hostname := s[0]
port := c.HttpPort serveMux.HandleFunc(hostname+"/", rootHandler) + serveMux.HandleFunc(hostname+"/feed", feedHandler) serveMux.HandleFunc(hostname+"/my_site", mySiteHandler) serveMux.HandleFunc(hostname+"/me", myAccountHandler) serveMux.HandleFunc(hostname+"/my_site/flounder-archive.zip", archiveHandler)
A
templates/feed.html
@@ -0,0 +1,14 @@
+{{$domain := .Host}} +{{template "header" .}} +<h1>🐟{{.PageTitle}} -- Feeds</h1> +{{template "nav.html" .}} +<br> +<p> +For more information on how to format your site to show up here, see <a href="https://admin.flounder.online/gemfeed.gmi">this documentation</a> +<h2>Feed:</h2> +{{ range .FeedEntries}} +<p> +<a href="{{.Url}}" class='person-link'>{{.DateString}} [{{.FeedTitle}}] — {{.Title}}</a> +</p> +{{end}} +{{template "footer" .}}
M
templates/index.html
→
templates/index.html
@@ -5,6 +5,7 @@ {{template "nav.html" .}}
<br> <p> Welcome to flounder! For more information and site updates, check out the <a href="//admin.{{$domain}}">admin page</a></p> + <h2>All users:</h2> {{ range .Users}} <a href="//{{.}}.{{$domain}}" class='person-link'>{{.}}</a>