flounder

A simple gemini site builder
Log | Files | Refs | README | LICENSE

commit fbba2837f74ed229dd1a9fc32a3ddf12d1a397d3
parent 92e3d828bff62e1549adf5a64f2ff55af993e466
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Tue, 15 Dec 2020 18:05:49 -0800

Add Gemfeed support

Diffstat:
Agemfeed.go | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mhttp.go | 22++++++++++++++++++++--
Atemplates/feed.html | 14++++++++++++++
Mtemplates/index.html | 1+
Mtemplates/nav.html | 1+
Mutils.go | 5+++++
6 files changed, 120 insertions(+), 2 deletions(-)

diff --git a/gemfeed.go b/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 +} diff --git a/http.go b/http.go @@ -78,6 +78,24 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { } } +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 @@ func userFile(w http.ResponseWriter, r *http.Request) { } // 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 @@ func runHTTPServer() { 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) diff --git a/templates/feed.html b/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" .}} diff --git a/templates/index.html b/templates/index.html @@ -5,6 +5,7 @@ <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> diff --git a/templates/nav.html b/templates/nav.html @@ -1,5 +1,6 @@ <nav> <a href="/">home</a> + <a href="/feed">feed</a> {{ if .AuthUser.LoggedIn }} <a href="/my_site">my_site</a> <a href="/me">me</a> diff --git a/utils.go b/utils.go @@ -11,6 +11,11 @@ import ( "time" ) +func isGemini(filename string) bool { + extension := path.Ext(filename) + return extension == ".gmi" || extension == ".gemini" +} + func timeago(t *time.Time) string { d := time.Since(*t) if d.Seconds() < 60 {