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}