all repos — flounder @ 5c25dd5f433771a06e2d3f88173e23bfcdecaf48

A small site builder for the Gemini protocol

gemfeed.go (view raw)

  1// Parses Gemfeed according to the companion spec:
  2// gemini://gemini.circumlunar.space/docs/companion/subscription.gmi
  3package main
  4
  5import (
  6	"bufio"
  7	"fmt"
  8	"github.com/gorilla/feeds"
  9	"io"
 10	"net/url"
 11	"os"
 12	"path"
 13	"path/filepath"
 14	"regexp"
 15	"sort"
 16	"strings"
 17	"time"
 18)
 19
 20type Gemfeed struct {
 21	Title   string
 22	Creator string
 23	Url     *url.URL
 24	Entries []FeedEntry
 25}
 26
 27func (gf *Gemfeed) toAtomFeed() string {
 28	feed := feeds.Feed{
 29		Title:  gf.Title,
 30		Author: &feeds.Author{Name: gf.Creator},
 31		Link:   &feeds.Link{Href: gf.Url.String()},
 32	}
 33	feed.Items = []*feeds.Item{}
 34	for _, fe := range gf.Entries {
 35		feed.Items = append(feed.Items, &feeds.Item{
 36			Title:   fe.Title,
 37			Link:    &feeds.Link{Href: fe.Url.String()}, // Rel=alternate?
 38			Created: fe.Date,                            // Updated not created?
 39		})
 40	}
 41	res, _ := feed.ToAtom()
 42	return res
 43}
 44
 45type FeedEntry struct {
 46	Title      string
 47	Url        *url.URL
 48	Date       time.Time
 49	DateString string
 50	Feed       *Gemfeed
 51	File       string // TODO refactor
 52}
 53
 54func urlFromPath(fullPath string) url.URL {
 55	creator := getCreator(fullPath)
 56	baseUrl := url.URL{}
 57	baseUrl.Host = creator + "." + c.Host
 58	baseUrl.Path = getLocalPath(fullPath)
 59	return baseUrl
 60}
 61
 62// Non-standard extension
 63// Requires yyyy-mm-dd formatted files
 64func generateFeedFromUser(user string) *Gemfeed {
 65	gemlogFolderPath := path.Join(c.FilesDirectory, user, GemlogFolder)
 66	// NOTE: assumes sanitized input
 67	u := urlFromPath(gemlogFolderPath)
 68	feed := Gemfeed{
 69		Title:   strings.Title(user) + "'s Gemlog",
 70		Creator: user,
 71		Url:     &u,
 72	}
 73	err := filepath.Walk(gemlogFolderPath, func(thepath string, info os.FileInfo, err error) error {
 74		base := path.Base(thepath)
 75		if len(base) >= 10 {
 76			entry := FeedEntry{}
 77			date, err := time.Parse("2006-01-02", base[:10])
 78			if err != nil {
 79				return nil
 80			}
 81			entry.Date = date
 82			entry.DateString = base[:10]
 83			entry.Feed = &feed
 84			f, err := os.Open(thepath)
 85			if err != nil {
 86				return nil
 87			}
 88			defer f.Close()
 89			scanner := bufio.NewScanner(f)
 90			for scanner.Scan() {
 91				// skip blank lines
 92				if scanner.Text() == "" {
 93					continue
 94				}
 95				line := scanner.Text()
 96				if strings.HasPrefix(line, "#") {
 97					entry.Title = strings.Trim(line, "# \t")
 98				} else {
 99					var title string
100					if len(line) > 50 {
101						title = line[:50]
102					} else {
103						title = line
104					}
105					entry.Title = "[" + title + "...]"
106				}
107				break
108			}
109			entry.File = getLocalPath(thepath)
110			u := urlFromPath(thepath)
111			entry.Url = &u
112			feed.Entries = append(feed.Entries, entry)
113		}
114		return nil
115	})
116	if err != nil {
117		return nil
118	}
119	// Reverse chronological sort
120	sort.Slice(feed.Entries, func(i, j int) bool {
121		return feed.Entries[i].Date.After(feed.Entries[j].Date)
122	})
123	return &feed
124}
125
126var GemfeedRegex = regexp.MustCompile(`=>\s*(\S+)\s([0-9]{4}-[0-9]{2}-[0-9]{2})\s?-?\s?(.*)`)
127
128// Parsed Gemfeed text Returns error if not a gemfeed
129// Doesn't sort output
130// Doesn't get posts dated in the future
131// if limit > -1 -- limit how many we are getting
132func ParseGemfeed(text io.Reader, baseUrl url.URL, limit int) (*Gemfeed, error) {
133	scanner := bufio.NewScanner(text)
134	gf := Gemfeed{}
135	for scanner.Scan() {
136		if limit > -1 && len(gf.Entries) >= limit {
137			break
138		}
139		line := scanner.Text()
140		if gf.Title == "" && strings.HasPrefix(line, "#") && !strings.HasPrefix(line, "##") {
141			gf.Title = strings.Trim(line[1:], " \t")
142		} else if strings.HasPrefix(line, "=>") {
143			matches := GemfeedRegex.FindStringSubmatch(line)
144			if len(matches) == 4 {
145				parsedUrl, err := url.Parse(matches[1])
146				if err != nil {
147					continue
148				}
149				date, err := time.Parse("2006-01-02", matches[2])
150				if err != nil {
151					continue
152				}
153				title := matches[3]
154				if parsedUrl.Host == "" {
155					// Is relative link
156					parsedUrl.Host = baseUrl.Host
157					parsedUrl.Path = path.Join(path.Dir(baseUrl.Path), parsedUrl.Path)
158				}
159				parsedUrl.Scheme = ""
160				if time.Now().After(date) {
161					fe := FeedEntry{title, parsedUrl, date, matches[2], &gf, ""}
162					if fe.Title == "" {
163						fe.Title = "(Untitled)"
164					}
165					gf.Entries = append(gf.Entries, fe)
166				}
167			}
168		}
169	}
170	if len(gf.Entries) == 0 {
171		return nil, fmt.Errorf("No Gemfeed entries found")
172	}
173	return &gf, nil
174}