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.

feat(pub): generate rss

+177
+1
cmd/lasa/main.go
··· 21 21 22 22 var commands = []internal.Command{ 23 23 {Name: "publication", Usage: "display publications", Callback: handlePublication}, 24 + {Name: "rss", Usage: "generate RSS", Callback: handleRSS}, 24 25 } 25 26 26 27 var client xrpc.Client
+50
cmd/lasa/rss.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "os" 6 + 7 + site "tangled.org/anhgelus.world/goat-site" 8 + "tangled.org/anhgelus.world/lasa" 9 + "tangled.org/anhgelus.world/lasa/cmd/internal" 10 + "tangled.org/anhgelus.world/xrpc" 11 + "tangled.org/anhgelus.world/xrpc/atproto" 12 + ) 13 + 14 + func handleRSSUsage() { 15 + internal.Usage( 16 + `lasa rss <identifier> <rkey>`, 17 + `Generate the RSS for the given publication.`, 18 + nil, 19 + nil, 20 + []string{ 21 + "lasa publication did:web:example.org fooBar\t-\tgenerate RSS publication of did:web:example.org referenced by fooBar", 22 + }, 23 + ) 24 + if !help { 25 + os.Exit(1) 26 + } 27 + } 28 + 29 + func handleRSS(args []string) { 30 + if len(args) != 2 { 31 + handleRSSUsage() 32 + return 33 + } 34 + did, err := lasa.Resolve(context.Background(), client.Directory(), args[0]) 35 + if err != nil { 36 + panic(err) 37 + } 38 + rkey, err := atproto.ParseRecordKey(args[1]) 39 + if err != nil { 40 + return 41 + } 42 + pub, err := xrpc.GetRecord[*site.Publication](context.Background(), client, did, rkey, nil) 43 + if err != nil { 44 + panic(err) 45 + } 46 + err = lasa.GenerateRSS(context.Background(), client, os.Stdout, did, pub) 47 + if err != nil { 48 + panic(err) 49 + } 50 + }
+101
rss.go
··· 1 + package lasa 2 + 3 + import ( 4 + "context" 5 + "embed" 6 + "html/template" 7 + "io" 8 + "reflect" 9 + "time" 10 + 11 + site "tangled.org/anhgelus.world/goat-site" 12 + "tangled.org/anhgelus.world/xrpc" 13 + "tangled.org/anhgelus.world/xrpc/atproto" 14 + ) 15 + 16 + //go:embed rss.xml 17 + var rssTemplate embed.FS 18 + 19 + type RSSItem struct { 20 + Title string 21 + Link string 22 + Description string 23 + PubDate string 24 + Author string 25 + Categories []string 26 + } 27 + 28 + type RSSData struct { 29 + // required 30 + Title string 31 + Link string 32 + Description string 33 + // optional 34 + LastBuildDate string 35 + Items []RSSItem 36 + } 37 + 38 + type ErrCannotGenerateRSS struct { 39 + v string 40 + } 41 + 42 + func (err ErrCannotGenerateRSS) Error() string { 43 + return "cannot generate RSS: " + err.v 44 + } 45 + 46 + func GenerateRSS( 47 + ctx context.Context, 48 + client xrpc.Client, 49 + w io.Writer, 50 + author *atproto.DID, 51 + pub xrpc.RecordStored[*site.Publication], 52 + ) error { 53 + if pub.Value.Description == nil { 54 + return ErrCannotGenerateRSS{"description is not set"} 55 + } 56 + data := RSSData{ 57 + Title: pub.Value.Name, 58 + Link: pub.Value.URL.String(), 59 + Description: *pub.Value.Description, 60 + } 61 + items, err := ListDocuments(ctx, client, author, pub.URI) 62 + if err != nil { 63 + return err 64 + } 65 + doc, err := client.Directory().ResolveDID(ctx, author) 66 + if err != nil { 67 + return err 68 + } 69 + data.Items = make([]RSSItem, len(items)) 70 + handle, ok := doc.Handle() 71 + for i, item := range items { 72 + if i == 0 { 73 + data.LastBuildDate = item.Value.PublishedAt.Format(time.RFC1123) 74 + } 75 + url := pub.Value.URL 76 + if item.Value.Path == nil { 77 + return ErrCannotGenerateRSS{"path is not set for " + item.Value.Title} 78 + } 79 + url.Path = *item.Value.Path 80 + d := RSSItem{ 81 + Link: url.String(), 82 + Title: item.Value.Title, 83 + PubDate: item.Value.PublishedAt.Format(time.RFC1123), 84 + Categories: item.Value.Tags, 85 + } 86 + if ok { 87 + d.Author = "@" + handle.String() 88 + } 89 + if item.Value.Description != nil { 90 + d.Description = *item.Value.Description 91 + } 92 + data.Items[i] = d 93 + } 94 + return template.Must(template.New("rss").Funcs(map[string]any{ 95 + "isSet": isSet, 96 + }).ParseFS(rssTemplate, "rss.xml")).ExecuteTemplate(w, "rss.xml", data) 97 + } 98 + 99 + func isSet(v any) bool { 100 + return !reflect.ValueOf(v).IsZero() 101 + }
+25
rss.xml
··· 1 + <?xml version="1.0"?> 2 + <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> 3 + <channel> 4 + <title>{{ .Title }}</title> 5 + <link>{{ .Link }}</link> 6 + <description>{{ .Description }}</description> 7 + <docs>https://www.rssboard.org/rss-specification</docs> 8 + <generator>Lasa</generator> 9 + {{ if isSet .LastBuildDate -}} 10 + <lastBuildDate>{{ .LastBuildDate }}</lastBuildDate> 11 + {{- end }} 12 + {{ range .Items -}} 13 + <item> 14 + <link>{{ .Link }}</link> 15 + <guid>{{ .Link }}</guid> 16 + {{ if isSet .Title -}}<title>{{ .Title }}</title>{{- end }} 17 + {{ if isSet .Description -}}<description>{{ .Description }}</description>{{- end }} 18 + {{ if isSet .PubDate -}}<pubDate>{{ .PubDate }}</pubDate>{{- end }} 19 + {{ if isSet .Author -}}<author>{{ .Author }}</author>{{- end }} 20 + {{ range .Categories }}<category>{{ . }}</category>{{ end }} 21 + </item> 22 + {{- end -}} 23 + <item></item> 24 + </channel> 25 + </rss>