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}