all repos — flounder @ 7dbd33126b29caac5ffbfd9fa34ead2afb2a406f

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				if feed.Title == "" {
 50					feed.Title = "(Untitled Feed)"
 51				}
 52				feed.Url = &baseUrl
 53				feedEntries = append(feedEntries, feed.Entries...)
 54			}
 55		}
 56		return nil
 57	})
 58	if err != nil {
 59		return nil, err
 60	} else {
 61		sort.Slice(feedEntries, func(i, j int) bool {
 62			return feedEntries[i].Date.After(feedEntries[j].Date)
 63		})
 64		if len(feedEntries) > maxItems {
 65			return feedEntries[:maxItems], nil
 66		}
 67		return feedEntries, nil
 68	}
 69}
 70
 71// Parsed Gemfeed text Returns error if not a gemfeed
 72// Doesn't sort output
 73// Doesn't get posts dated in the future
 74// if limit > -1 -- limit how many we are getting
 75func ParseGemfeed(text io.Reader, baseUrl url.URL, limit int) (*Gemfeed, error) {
 76	scanner := bufio.NewScanner(text)
 77	gf := Gemfeed{}
 78	for scanner.Scan() {
 79		if limit > -1 && len(gf.Entries) >= limit {
 80			break
 81		}
 82		line := scanner.Text()
 83		if gf.Title == "" && strings.HasPrefix(line, "#") && !strings.HasPrefix(line, "##") {
 84			gf.Title = strings.Trim(line[1:], " \t")
 85		} else if strings.HasPrefix(line, "=>") {
 86			link := strings.Trim(line[2:], " \t")
 87			splits := strings.SplitN(link, " ", 2)
 88			if len(splits) == 2 && len(splits[1]) >= 10 {
 89				dateString := splits[1][:10]
 90				date, err := time.Parse("2006-01-02", dateString)
 91				if err != nil {
 92					continue
 93				}
 94				parsedUrl, err := url.Parse(splits[0])
 95				if err != nil {
 96					continue
 97				}
 98				if parsedUrl.Host == "" {
 99					// Is relative link
100					parsedUrl.Host = baseUrl.Host
101					parsedUrl.Path = path.Join(path.Dir(baseUrl.Path), parsedUrl.Path)
102				}
103				parsedUrl.Scheme = ""
104				if time.Now().After(date) {
105					title := strings.Trim(splits[1][10:], " -\t")
106					fe := FeedEntry{title, parsedUrl, date, dateString, &gf}
107					if fe.Title == "" {
108						fe.Title = "(Untitled)"
109					}
110					gf.Entries = append(gf.Entries, &fe)
111				}
112			}
113		}
114	}
115	if len(gf.Entries) == 0 {
116		return nil, fmt.Errorf("No Gemfeed entries found")
117	}
118	return &gf, nil
119}