all repos — flounder @ 548be9c9ac203c4185faff0f5ca17a95a545750f

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