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}