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 FeedUrl *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 feed.FeedUrl = &baseUrl
50 feedEntries = append(feedEntries, feed.Entries...)
51 }
52 }
53 return nil
54 })
55 if err != nil {
56 return nil, err
57 } else {
58 sort.Slice(feedEntries, func(i, j int) bool {
59 return feedEntries[i].Date.After(feedEntries[j].Date)
60 })
61 if len(feedEntries) > maxItems {
62 return feedEntries[:maxItems], nil
63 }
64 return feedEntries, nil
65 }
66}
67
68// Parsed Gemfeed text Returns error if not a gemfeed
69// Doesn't sort output
70// Doesn't get posts dated in the future
71// if limit > -1 -- limit how many we are getting
72func ParseGemfeed(text io.Reader, baseUrl url.URL, limit int) (*Gemfeed, error) {
73 scanner := bufio.NewScanner(text)
74 gf := Gemfeed{}
75 for scanner.Scan() {
76 if limit > -1 && len(gf.Entries) >= limit {
77 break
78 }
79 line := scanner.Text()
80 if gf.Title == "" && strings.HasPrefix(line, "#") && !strings.HasPrefix(line, "##") {
81 gf.Title = strings.Trim(line[1:], " \t")
82 } else if strings.HasPrefix(line, "=>") {
83 link := strings.Trim(line[2:], " \t")
84 splits := strings.SplitN(link, " ", 2)
85 if len(splits) == 2 && len(splits[1]) >= 10 {
86 dateString := splits[1][:10]
87 date, err := time.Parse("2006-01-02", dateString)
88 if err != nil {
89 continue
90 }
91 parsedUrl, err := url.Parse(splits[0])
92 if err != nil {
93 continue
94 }
95 if parsedUrl.Host == "" {
96 // Is relative link
97 parsedUrl.Host = baseUrl.Host
98 parsedUrl.Path = path.Join(path.Dir(baseUrl.Path), parsedUrl.Path)
99 }
100 parsedUrl.Scheme = ""
101 if time.Now().After(date) {
102 title := strings.Trim(splits[1][10:], " -\t")
103 fe := FeedEntry{title, parsedUrl, date, dateString, &gf}
104 gf.Entries = append(gf.Entries, &fe)
105 }
106 }
107 }
108 }
109 if len(gf.Entries) == 0 {
110 return nil, fmt.Errorf("No Gemfeed entries found")
111 }
112 return &gf, nil
113}