Lasa is a stateless proxy that generates a RSS or an Atom feed from a Standard.site publication. lasa.anhgelus.world
rss atom atprotocol standard-site atproto
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 130 lines 2.8 kB view raw
1package lasa 2 3import ( 4 "context" 5 "embed" 6 "html" 7 "io" 8 "reflect" 9 "text/template" 10 "time" 11 12 site "tangled.org/anhgelus.world/goat-site" 13 "tangled.org/anhgelus.world/xrpc" 14 "tangled.org/anhgelus.world/xrpc/atproto" 15) 16 17//go:embed rss.xml 18var rssTemplate embed.FS 19 20type FeedItem struct { 21 Title string 22 Link string 23 Description string 24 PubDate string 25 UpdatedDate string 26 Author string 27 Categories []string 28} 29 30type FeedData struct { 31 // required 32 Title string 33 Link string 34 Description string 35 // optional 36 LastBuildDate string 37 Items []FeedItem 38 Author string 39} 40 41type ErrCannotGenerateFeed struct { 42 v string 43} 44 45func (err ErrCannotGenerateFeed) Error() string { 46 return "cannot generate RSS: " + err.v 47} 48 49func GenerateRSS( 50 ctx context.Context, 51 client xrpc.Client, 52 w io.Writer, 53 author *atproto.DID, 54 pub xrpc.RecordStored[*site.Publication], 55) error { 56 if pub.Value.Description == nil { 57 return ErrCannotGenerateFeed{"description is not set"} 58 } 59 data, err := genFeedData(ctx, client, author, pub) 60 if err != nil { 61 return err 62 } 63 return template.Must(template.New("rss").Funcs(map[string]any{ 64 "isSet": IsSet, 65 }).ParseFS(rssTemplate, "rss.xml")).ExecuteTemplate(w, "rss.xml", data) 66} 67 68func genFeedData( 69 ctx context.Context, 70 client xrpc.Client, 71 author *atproto.DID, 72 pub xrpc.RecordStored[*site.Publication], 73) (FeedData, error) { 74 data := FeedData{ 75 Title: html.EscapeString(pub.Value.Name), 76 Link: pub.Value.URL.String(), 77 } 78 if pub.Value.Description != nil { 79 data.Description = *pub.Value.Description 80 } 81 items, err := ListDocuments(ctx, client, author, pub.URI) 82 if err != nil { 83 return data, err 84 } 85 doc, err := client.Directory().ResolveDID(ctx, author) 86 if err != nil { 87 return data, err 88 } 89 handle, ok := doc.Handle() 90 if ok { 91 data.Author = handle.String() 92 } 93 data.Items = make([]FeedItem, len(items)) 94 for i, item := range items { 95 url := pub.Value.URL 96 if item.Value.Path == nil { 97 return data, ErrCannotGenerateFeed{"path is not set for " + item.Value.Title} 98 } 99 url.Path = *item.Value.Path 100 for i, v := range item.Value.Tags { 101 item.Value.Tags[i] = html.EscapeString(v) 102 } 103 d := FeedItem{ 104 Link: url.String(), 105 Title: html.EscapeString(item.Value.Title), 106 PubDate: item.Value.PublishedAt.Format(time.RFC1123), 107 Categories: item.Value.Tags, 108 } 109 if ok { 110 d.Author = "@" + handle.String() 111 } 112 if item.Value.Description != nil { 113 d.Description = html.EscapeString(*item.Value.Description) 114 } 115 if item.Value.UpdatedAt != nil { 116 d.UpdatedDate = item.Value.UpdatedAt.Format(time.RFC1123) 117 } else { 118 d.UpdatedDate = d.PubDate 119 } 120 if i == 0 { 121 data.LastBuildDate = d.UpdatedDate 122 } 123 data.Items[i] = d 124 } 125 return data, nil 126} 127 128func IsSet(v any) bool { 129 return !reflect.ValueOf(v).IsZero() 130}