all repos — flounder @ 976db1036ed6aa057228c685bcd74a15a38180b6

A small site builder for the Gemini protocol

gemfeed.go (view raw)

  1// Parses Gemfeed according to the companion spec: gemini://gemini.circumlunar.space/docs/companion/subscription.gmi
  2package main
  3
  4import (
  5	"bufio"
  6	"fmt"
  7	"io"
  8	"net/url"
  9	"os"
 10	"path"
 11	"path/filepath"
 12	"sort"
 13	"strings"
 14	"time"
 15)
 16
 17type Gemfeed struct {
 18	Title   string
 19	Creator string
 20	Url     *url.URL
 21	Entries []*FeedEntry
 22}
 23
 24type FeedEntry struct {
 25	Title      string
 26	Url        *url.URL
 27	Date       time.Time
 28	DateString string
 29	Feed       *Gemfeed
 30}
 31
 32// TODO definitely cache this function -- it reads EVERY gemini file on flounder.
 33func getAllGemfeedEntries() ([]*FeedEntry, error) {
 34	maxUserItems := 25
 35	maxItems := 100
 36	var feedEntries []*FeedEntry
 37	err := filepath.Walk(c.FilesDirectory, func(thepath string, info os.FileInfo, err error) error {
 38		if isGemini(info.Name()) {
 39			f, err := os.Open(thepath)
 40			// TODO verify no path bugs here
 41			creator := getCreator(thepath)
 42			baseUrl := url.URL{}
 43			baseUrl.Host = creator + "." + c.Host
 44			baseUrl.Path = getLocalPath(thepath)
 45			feed, err := ParseGemfeed(f, baseUrl, maxUserItems) // TODO make configurable
 46			f.Close()
 47			if err == nil {
 48				feed.Creator = creator
 49				feed.Url = &baseUrl
 50				feedEntries = append(feedEntries, feed.Entries...)
 51			}
 52		}
 53		return nil
 54	})
 55	if err != nil {
 56		return nil, err
 57	} else {
 58		sort.Slice(feedEntries, func(i, j int) bool {
 59			return feedEntries[i].Date.After(feedEntries[j].Date)
 60		})
 61		if len(feedEntries) > maxItems {
 62			return feedEntries[:maxItems], nil
 63		}
 64		return feedEntries, nil
 65	}
 66}
 67
 68// Parsed Gemfeed text Returns error if not a gemfeed
 69// Doesn't sort output
 70// Doesn't get posts dated in the future
 71// if limit > -1 -- limit how many we are getting
 72func ParseGemfeed(text io.Reader, baseUrl url.URL, limit int) (*Gemfeed, error) {
 73	scanner := bufio.NewScanner(text)
 74	gf := Gemfeed{}
 75	for scanner.Scan() {
 76		if limit > -1 && len(gf.Entries) >= limit {
 77			break
 78		}
 79		line := scanner.Text()
 80		if gf.Title == "" && strings.HasPrefix(line, "#") && !strings.HasPrefix(line, "##") {
 81			gf.Title = strings.Trim(line[1:], " \t")
 82		} else if strings.HasPrefix(line, "=>") {
 83			link := strings.Trim(line[2:], " \t")
 84			splits := strings.SplitN(link, " ", 2)
 85			if len(splits) == 2 && len(splits[1]) >= 10 {
 86				dateString := splits[1][:10]
 87				date, err := time.Parse("2006-01-02", dateString)
 88				if err != nil {
 89					continue
 90				}
 91				parsedUrl, err := url.Parse(splits[0])
 92				if err != nil {
 93					continue
 94				}
 95				if parsedUrl.Host == "" {
 96					// Is relative link
 97					parsedUrl.Host = baseUrl.Host
 98					parsedUrl.Path = path.Join(path.Dir(baseUrl.Path), parsedUrl.Path)
 99				}
100				parsedUrl.Scheme = ""
101				if time.Now().After(date) {
102					title := strings.Trim(splits[1][10:], " -\t")
103					fe := FeedEntry{title, parsedUrl, date, dateString, &gf}
104					gf.Entries = append(gf.Entries, &fe)
105				}
106			}
107		}
108	}
109	if len(gf.Entries) == 0 {
110		return nil, fmt.Errorf("No Gemfeed entries found")
111	}
112	return &gf, nil
113}