all repos — flounder @ 18a042de0538b53ca8cf949b0b575d1d597bf92a

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