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.

style(server): move http server in own package

+159 -142
cmd/lasad/author.html cmd/lasad/server/author.html
+1 -1
cmd/lasad/context.go cmd/lasad/server/context.go
··· 1 - package main 1 + package server 2 2 3 3 type Key uint 4 4
+1 -1
cmd/lasad/directory.go cmd/lasad/server/directory.go
··· 1 - package main 1 + package server 2 2 3 3 import ( 4 4 "bytes"
+1 -1
cmd/lasad/errors.go cmd/lasad/server/errors.go
··· 1 - package main 1 + package server 2 2 3 3 import ( 4 4 "errors"
cmd/lasad/index.html cmd/lasad/server/index.html
+1 -1
cmd/lasad/limiter.go cmd/lasad/server/limiter.go
··· 1 - package main 1 + package server 2 2 3 3 import ( 4 4 "log/slog"
+2 -138
cmd/lasad/run.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "embed" 6 - "errors" 7 - "fmt" 8 5 "log/slog" 9 6 "net" 10 7 "net/http" 11 8 "os" 12 9 "os/signal" 13 - "strings" 14 10 "syscall" 15 11 "time" 16 12 ··· 18 14 "tangled.org/anhgelus.world/lasa" 19 15 "tangled.org/anhgelus.world/lasa/cmd/internal" 20 16 "tangled.org/anhgelus.world/lasa/cmd/lasad/config" 21 - "tangled.org/anhgelus.world/xrpc" 17 + "tangled.org/anhgelus.world/lasa/cmd/lasad/server" 22 18 ) 23 - 24 - //go:embed index.html author.html 25 - var files embed.FS 26 19 27 20 func handleRunHelp() { 28 21 internal.Usage( ··· 39 32 } 40 33 } 41 34 42 - type Publication struct { 43 - URL string 44 - Link string 45 - Name string 46 - RKey string 47 - } 48 - 49 35 func handleRun(args []string) { 50 36 if len(args) != 0 || help { 51 37 handleRunHelp() ··· 59 45 if err != nil { 60 46 panic(err) 61 47 } 62 - ctx = context.WithValue(ctx, keyCfg, cfg) 63 - 64 48 var cache *glide.Client 65 49 var dur time.Duration 66 50 if cfg.Cache != nil { ··· 72 56 dur = time.Duration(cfg.Cache.Duration) * time.Minute 73 57 } 74 58 client := lasa.NewClient(http.DefaultClient, net.DefaultResolver, cache, dur, cfg.Domain) 75 - ctx = context.WithValue(ctx, keyClient, client) 76 - ctx = context.WithValue(ctx, keyDir, NewDirectory(cache, dur)) 77 - ctx = context.WithValue(ctx, keyLimiter, &Limiter{limited: make(map[string]*limited)}) 78 - 79 - mux := http.NewServeMux() 80 - mux.HandleFunc("GET /{id}/{rkey}/rss", func(w http.ResponseWriter, r *http.Request) { 81 - dir := r.Context().Value(keyDir).(*Directory) 82 - err := dir.Feed(r.Context(), w, r, "rss", lasa.GenerateRSS) 83 - if err != nil { 84 - HandleErrors(w, err) 85 - return 86 - } 87 - }) 88 - mux.HandleFunc("GET /{id}/{rkey}/atom", func(w http.ResponseWriter, r *http.Request) { 89 - dir := r.Context().Value(keyDir).(*Directory) 90 - err := dir.Feed(r.Context(), w, r, "atom", lasa.GenerateAtom) 91 - if err != nil { 92 - HandleErrors(w, err) 93 - return 94 - } 95 - }) 96 - mux.HandleFunc("GET /{id}/{$}", func(w http.ResponseWriter, r *http.Request) { 97 - ctx := r.Context() 98 - client := ctx.Value(keyClient).(xrpc.Client) 99 - did, err := lasa.Resolve(ctx, client.Directory(), r.PathValue("id")) 100 - if err != nil { 101 - HandleErrors(w, err) 102 - return 103 - } 104 - dir := ctx.Value(keyDir).(*Directory) 105 - b, err := dir.Author(ctx, did) 106 - if err != nil { 107 - HandleErrors(w, err) 108 - return 109 - } 110 - w.Write(b) 111 - }) 112 - mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { 113 - b, err := files.ReadFile("index.html") 114 - if err != nil { 115 - HandleErrors(w, err) 116 - return 117 - } 118 - w.Write(b) 119 - }) 120 59 121 60 ch := make(chan error, 1) 122 61 123 62 go func() { 124 63 slog.Info("starting") 125 - ch <- http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), middlewares(mux, ctx)) 64 + ch <- server.Run(ctx, cfg, client, cache, dur) 126 65 }() 127 66 select { 128 67 case <-ctx.Done(): ··· 134 73 panic(err) 135 74 } 136 75 } 137 - 138 - type statusWriter struct { 139 - http.ResponseWriter 140 - code int 141 - } 142 - 143 - func (w *statusWriter) WriteHeader(statusCode int) { 144 - w.ResponseWriter.WriteHeader(statusCode) 145 - w.code = statusCode 146 - } 147 - 148 - func middlewares(h http.Handler, parent context.Context) http.Handler { 149 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 150 - if strings.HasPrefix(r.RequestURI, "/favicon.ico") { 151 - w.WriteHeader(http.StatusNotFound) 152 - return 153 - } 154 - 155 - // timeouts request handling 156 - ctx, cancel := context.WithTimeoutCause(parent, 15*time.Second, errors.New("handling timeouts")) 157 - defer cancel() 158 - 159 - var cancelCause context.CancelCauseFunc 160 - ctx, cancelCause = context.WithCancelCause(ctx) 161 - defer cancelCause(errors.New("handling finished")) 162 - 163 - limiter := ctx.Value(keyLimiter).(*Limiter) 164 - status := &statusWriter{w, http.StatusOK} 165 - log := slog.With("uri", r.RequestURI) 166 - if limiter.isLimited(r) { 167 - status.WriteHeader(http.StatusTooManyRequests) 168 - limiter.handle(status, r, log.With("status", http.StatusTooManyRequests)) 169 - return 170 - } 171 - 172 - now := time.Now() 173 - defer func() { 174 - if err := recover(); err != nil { 175 - w.WriteHeader(http.StatusInternalServerError) 176 - log.Error("panic!", "error", err, "duration", time.Since(now)) 177 - switch e := err.(type) { 178 - case error: 179 - cancelCause(e) 180 - case string: 181 - cancelCause(errors.New(e)) 182 - default: 183 - log.Warn( 184 - "cannot set cancel cause, because error type is not supported", 185 - "type", fmt.Sprintf("%T", e), 186 - ) 187 - } 188 - } 189 - }() 190 - 191 - h.ServeHTTP(status, r.WithContext(ctx)) 192 - 193 - log = log.With("status", status.code, "duration", time.Since(now)) 194 - if status.code < 400 { 195 - log.Debug("handled") 196 - } else if status.code < 500 { 197 - cfg := ctx.Value(keyCfg).(*config.Config) 198 - level := slog.LevelDebug 199 - if (status.code == http.StatusNotFound && cfg.LogNotFound) || 200 - (status.code == http.StatusBadRequest && cfg.LogBadRequest) || 201 - (status.code != http.StatusNotFound && status.code != http.StatusBadRequest) { 202 - level = slog.LevelWarn 203 - } 204 - log.Log(context.Background(), level, "invalid request") 205 - } else { 206 - log.Error("error while handling request") 207 - } 208 - 209 - limiter.handle(status, r, log) 210 - }) 211 - }
+153
cmd/lasad/server/run.go
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "embed" 6 + "errors" 7 + "fmt" 8 + "log/slog" 9 + "net/http" 10 + "strings" 11 + "time" 12 + 13 + glide "github.com/valkey-io/valkey-glide/go/v2" 14 + "tangled.org/anhgelus.world/lasa" 15 + "tangled.org/anhgelus.world/lasa/cmd/lasad/config" 16 + "tangled.org/anhgelus.world/xrpc" 17 + ) 18 + 19 + //go:embed index.html author.html 20 + var files embed.FS 21 + 22 + type Publication struct { 23 + URL string 24 + Link string 25 + Name string 26 + RKey string 27 + } 28 + 29 + func Run(ctx context.Context, cfg *config.Config, client xrpc.Client, cache *glide.Client, dur time.Duration) error { 30 + ctx = context.WithValue(ctx, keyCfg, cfg) 31 + ctx = context.WithValue(ctx, keyClient, client) 32 + ctx = context.WithValue(ctx, keyDir, NewDirectory(cache, dur)) 33 + ctx = context.WithValue(ctx, keyLimiter, &Limiter{limited: make(map[string]*limited)}) 34 + 35 + mux := http.NewServeMux() 36 + mux.HandleFunc("GET /{id}/{rkey}/rss", func(w http.ResponseWriter, r *http.Request) { 37 + dir := r.Context().Value(keyDir).(*Directory) 38 + err := dir.Feed(r.Context(), w, r, "rss", lasa.GenerateRSS) 39 + if err != nil { 40 + HandleErrors(w, err) 41 + return 42 + } 43 + }) 44 + mux.HandleFunc("GET /{id}/{rkey}/atom", func(w http.ResponseWriter, r *http.Request) { 45 + dir := r.Context().Value(keyDir).(*Directory) 46 + err := dir.Feed(r.Context(), w, r, "atom", lasa.GenerateAtom) 47 + if err != nil { 48 + HandleErrors(w, err) 49 + return 50 + } 51 + }) 52 + mux.HandleFunc("GET /{id}/{$}", func(w http.ResponseWriter, r *http.Request) { 53 + ctx := r.Context() 54 + client := ctx.Value(keyClient).(xrpc.Client) 55 + did, err := lasa.Resolve(ctx, client.Directory(), r.PathValue("id")) 56 + if err != nil { 57 + HandleErrors(w, err) 58 + return 59 + } 60 + dir := ctx.Value(keyDir).(*Directory) 61 + b, err := dir.Author(ctx, did) 62 + if err != nil { 63 + HandleErrors(w, err) 64 + return 65 + } 66 + w.Write(b) 67 + }) 68 + mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { 69 + b, err := files.ReadFile("index.html") 70 + if err != nil { 71 + HandleErrors(w, err) 72 + return 73 + } 74 + w.Write(b) 75 + }) 76 + 77 + return http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), middlewares(mux, ctx)) 78 + } 79 + 80 + type statusWriter struct { 81 + http.ResponseWriter 82 + code int 83 + } 84 + 85 + func (w *statusWriter) WriteHeader(statusCode int) { 86 + w.ResponseWriter.WriteHeader(statusCode) 87 + w.code = statusCode 88 + } 89 + 90 + func middlewares(h http.Handler, parent context.Context) http.Handler { 91 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 92 + if strings.HasPrefix(r.RequestURI, "/favicon.ico") { 93 + w.WriteHeader(http.StatusNotFound) 94 + return 95 + } 96 + 97 + // timeouts request handling 98 + ctx, cancel := context.WithTimeoutCause(parent, 15*time.Second, errors.New("handling timeouts")) 99 + defer cancel() 100 + 101 + var cancelCause context.CancelCauseFunc 102 + ctx, cancelCause = context.WithCancelCause(ctx) 103 + defer cancelCause(errors.New("handling finished")) 104 + 105 + limiter := ctx.Value(keyLimiter).(*Limiter) 106 + status := &statusWriter{w, http.StatusOK} 107 + log := slog.With("uri", r.RequestURI) 108 + if limiter.isLimited(r) { 109 + status.WriteHeader(http.StatusTooManyRequests) 110 + limiter.handle(status, r, log.With("status", http.StatusTooManyRequests)) 111 + return 112 + } 113 + 114 + now := time.Now() 115 + defer func() { 116 + if err := recover(); err != nil { 117 + w.WriteHeader(http.StatusInternalServerError) 118 + log.Error("panic!", "error", err, "duration", time.Since(now)) 119 + switch e := err.(type) { 120 + case error: 121 + cancelCause(e) 122 + case string: 123 + cancelCause(errors.New(e)) 124 + default: 125 + log.Warn( 126 + "cannot set cancel cause, because error type is not supported", 127 + "type", fmt.Sprintf("%T", e), 128 + ) 129 + } 130 + } 131 + }() 132 + 133 + h.ServeHTTP(status, r.WithContext(ctx)) 134 + 135 + log = log.With("status", status.code, "duration", time.Since(now)) 136 + if status.code < 400 { 137 + log.Debug("handled") 138 + } else if status.code < 500 { 139 + cfg := ctx.Value(keyCfg).(*config.Config) 140 + level := slog.LevelDebug 141 + if (status.code == http.StatusNotFound && cfg.LogNotFound) || 142 + (status.code == http.StatusBadRequest && cfg.LogBadRequest) || 143 + (status.code != http.StatusNotFound && status.code != http.StatusBadRequest) { 144 + level = slog.LevelWarn 145 + } 146 + log.Log(context.Background(), level, "invalid request") 147 + } else { 148 + log.Error("error while handling request") 149 + } 150 + 151 + limiter.handle(status, r, log) 152 + }) 153 + }