this repo has no description
0
fork

Configure Feed

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

athome: refactor to new house style

+325 -249
+85
cmd/athome/handlers.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + 7 + appbsky "github.com/bluesky-social/indigo/api/bsky" 8 + 9 + "github.com/flosch/pongo2/v6" 10 + "github.com/labstack/echo/v4" 11 + ) 12 + 13 + func (srv *Server) WebHome(c echo.Context) error { 14 + data := pongo2.Context{} 15 + return c.Render(http.StatusOK, "home.html", data) 16 + } 17 + 18 + func (srv *Server) WebPost(c echo.Context) error { 19 + data := pongo2.Context{} 20 + handle := c.Param("handle") 21 + rkey := c.Param("rkey") 22 + // sanity check argument 23 + if len(handle) > 4 && len(handle) < 128 && len(rkey) > 0 { 24 + ctx := c.Request().Context() 25 + // requires two fetches: first fetch profile (!) 26 + pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) 27 + if err != nil { 28 + slog.Warn("failed to fetch handle", "handle", handle, "err", err) 29 + // TODO: only if "not found" 30 + return echo.NewHTTPError(404, "handle not found: %s", handle) 31 + } else { 32 + did := pv.Did 33 + data["did"] = did 34 + 35 + // then fetch the post thread (with extra context) 36 + uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey) 37 + // TODO: more of thread? 38 + tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 1, uri) 39 + if err != nil { 40 + slog.Warn("failed to fetch post", "uri", uri, "err", err) 41 + // TODO: only if "not found" 42 + return echo.NewHTTPError(404, "post not found: %s", handle) 43 + } else { 44 + req := c.Request() 45 + postView := tpv.Thread.FeedDefs_ThreadViewPost.Post 46 + data["postView"] = postView 47 + data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) 48 + if postView.Embed != nil && postView.Embed.EmbedImages_View != nil { 49 + data["imgThumbUrl"] = postView.Embed.EmbedImages_View.Images[0].Thumb 50 + } 51 + } 52 + } 53 + 54 + } 55 + return c.Render(http.StatusOK, "post.html", data) 56 + } 57 + 58 + func (srv *Server) WebProfile(c echo.Context) error { 59 + data := pongo2.Context{} 60 + handle := c.Param("handle") 61 + // sanity check argument 62 + if len(handle) > 4 && len(handle) < 128 { 63 + ctx := c.Request().Context() 64 + pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) 65 + if err != nil { 66 + slog.Warn("failed to fetch handle", "handle", handle, "err", err) 67 + // TODO: only if "not found" 68 + return echo.NewHTTPError(404, "handle not found: %s", handle) 69 + } else { 70 + req := c.Request() 71 + data["profileView"] = pv 72 + data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) 73 + } 74 + af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, handle, "", "", 100) 75 + if err != nil { 76 + slog.Warn("failed to fetch author feed", "handle", handle, "err", err) 77 + // TODO: show some error? 78 + } else { 79 + data["authorFeed"] = af.Feed 80 + //slog.Warn("author feed", "feed", af.Feed) 81 + } 82 + } 83 + 84 + return c.Render(http.StatusOK, "profile.html", data) 85 + }
+32 -31
cmd/athome/main.go
··· 1 1 package main 2 2 3 3 import ( 4 + slogging "log/slog" 4 5 "os" 5 - 6 - _ "github.com/joho/godotenv/autoload" 6 + "fmt" 7 7 8 - logging "github.com/ipfs/go-log" 8 + "github.com/carlmjohnson/versioninfo" 9 9 "github.com/urfave/cli/v2" 10 + 11 + _ "github.com/joho/godotenv/autoload" 10 12 ) 11 13 12 - var log = logging.Logger("pubweb") 13 14 14 - func init() { 15 - logging.SetAllLoggers(logging.LevelDebug) 16 - //logging.SetAllLoggers(logging.LevelWarn) 17 - } 15 + var ( 16 + slog = slogging.New(slogging.NewJSONHandler(os.Stdout, nil)) 17 + version = versioninfo.Short() 18 + ) 18 19 19 20 func main() { 20 - run(os.Args) 21 + if err := run(os.Args); err != nil { 22 + slog.Error("fatal", "err", err) 23 + os.Exit(-1) 24 + } 21 25 } 22 26 23 - func run(args []string) { 27 + func run(args []string) error { 24 28 25 29 app := cli.App{ 26 - Name: "pubweb", 27 - Usage: "public web interface to bluesky content", 30 + Name: "athome", 31 + Usage: "public web interface to bluesky account content", 28 32 } 29 33 30 34 app.Commands = []*cli.Command{ ··· 34 38 Action: serve, 35 39 Flags: []cli.Flag{ 36 40 &cli.StringFlag{ 37 - Name: "pds-host", 38 - Usage: "method, hostname, and port of PDS instance", 39 - Value: "http://localhost:4849", 40 - EnvVars: []string{"ATP_PDS_HOST"}, 41 + Name: "appview-host", 42 + Usage: "method, hostname, and port of AppView instance", 43 + Value: "https://api.bsky.app", 44 + EnvVars: []string{"ATP_APPVIEW_HOST"}, 41 45 }, 42 46 &cli.StringFlag{ 43 - Name: "handle", 44 - Usage: "for PDS login", 45 - Required: true, 46 - EnvVars: []string{"ATP_AUTH_HANDLE"}, 47 - }, 48 - &cli.StringFlag{ 49 - Name: "password", 50 - Usage: "for PDS login", 51 - Required: true, 52 - EnvVars: []string{"ATP_AUTH_PASSWORD"}, 53 - }, 54 - &cli.StringFlag{ 55 - Name: "http-address", 47 + Name: "bind", 56 48 Usage: "Specify the local IP/port to bind to", 57 49 Required: false, 58 50 Value: ":8200", 59 - EnvVars: []string{"PUBWEB_BIND"}, 51 + EnvVars: []string{"ATHOME_BIND"}, 60 52 }, 61 53 &cli.BoolFlag{ 62 54 Name: "debug", ··· 67 59 }, 68 60 }, 69 61 }, 62 + &cli.Command{ 63 + Name: "version", 64 + Usage: "print version", 65 + Action: func(cctx *cli.Context) error { 66 + fmt.Println(version) 67 + return nil 68 + }, 69 + }, 70 70 } 71 - app.RunAndExitOnError() 71 + 72 + return app.Run(args) 72 73 }
-218
cmd/athome/server.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "io/fs" 7 - "net/http" 8 - "os" 9 - "strings" 10 - "embed" 11 - 12 - comatproto "github.com/bluesky-social/indigo/api/atproto" 13 - appbsky "github.com/bluesky-social/indigo/api/bsky" 14 - cliutil "github.com/bluesky-social/indigo/cmd/gosky/util" 15 - "github.com/bluesky-social/indigo/xrpc" 16 - 17 - "github.com/flosch/pongo2/v6" 18 - "github.com/labstack/echo/v4" 19 - "github.com/labstack/echo/v4/middleware" 20 - "github.com/urfave/cli/v2" 21 - ) 22 - 23 - //go:embed static/* 24 - var StaticFS embed.FS 25 - 26 - type Server struct { 27 - xrpcc *xrpc.Client 28 - } 29 - 30 - func serve(cctx *cli.Context) error { 31 - debug := cctx.Bool("debug") 32 - httpAddress := cctx.String("http-address") 33 - pdsHost := cctx.String("pds-host") 34 - atpHandle := cctx.String("handle") 35 - atpPassword := cctx.String("password") 36 - 37 - // create a new session 38 - // TODO: does this work with no auth at all? 39 - xrpcc := &xrpc.Client{ 40 - Client: cliutil.NewHttpClient(), 41 - Host: pdsHost, 42 - Auth: &xrpc.AuthInfo{ 43 - Handle: atpHandle, 44 - }, 45 - } 46 - 47 - auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{ 48 - Identifier: xrpcc.Auth.Handle, 49 - Password: atpPassword, 50 - }) 51 - if err != nil { 52 - return err 53 - } 54 - xrpcc.Auth.AccessJwt = auth.AccessJwt 55 - xrpcc.Auth.RefreshJwt = auth.RefreshJwt 56 - xrpcc.Auth.Did = auth.Did 57 - xrpcc.Auth.Handle = auth.Handle 58 - 59 - server := Server{xrpcc} 60 - 61 - staticHandler := http.FileServer(func() http.FileSystem { 62 - if debug { 63 - return http.FS(os.DirFS("static")) 64 - } 65 - fsys, err := fs.Sub(StaticFS, "static") 66 - if err != nil { 67 - log.Fatal(err) 68 - } 69 - return http.FS(fsys) 70 - }()) 71 - 72 - e := echo.New() 73 - e.HideBanner = true 74 - // SECURITY: Do not modify without due consideration. 75 - e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ 76 - ContentTypeNosniff: "nosniff", 77 - XFrameOptions: "SAMEORIGIN", 78 - HSTSMaxAge: 31536000, // 365 days 79 - // TODO: 80 - // ContentSecurityPolicy 81 - // XSSProtection 82 - })) 83 - e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ 84 - // Don't log requests for static content. 85 - Skipper: func(c echo.Context) bool { 86 - return strings.HasPrefix(c.Request().URL.Path, "/static") 87 - }, 88 - Format: "method=${method} path=${uri} status=${status} latency=${latency_human}\n", 89 - })) 90 - e.Renderer = NewRenderer("templates/", &TemplateFS, debug) 91 - e.HTTPErrorHandler = customHTTPErrorHandler 92 - 93 - // redirect trailing slash to non-trailing slash. 94 - // all of our current endpoints have no trailing slash. 95 - e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ 96 - RedirectCode: http.StatusFound, 97 - })) 98 - 99 - // basic static routes 100 - e.GET("/robots.txt", echo.WrapHandler(staticHandler)) 101 - e.GET("/favicon.ico", echo.WrapHandler(staticHandler)) 102 - e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) 103 - e.GET("/", server.WebHome) 104 - 105 - // generic routes 106 - //e.GET("/search", server.WebGeneric) 107 - //e.GET("/support", server.WebGeneric) 108 - //e.GET("/support/privacy", server.WebGeneric) 109 - //e.GET("/support/tos", server.WebGeneric) 110 - //e.GET("/support/community-guidelines", server.WebGeneric) 111 - //e.GET("/support/copyright", server.WebGeneric) 112 - 113 - // profile endpoints; only first populates info 114 - e.GET("/profile/:handle", server.WebProfile) 115 - //e.GET("/profile/:handle/repo.car.gz", server.WebProfile) 116 - //e.GET("/profile/:handle/follows", server.WebGeneric) 117 - //e.GET("/profile/:handle/followers", server.WebGeneric) 118 - 119 - // post endpoints; only first populates info 120 - e.GET("/profile/:handle/post/:rkey", server.WebPost) 121 - //e.GET("/profile/:handle/post/:rkey/liked-by", server.WebGeneric) 122 - //e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric) 123 - 124 - // feeds 125 - e.GET("/feed/:name", server.WebFeed) 126 - 127 - // redirect 128 - //e.GET("/at://:account", server.WebAccountURI) 129 - //e.GET("/at://:account/:nsid/:rkey", server.WebRecordURI) 130 - 131 - log.Infow("starting server", "bind", httpAddress) 132 - return e.Start(httpAddress) 133 - } 134 - 135 - func customHTTPErrorHandler(err error, c echo.Context) { 136 - code := http.StatusInternalServerError 137 - if he, ok := err.(*echo.HTTPError); ok { 138 - code = he.Code 139 - } 140 - c.Logger().Error(err) 141 - data := pongo2.Context{ 142 - "statusCode": code, 143 - } 144 - c.Render(code, "error.html", data) 145 - } 146 - 147 - func (srv *Server) WebHome(c echo.Context) error { 148 - data := pongo2.Context{} 149 - return c.Render(http.StatusOK, "home.html", data) 150 - } 151 - 152 - func (srv *Server) WebPost(c echo.Context) error { 153 - data := pongo2.Context{} 154 - handle := c.Param("handle") 155 - rkey := c.Param("rkey") 156 - // sanity check argument 157 - if len(handle) > 4 && len(handle) < 128 && len(rkey) > 0 { 158 - ctx := c.Request().Context() 159 - // requires two fetches: first fetch profile (!) 160 - pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) 161 - if err != nil { 162 - log.Warnf("failed to fetch handle: %s\t%v", handle, err) 163 - // TODO: only if "not found" 164 - return echo.NewHTTPError(404, "handle not found: %s", handle) 165 - } else { 166 - did := pv.Did 167 - data["did"] = did 168 - 169 - // then fetch the post thread (with extra context) 170 - uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey) 171 - tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, uri) 172 - if err != nil { 173 - log.Warnf("failed to fetch post: %s\t%v", uri, err) 174 - // TODO: only if "not found" 175 - return echo.NewHTTPError(404, "post not found: %s", handle) 176 - } else { 177 - req := c.Request() 178 - postView := tpv.Thread.FeedDefs_ThreadViewPost.Post 179 - data["postView"] = postView 180 - data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) 181 - if postView.Embed != nil && postView.Embed.EmbedImages_View != nil { 182 - data["imgThumbUrl"] = postView.Embed.EmbedImages_View.Images[0].Thumb 183 - } 184 - } 185 - } 186 - 187 - } 188 - return c.Render(http.StatusOK, "post.html", data) 189 - } 190 - 191 - func (srv *Server) WebProfile(c echo.Context) error { 192 - data := pongo2.Context{} 193 - handle := c.Param("handle") 194 - // sanity check argument 195 - if len(handle) > 4 && len(handle) < 128 { 196 - ctx := c.Request().Context() 197 - pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) 198 - if err != nil { 199 - log.Warnw("failed to fetch handle", "handle", handle, "err", err) 200 - // TODO: only if "not found" 201 - return echo.NewHTTPError(404, "handle not found: %s", handle) 202 - } else { 203 - req := c.Request() 204 - data["profileView"] = pv 205 - data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) 206 - } 207 - af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, handle, "", 100) 208 - if err != nil { 209 - log.Warnw("failed to fetch author feed", "handle", handle, "err", err) 210 - // TODO: show some error? 211 - } else { 212 - data["authorFeed"] = af.Feed 213 - //log.Warnw("author feed", "feed", af.Feed) 214 - } 215 - } 216 - 217 - return c.Render(http.StatusOK, "profile.html", data) 218 - }
+202
cmd/athome/service.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "io/fs" 6 + "net/http" 7 + "os" 8 + "time" 9 + "embed" 10 + "os/signal" 11 + "errors" 12 + "syscall" 13 + 14 + "github.com/bluesky-social/indigo/xrpc" 15 + "github.com/bluesky-social/indigo/util" 16 + 17 + "github.com/urfave/cli/v2" 18 + "github.com/flosch/pongo2/v6" 19 + "github.com/labstack/echo/v4" 20 + "github.com/labstack/echo/v4/middleware" 21 + "github.com/labstack/echo-contrib/echoprometheus" 22 + slogecho "github.com/samber/slog-echo" 23 + ) 24 + 25 + //go:embed static/* 26 + var StaticFS embed.FS 27 + 28 + type Server struct { 29 + echo *echo.Echo 30 + httpd *http.Server 31 + xrpcc *xrpc.Client 32 + } 33 + 34 + func serve(cctx *cli.Context) error { 35 + debug := cctx.Bool("debug") 36 + httpAddress := cctx.String("bind") 37 + appviewHost := cctx.String("appview-host") 38 + 39 + xrpcc := &xrpc.Client{ 40 + Client: util.RobustHTTPClient(), 41 + Host: appviewHost, 42 + // Headers: version 43 + } 44 + e := echo.New() 45 + 46 + // httpd 47 + var ( 48 + httpTimeout = 1 * time.Minute 49 + httpMaxHeaderBytes = 1 * (1024 * 1024) 50 + ) 51 + 52 + srv := &Server{ 53 + echo: e, 54 + xrpcc: xrpcc, 55 + } 56 + srv.httpd = &http.Server{ 57 + Handler: srv, 58 + Addr: httpAddress, 59 + WriteTimeout: httpTimeout, 60 + ReadTimeout: httpTimeout, 61 + MaxHeaderBytes: httpMaxHeaderBytes, 62 + } 63 + 64 + e.HideBanner = true 65 + e.Use(slogecho.New(slog)) 66 + e.Use(middleware.Recover()) 67 + e.Use(echoprometheus.NewMiddleware("athome")) 68 + e.Use(middleware.BodyLimit("64M")) 69 + e.HTTPErrorHandler = srv.errorHandler 70 + e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ 71 + ContentTypeNosniff: "nosniff", 72 + XFrameOptions: "SAMEORIGIN", 73 + HSTSMaxAge: 31536000, // 365 days 74 + // TODO: 75 + // ContentSecurityPolicy 76 + // XSSProtection 77 + })) 78 + 79 + // redirect trailing slash to non-trailing slash. 80 + // all of our current endpoints have no trailing slash. 81 + e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ 82 + RedirectCode: http.StatusFound, 83 + })) 84 + 85 + staticHandler := http.FileServer(func() http.FileSystem { 86 + if debug { 87 + return http.FS(os.DirFS("static")) 88 + } 89 + fsys, err := fs.Sub(StaticFS, "static") 90 + if err != nil { 91 + slog.Error("static template error", "err", err) 92 + os.Exit(-1) 93 + } 94 + return http.FS(fsys) 95 + }()) 96 + 97 + e.Renderer = NewRenderer("templates/", &TemplateFS, debug) 98 + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) 99 + 100 + e.GET("/_health", srv.HandleHealthCheck) 101 + e.GET("/metrics", echoprometheus.NewHandler()) 102 + 103 + // basic static routes 104 + e.GET("/robots.txt", echo.WrapHandler(staticHandler)) 105 + e.GET("/favicon.ico", echo.WrapHandler(staticHandler)) 106 + e.GET("/", srv.WebHome) 107 + 108 + // generic routes 109 + //e.GET("/search", srv.WebGeneric) 110 + //e.GET("/support", srv.WebGeneric) 111 + //e.GET("/support/privacy", srv.WebGeneric) 112 + //e.GET("/support/tos", srv.WebGeneric) 113 + //e.GET("/support/community-guidelines", srv.WebGeneric) 114 + //e.GET("/support/copyright", srv.WebGeneric) 115 + 116 + // profile endpoints; only first populates info 117 + e.GET("/profile/:handle", srv.WebProfile) 118 + //e.GET("/profile/:handle/repo.car.gz", srv.WebProfile) 119 + //e.GET("/profile/:handle/follows", srv.WebGeneric) 120 + //e.GET("/profile/:handle/followers", srv.WebGeneric) 121 + 122 + // post endpoints; only first populates info 123 + e.GET("/profile/:handle/post/:rkey", srv.WebPost) 124 + //e.GET("/profile/:handle/post/:rkey/liked-by", srv.WebGeneric) 125 + //e.GET("/profile/:handle/post/:rkey/reposted-by", srv.WebGeneric) 126 + 127 + // feeds 128 + //e.GET("/feed/:name", srv.WebFeed) 129 + 130 + // redirect 131 + //e.GET("/at://:account", srv.WebAccountURI) 132 + //e.GET("/at://:account/:nsid/:rkey", srv.WebRecordURI) 133 + 134 + // Start the server 135 + slog.Info("starting server", "bind", httpAddress) 136 + go func() { 137 + if err := srv.httpd.ListenAndServe(); err != nil { 138 + if !errors.Is(err, http.ErrServerClosed) { 139 + slog.Error("HTTP server shutting down unexpectedly", "err", err) 140 + } 141 + } 142 + }() 143 + 144 + // Wait for a signal to exit. 145 + slog.Info("registering OS exit signal handler") 146 + quit := make(chan struct{}) 147 + exitSignals := make(chan os.Signal, 1) 148 + signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) 149 + go func() { 150 + sig := <-exitSignals 151 + slog.Info("received OS exit signal", "signal", sig) 152 + 153 + // Shut down the HTTP server 154 + if err := srv.Shutdown(); err != nil { 155 + slog.Error("HTTP server shutdown error", "err", err) 156 + } 157 + 158 + // Trigger the return that causes an exit. 159 + close(quit) 160 + }() 161 + <-quit 162 + slog.Info("graceful shutdown complete") 163 + return nil 164 + } 165 + 166 + type GenericStatus struct { 167 + Daemon string `json:"daemon"` 168 + Status string `json:"status"` 169 + Message string `json:"msg,omitempty"` 170 + } 171 + 172 + func (srv *Server) errorHandler(err error, c echo.Context) { 173 + code := http.StatusInternalServerError 174 + if he, ok := err.(*echo.HTTPError); ok { 175 + code = he.Code 176 + } 177 + if code >= 500 { 178 + slog.Warn("abyss-http-internal-error", "err", err) 179 + } 180 + data := pongo2.Context{ 181 + "statusCode": code, 182 + } 183 + c.Render(code, "error.html", data) 184 + } 185 + 186 + func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 187 + srv.echo.ServeHTTP(rw, req) 188 + } 189 + 190 + func (srv *Server) Shutdown() error { 191 + slog.Info("shutting down") 192 + 193 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 194 + defer cancel() 195 + 196 + return srv.httpd.Shutdown(ctx) 197 + } 198 + 199 + func (s *Server) HandleHealthCheck(c echo.Context) error { 200 + return c.JSON(200, GenericStatus{Status: "ok", Daemon: "abyss"}) 201 + } 202 +
+2
go.mod
··· 6 6 contrib.go.opencensus.io/exporter/prometheus v0.4.2 7 7 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de 8 8 github.com/brianvoe/gofakeit/v6 v6.20.2 9 + github.com/carlmjohnson/versioninfo v0.22.5 9 10 github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49 11 + github.com/flosch/pongo2/v6 v6.0.0 10 12 github.com/goccy/go-json v0.10.2 11 13 github.com/gocql/gocql v1.6.0 12 14 github.com/golang-jwt/jwt v3.2.2+incompatible
+4
go.sum
··· 73 73 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 74 74 github.com/brianvoe/gofakeit/v6 v6.20.2 h1:FLloufuC7NcbHqDzVQ42CG9AKryS1gAGCRt8nQRsW+Y= 75 75 github.com/brianvoe/gofakeit/v6 v6.20.2/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= 76 + github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= 77 + github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= 76 78 github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 77 79 github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 78 80 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= ··· 117 119 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 118 120 github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= 119 121 github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 122 + github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= 123 + github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= 120 124 github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= 121 125 github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 122 126 github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=