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(http): handle RSS

+74 -8
+2
README.md
··· 30 30 ``` 31 31 32 32 `build/lasad` is the daemon running the web server. 33 + Run `lasad -h` to get the help. 33 34 34 35 `build/lasa` is a CLI. 36 + Run `lasa -h` to get the help.
+8 -2
client.go
··· 12 12 "tangled.org/anhgelus.world/xrpc/atproto" 13 13 ) 14 14 15 - func NewClient(client *http.Client, resolver *net.Resolver, cache valkey.Client, dur time.Duration) xrpc.Client { 15 + func NewClient( 16 + client *http.Client, 17 + resolver *net.Resolver, 18 + cache valkey.Client, 19 + dur time.Duration, 20 + host string, 21 + ) xrpc.Client { 16 22 client.Timeout = 30 * time.Second 17 23 dir := NewDirectory(atproto.NewDirectory(client, resolver), cache, dur) 18 - return xrpc.NewClient(client, dir, "Lasa/v0.1.0 (Linux; +https://tangled.org/anhgelus.world/lasa)") 24 + return xrpc.NewClient(client, dir, "Lasa/v0.1.0 (Linux; +"+host+")") 19 25 } 20 26 21 27 func ListDocuments(
+1 -1
cmd/lasa/main.go
··· 35 35 handleHelp() 36 36 return 37 37 } 38 - client = lasa.NewClient(http.DefaultClient, net.DefaultResolver, nil, 0) 38 + client = lasa.NewClient(http.DefaultClient, net.DefaultResolver, nil, 0, "local") 39 39 command := args[0] 40 40 var next []string 41 41 if len(args) > 1 {
+1
cmd/lasad/context.go
··· 4 4 5 5 const ( 6 6 keyCfg = iota 7 + keyClient 7 8 )
+53
cmd/lasad/run.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "errors" 5 6 "fmt" 6 7 "log/slog" 8 + "net" 7 9 "net/http" 8 10 "os" 9 11 "os/signal" 10 12 "syscall" 13 + "time" 11 14 15 + "github.com/valkey-io/valkey-go" 16 + site "tangled.org/anhgelus.world/goat-site" 17 + "tangled.org/anhgelus.world/lasa" 12 18 "tangled.org/anhgelus.world/lasa/cmd/internal" 13 19 "tangled.org/anhgelus.world/lasa/cmd/lasad/config" 20 + "tangled.org/anhgelus.world/xrpc" 21 + "tangled.org/anhgelus.world/xrpc/atproto" 14 22 ) 15 23 16 24 func handleRunHelp() { ··· 43 51 } 44 52 ctx = context.WithValue(ctx, keyCfg, cfg) 45 53 54 + var cache valkey.Client 55 + var dur time.Duration 56 + if cfg.Cache != nil { 57 + cache, err = cfg.Cache.Connect() 58 + if err != nil { 59 + panic(err) 60 + } 61 + dur = time.Duration(cfg.Cache.Duration) * time.Minute 62 + } 63 + client := lasa.NewClient(http.DefaultClient, net.DefaultResolver, cache, dur, cfg.Domain) 64 + ctx = context.WithValue(ctx, keyClient, client) 65 + 46 66 mux := http.NewServeMux() 67 + mux.HandleFunc("GET /{id}/{rkey}/rss", func(w http.ResponseWriter, r *http.Request) { 68 + ctx := r.Context() 69 + client := ctx.Value(keyClient).(xrpc.Client) 70 + did, err := lasa.Resolve(ctx, client.Directory(), r.PathValue("id")) 71 + if err != nil { 72 + w.WriteHeader(http.StatusBadRequest) 73 + return 74 + } 75 + rkey, err := atproto.ParseRecordKey(r.PathValue("rkey")) 76 + if err != nil { 77 + w.WriteHeader(http.StatusBadRequest) 78 + return 79 + } 80 + pub, err := xrpc.GetRecord[*site.Publication](ctx, client, did, rkey, nil) 81 + if err != nil { 82 + if err, ok := errors.AsType[xrpc.ErrStandardResponse](err); ok { 83 + if errors.Is(err, xrpc.ErrRecordNotFound) { 84 + w.WriteHeader(http.StatusNotFound) 85 + return 86 + } 87 + panic(err) 88 + } else { 89 + panic(err) 90 + } 91 + } 92 + w.Header().Set("Content-Type", "application/rss+xml") 93 + err = lasa.GenerateRSS(ctx, client, w, did, pub) 94 + if err != nil { 95 + panic(err) 96 + } 97 + }) 98 + mux.HandleFunc("GET /{id}/{$}", func(w http.ResponseWriter, r *http.Request) { 99 + }) 47 100 48 101 ch := make(chan error, 1) 49 102
+9 -5
rss.go
··· 3 3 import ( 4 4 "context" 5 5 "embed" 6 - "html/template" 6 + "html" 7 7 "io" 8 8 "reflect" 9 + "text/template" 9 10 "time" 10 11 11 12 site "tangled.org/anhgelus.world/goat-site" ··· 54 55 return ErrCannotGenerateRSS{"description is not set"} 55 56 } 56 57 data := RSSData{ 57 - Title: pub.Value.Name, 58 + Title: html.EscapeString(pub.Value.Name), 58 59 Link: pub.Value.URL.String(), 59 - Description: *pub.Value.Description, 60 + Description: html.EscapeString(*pub.Value.Description), 60 61 } 61 62 items, err := ListDocuments(ctx, client, author, pub.URI) 62 63 if err != nil { ··· 77 78 return ErrCannotGenerateRSS{"path is not set for " + item.Value.Title} 78 79 } 79 80 url.Path = *item.Value.Path 81 + for i, v := range item.Value.Tags { 82 + item.Value.Tags[i] = html.EscapeString(v) 83 + } 80 84 d := RSSItem{ 81 85 Link: url.String(), 82 - Title: item.Value.Title, 86 + Title: html.EscapeString(item.Value.Title), 83 87 PubDate: item.Value.PublishedAt.Format(time.RFC1123), 84 88 Categories: item.Value.Tags, 85 89 } ··· 87 91 d.Author = "@" + handle.String() 88 92 } 89 93 if item.Value.Description != nil { 90 - d.Description = *item.Value.Description 94 + d.Description = html.EscapeString(*item.Value.Description) 91 95 } 92 96 data.Items[i] = d 93 97 }