this repo has no description
0
fork

Configure Feed

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

astrolabe: crude web UI for exploring atmosphere (#725)

- [x] remove footer
- [x] fix double-rendered error page
- [x] fix unknown lexicons not listing
- [ ] link-ify AT-URIs in JSON
- [ ] better table aesthetics
- [ ] better aesthetics generally

authored by

bnewbold and committed by
GitHub
49420105 502cf77b

+845
+39
cmd/astrolabe/README.md
··· 1 + 2 + astrolabe: basic atproto network data explorer 3 + ============================================== 4 + 5 + ⚠️ This is a fun little proof-of-concept ⚠️ 6 + 7 + 8 + ## Run It 9 + 10 + The recommended way to run `astrolabe` is behind a `caddy` HTTPS server which does automatic on-demand SSL certificate registration (using Let's Encrypt). 11 + 12 + Build and run `astrolabe`: 13 + 14 + go build ./cmd/astrolabe 15 + 16 + # will listen on :8400 by default 17 + ./astrolabe serve 18 + 19 + Create a `Caddyfile`: 20 + 21 + ``` 22 + { 23 + on_demand_tls { 24 + interval 1h 25 + burst 8 26 + } 27 + } 28 + 29 + :443 { 30 + reverse_proxy localhost:8400 31 + tls YOUREMAIL@example.com { 32 + on_demand 33 + } 34 + } 35 + ``` 36 + 37 + Run `caddy`: 38 + 39 + caddy run
+243
cmd/astrolabe/handlers.go
··· 1 + package main 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + "strings" 8 + 9 + comatproto "github.com/bluesky-social/indigo/api/atproto" 10 + _ "github.com/bluesky-social/indigo/api/bsky" 11 + "github.com/bluesky-social/indigo/atproto/data" 12 + "github.com/bluesky-social/indigo/atproto/identity" 13 + "github.com/bluesky-social/indigo/atproto/syntax" 14 + "github.com/bluesky-social/indigo/xrpc" 15 + 16 + "github.com/flosch/pongo2/v6" 17 + "github.com/labstack/echo/v4" 18 + ) 19 + 20 + func (srv *Server) WebHome(c echo.Context) error { 21 + info := pongo2.Context{} 22 + return c.Render(http.StatusOK, "home.html", info) 23 + } 24 + 25 + func (srv *Server) WebQuery(c echo.Context) error { 26 + 27 + // parse the q query param, redirect based on that 28 + q := c.QueryParam("q") 29 + if q == "" { 30 + return c.Redirect(http.StatusFound, "/") 31 + } 32 + if strings.HasPrefix(q, "at://") { 33 + if strings.HasSuffix(q, "/") { 34 + q = q[0:len(q)-1] 35 + } 36 + 37 + aturi, err := syntax.ParseATURI(q) 38 + if err != nil { 39 + return err 40 + } 41 + if aturi.RecordKey() != "" { 42 + return c.Redirect(http.StatusFound, fmt.Sprintf("/at/%s/%s/%s", aturi.Authority(), aturi.Collection(), aturi.RecordKey())) 43 + } 44 + if aturi.Collection() != "" { 45 + return c.Redirect(http.StatusFound, fmt.Sprintf("/at/%s/%s", aturi.Authority(), aturi.Collection())) 46 + } 47 + return c.Redirect(http.StatusFound, fmt.Sprintf("/at/%s", aturi.Authority())) 48 + } 49 + if strings.HasPrefix(q, "did:") { 50 + return c.Redirect(http.StatusFound, fmt.Sprintf("/account/%s", q)) 51 + } 52 + _, err := syntax.ParseHandle(q) 53 + if nil == err { 54 + return c.Redirect(http.StatusFound, fmt.Sprintf("/account/%s", q)) 55 + } 56 + return echo.NewHTTPError(400, "failed to parse query") 57 + } 58 + 59 + // e.GET("/account/:atid", srv.WebAccount) 60 + func (srv *Server) WebAccount(c echo.Context) error { 61 + ctx := c.Request().Context() 62 + //req := c.Request() 63 + info := pongo2.Context{} 64 + 65 + atid, err := syntax.ParseAtIdentifier(c.Param("atid")) 66 + if err != nil { 67 + return echo.NewHTTPError(404, fmt.Sprintf("failed to parse handle or DID")) 68 + } 69 + 70 + ident, err := srv.dir.Lookup(ctx, *atid) 71 + if err != nil { 72 + // TODO: proper error page? 73 + return err 74 + } 75 + 76 + bdir := identity.BaseDirectory{} 77 + doc, err := bdir.ResolveDID(ctx, ident.DID) 78 + if nil == err { 79 + b, err := json.MarshalIndent(doc, "", " ") 80 + if err != nil { 81 + return err 82 + } 83 + info["didDocJSON"] = string(b) 84 + } 85 + info["atid"] = atid 86 + info["ident"] = ident 87 + info["uri"] = atid 88 + return c.Render(http.StatusOK, "account.html", info) 89 + } 90 + 91 + // e.GET("/at/:atid", srv.WebRepo) 92 + func (srv *Server) WebRepo(c echo.Context) error { 93 + ctx := c.Request().Context() 94 + //req := c.Request() 95 + info := pongo2.Context{} 96 + 97 + atid, err := syntax.ParseAtIdentifier(c.Param("atid")) 98 + if err != nil { 99 + return echo.NewHTTPError(400, fmt.Sprintf("failed to parse handle or DID")) 100 + } 101 + 102 + ident, err := srv.dir.Lookup(ctx, *atid) 103 + if err != nil { 104 + // TODO: proper error page? 105 + return err 106 + } 107 + info["atid"] = atid 108 + info["ident"] = ident 109 + info["uri"] = fmt.Sprintf("at://%s", atid) 110 + 111 + // create a new API client to connect to the account's PDS 112 + xrpcc := xrpc.Client{ 113 + Host: ident.PDSEndpoint(), 114 + } 115 + if xrpcc.Host == "" { 116 + return fmt.Errorf("no PDS endpoint for identity") 117 + } 118 + 119 + desc, err := comatproto.RepoDescribeRepo(ctx, &xrpcc, ident.DID.String()) 120 + if err != nil { 121 + return err 122 + } 123 + info["collections"] = desc.Collections 124 + 125 + return c.Render(http.StatusOK, "repo.html", info) 126 + } 127 + 128 + // e.GET("/at/:atid/:collection", srv.WebCollection) 129 + func (srv *Server) WebRepoCollection(c echo.Context) error { 130 + ctx := c.Request().Context() 131 + //req := c.Request() 132 + info := pongo2.Context{} 133 + 134 + atid, err := syntax.ParseAtIdentifier(c.Param("atid")) 135 + if err != nil { 136 + return echo.NewHTTPError(400, fmt.Sprintf("failed to parse handle or DID")) 137 + } 138 + 139 + collection, err := syntax.ParseNSID(c.Param("collection")) 140 + if err != nil { 141 + return echo.NewHTTPError(400, fmt.Sprintf("failed to parse collection NSID")) 142 + } 143 + 144 + ident, err := srv.dir.Lookup(ctx, *atid) 145 + if err != nil { 146 + // TODO: proper error page? 147 + return err 148 + } 149 + info["atid"] = atid 150 + info["ident"] = ident 151 + info["collection"] = collection 152 + info["uri"] = fmt.Sprintf("at://%s/%s", atid, collection) 153 + 154 + // create a new API client to connect to the account's PDS 155 + xrpcc := xrpc.Client{ 156 + Host: ident.PDSEndpoint(), 157 + } 158 + if xrpcc.Host == "" { 159 + return fmt.Errorf("no PDS endpoint for identity") 160 + } 161 + 162 + cursor := c.QueryParam("cursor") 163 + // collection string, cursor string, limit int64, repo string, reverse bool, rkeyEnd string, rkeyStart string 164 + resp, err := RepoListRecords(ctx, &xrpcc, collection.String(), cursor, 100, ident.DID.String(), false, "", "") 165 + if err != nil { 166 + return err 167 + } 168 + recordURIs := make([]syntax.ATURI, len(resp.Records)) 169 + for i, rec := range resp.Records { 170 + aturi, err := syntax.ParseATURI(rec.Uri) 171 + if err != nil { 172 + return err 173 + } 174 + recordURIs[i] = aturi 175 + } 176 + if resp.Cursor != nil && *resp.Cursor != "" { 177 + cursor = *resp.Cursor 178 + } 179 + 180 + info["records"] = resp.Records 181 + info["recordURIs"] = recordURIs 182 + info["cursor"] = cursor 183 + return c.Render(http.StatusOK, "repo_collection.html", info) 184 + } 185 + 186 + // e.GET("/at/:atid/:collection/:rkey", srv.WebRecord) 187 + func (srv *Server) WebRepoRecord(c echo.Context) error { 188 + ctx := c.Request().Context() 189 + //req := c.Request() 190 + info := pongo2.Context{} 191 + 192 + atid, err := syntax.ParseAtIdentifier(c.Param("atid")) 193 + if err != nil { 194 + return echo.NewHTTPError(400, fmt.Sprintf("failed to parse handle or DID")) 195 + } 196 + 197 + collection, err := syntax.ParseNSID(c.Param("collection")) 198 + if err != nil { 199 + return echo.NewHTTPError(400, fmt.Sprintf("failed to parse collection NSID")) 200 + } 201 + 202 + rkey, err := syntax.ParseRecordKey(c.Param("rkey")) 203 + if err != nil { 204 + return echo.NewHTTPError(400, fmt.Sprintf("failed to parse record key")) 205 + } 206 + 207 + ident, err := srv.dir.Lookup(ctx, *atid) 208 + if err != nil { 209 + // TODO: proper error page? 210 + return err 211 + } 212 + info["atid"] = atid 213 + info["ident"] = ident 214 + info["collection"] = collection 215 + info["rkey"] = rkey 216 + info["uri"] = fmt.Sprintf("at://%s/%s/%s", atid, collection, rkey) 217 + 218 + xrpcc := xrpc.Client{ 219 + Host: ident.PDSEndpoint(), 220 + } 221 + resp, err := RepoGetRecord(ctx, &xrpcc, "", collection.String(), ident.DID.String(), rkey.String()) 222 + if err != nil { 223 + return echo.NewHTTPError(400, fmt.Sprintf("failed to load record: %s", err)) 224 + } 225 + 226 + if nil == resp.Value { 227 + return fmt.Errorf("empty record in response") 228 + } 229 + 230 + record, err := data.UnmarshalJSON(*resp.Value) 231 + if err != nil { 232 + return fmt.Errorf("fetched record was invalid data: %w", err) 233 + } 234 + info["record"] = record 235 + 236 + b, err := json.MarshalIndent(record, "", " ") 237 + if err != nil { 238 + return err 239 + } 240 + info["recordJSON"] = string(b) 241 + 242 + return c.Render(http.StatusOK, "repo_record.html", info) 243 + }
+66
cmd/astrolabe/main.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + slogging "log/slog" 6 + "os" 7 + 8 + "github.com/carlmjohnson/versioninfo" 9 + "github.com/urfave/cli/v2" 10 + 11 + _ "github.com/joho/godotenv/autoload" 12 + ) 13 + 14 + var ( 15 + slog = slogging.New(slogging.NewJSONHandler(os.Stdout, nil)) 16 + version = versioninfo.Short() 17 + ) 18 + 19 + func main() { 20 + if err := run(os.Args); err != nil { 21 + slog.Error("fatal", "err", err) 22 + os.Exit(-1) 23 + } 24 + } 25 + 26 + func run(args []string) error { 27 + 28 + app := cli.App{ 29 + Name: "astrolabe", 30 + Usage: "public web interface to explore atproto network content", 31 + } 32 + 33 + app.Commands = []*cli.Command{ 34 + &cli.Command{ 35 + Name: "serve", 36 + Usage: "run the server", 37 + Action: serve, 38 + Flags: []cli.Flag{ 39 + &cli.StringFlag{ 40 + Name: "bind", 41 + Usage: "Specify the local IP/port to bind to", 42 + Required: false, 43 + Value: ":8400", 44 + EnvVars: []string{"ASTROLABE_BIND"}, 45 + }, 46 + &cli.BoolFlag{ 47 + Name: "debug", 48 + Usage: "Enable debug mode", 49 + Value: false, 50 + Required: false, 51 + EnvVars: []string{"DEBUG"}, 52 + }, 53 + }, 54 + }, 55 + &cli.Command{ 56 + Name: "version", 57 + Usage: "print version", 58 + Action: func(cctx *cli.Context) error { 59 + fmt.Println(version) 60 + return nil 61 + }, 62 + }, 63 + } 64 + 65 + return app.Run(args) 66 + }
+85
cmd/astrolabe/renderer.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "embed" 6 + "errors" 7 + "fmt" 8 + "io" 9 + "path/filepath" 10 + 11 + "github.com/flosch/pongo2/v6" 12 + "github.com/labstack/echo/v4" 13 + ) 14 + 15 + //go:embed templates/* 16 + var TemplateFS embed.FS 17 + 18 + type RendererLoader struct { 19 + prefix string 20 + fs *embed.FS 21 + } 22 + 23 + func NewRendererLoader(prefix string, fs *embed.FS) pongo2.TemplateLoader { 24 + return &RendererLoader{ 25 + prefix: prefix, 26 + fs: fs, 27 + } 28 + } 29 + func (l *RendererLoader) Abs(_, name string) string { 30 + // TODO: remove this workaround 31 + // Figure out why this method is being called 32 + // twice on template names resulting in a failure to resolve 33 + // the template name. 34 + if filepath.HasPrefix(name, l.prefix) { 35 + return name 36 + } 37 + return filepath.Join(l.prefix, name) 38 + } 39 + 40 + func (l *RendererLoader) Get(path string) (io.Reader, error) { 41 + b, err := l.fs.ReadFile(path) 42 + if err != nil { 43 + return nil, fmt.Errorf("reading template %q failed: %w", path, err) 44 + } 45 + return bytes.NewReader(b), nil 46 + } 47 + 48 + type Renderer struct { 49 + TemplateSet *pongo2.TemplateSet 50 + Debug bool 51 + } 52 + 53 + func NewRenderer(prefix string, fs *embed.FS, debug bool) *Renderer { 54 + return &Renderer{ 55 + TemplateSet: pongo2.NewSet(prefix, NewRendererLoader(prefix, fs)), 56 + Debug: debug, 57 + } 58 + } 59 + 60 + func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 61 + var ctx pongo2.Context 62 + 63 + if data != nil { 64 + var ok bool 65 + ctx, ok = data.(pongo2.Context) 66 + if !ok { 67 + return errors.New("no pongo2.Context data was passed") 68 + } 69 + } 70 + 71 + var t *pongo2.Template 72 + var err error 73 + 74 + if r.Debug { 75 + t, err = pongo2.FromFile(name) 76 + } else { 77 + t, err = r.TemplateSet.FromFile(name) 78 + } 79 + 80 + if err != nil { 81 + return err 82 + } 83 + 84 + return t.ExecuteWriter(ctx, w) 85 + }
+42
cmd/astrolabe/repogetRecord.go
··· 1 + // Copied from indigo:api/atproto/repolistRecords.go 2 + 3 + package main 4 + 5 + // schema: com.atproto.repo.getRecord 6 + 7 + import ( 8 + "context" 9 + "encoding/json" 10 + 11 + "github.com/bluesky-social/indigo/xrpc" 12 + ) 13 + 14 + // RepoGetRecord_Output is the output of a com.atproto.repo.getRecord call. 15 + type RepoGetRecord_Output struct { 16 + Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` 17 + Uri string `json:"uri" cborgen:"uri"` 18 + // NOTE: changed from lex decoder to json.RawMessage 19 + Value *json.RawMessage `json:"value" cborgen:"value"` 20 + } 21 + 22 + // RepoGetRecord calls the XRPC method "com.atproto.repo.getRecord". 23 + // 24 + // cid: The CID of the version of the record. If not specified, then return the most recent version. 25 + // collection: The NSID of the record collection. 26 + // repo: The handle or DID of the repo. 27 + // rkey: The Record Key. 28 + func RepoGetRecord(ctx context.Context, c *xrpc.Client, cid string, collection string, repo string, rkey string) (*RepoGetRecord_Output, error) { 29 + var out RepoGetRecord_Output 30 + 31 + params := map[string]interface{}{ 32 + "cid": cid, 33 + "collection": collection, 34 + "repo": repo, 35 + "rkey": rkey, 36 + } 37 + if err := c.Do(ctx, xrpc.Query, "", "com.atproto.repo.getRecord", params, nil, &out); err != nil { 38 + return nil, err 39 + } 40 + 41 + return &out, nil 42 + }
+53
cmd/astrolabe/repolistRecords.go
··· 1 + // Copied from indigo:api/atproto/repolistRecords.go 2 + 3 + package main 4 + 5 + // schema: com.atproto.repo.listRecords 6 + 7 + import ( 8 + "context" 9 + "encoding/json" 10 + 11 + "github.com/bluesky-social/indigo/xrpc" 12 + ) 13 + 14 + // RepoListRecords_Output is the output of a com.atproto.repo.listRecords call. 15 + type RepoListRecords_Output struct { 16 + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` 17 + Records []*RepoListRecords_Record `json:"records" cborgen:"records"` 18 + } 19 + 20 + // RepoListRecords_Record is a "record" in the com.atproto.repo.listRecords schema. 21 + type RepoListRecords_Record struct { 22 + Cid string `json:"cid" cborgen:"cid"` 23 + Uri string `json:"uri" cborgen:"uri"` 24 + // NOTE: changed from lex decoder to json.RawMessage 25 + Value *json.RawMessage `json:"value" cborgen:"value"` 26 + } 27 + 28 + // RepoListRecords calls the XRPC method "com.atproto.repo.listRecords". 29 + // 30 + // collection: The NSID of the record type. 31 + // limit: The number of records to return. 32 + // repo: The handle or DID of the repo. 33 + // reverse: Flag to reverse the order of the returned records. 34 + // rkeyEnd: DEPRECATED: The highest sort-ordered rkey to stop at (exclusive) 35 + // rkeyStart: DEPRECATED: The lowest sort-ordered rkey to start from (exclusive) 36 + func RepoListRecords(ctx context.Context, c *xrpc.Client, collection string, cursor string, limit int64, repo string, reverse bool, rkeyEnd string, rkeyStart string) (*RepoListRecords_Output, error) { 37 + var out RepoListRecords_Output 38 + 39 + params := map[string]interface{}{ 40 + "collection": collection, 41 + "cursor": cursor, 42 + "limit": limit, 43 + "repo": repo, 44 + "reverse": reverse, 45 + "rkeyEnd": rkeyEnd, 46 + "rkeyStart": rkeyStart, 47 + } 48 + if err := c.Do(ctx, xrpc.Query, "", "com.atproto.repo.listRecords", params, nil, &out); err != nil { 49 + return nil, err 50 + } 51 + 52 + return &out, nil 53 + }
+178
cmd/astrolabe/service.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "embed" 6 + "errors" 7 + "fmt" 8 + "io/fs" 9 + "net/http" 10 + "os" 11 + "os/signal" 12 + "syscall" 13 + "time" 14 + 15 + "github.com/bluesky-social/indigo/atproto/identity" 16 + 17 + "github.com/flosch/pongo2/v6" 18 + "github.com/labstack/echo/v4" 19 + "github.com/labstack/echo/v4/middleware" 20 + slogecho "github.com/samber/slog-echo" 21 + "github.com/urfave/cli/v2" 22 + ) 23 + 24 + //go:embed static/* 25 + var StaticFS embed.FS 26 + 27 + type Server struct { 28 + echo *echo.Echo 29 + httpd *http.Server 30 + dir identity.Directory 31 + } 32 + 33 + func serve(cctx *cli.Context) error { 34 + debug := cctx.Bool("debug") 35 + httpAddress := cctx.String("bind") 36 + 37 + e := echo.New() 38 + 39 + // httpd 40 + var ( 41 + httpTimeout = 1 * time.Minute 42 + httpMaxHeaderBytes = 1 * (1024 * 1024) 43 + ) 44 + 45 + srv := &Server{ 46 + echo: e, 47 + dir: identity.DefaultDirectory(), 48 + } 49 + srv.httpd = &http.Server{ 50 + Handler: srv, 51 + Addr: httpAddress, 52 + WriteTimeout: httpTimeout, 53 + ReadTimeout: httpTimeout, 54 + MaxHeaderBytes: httpMaxHeaderBytes, 55 + } 56 + 57 + e.HideBanner = true 58 + e.Use(slogecho.New(slog)) 59 + e.Use(middleware.Recover()) 60 + e.Use(middleware.BodyLimit("64M")) 61 + e.HTTPErrorHandler = srv.errorHandler 62 + e.Renderer = NewRenderer("templates/", &TemplateFS, debug) 63 + e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ 64 + ContentTypeNosniff: "nosniff", 65 + XFrameOptions: "SAMEORIGIN", 66 + HSTSMaxAge: 31536000, // 365 days 67 + // TODO: 68 + // ContentSecurityPolicy 69 + // XSSProtection 70 + })) 71 + 72 + // redirect trailing slash to non-trailing slash. 73 + // all of our current endpoints have no trailing slash. 74 + e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ 75 + RedirectCode: http.StatusFound, 76 + })) 77 + 78 + staticHandler := http.FileServer(func() http.FileSystem { 79 + if debug { 80 + return http.FS(os.DirFS("static")) 81 + } 82 + fsys, err := fs.Sub(StaticFS, "static") 83 + if err != nil { 84 + slog.Error("static template error", "err", err) 85 + os.Exit(-1) 86 + } 87 + return http.FS(fsys) 88 + }()) 89 + 90 + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) 91 + e.GET("/_health", srv.HandleHealthCheck) 92 + 93 + // basic static routes 94 + e.GET("/robots.txt", echo.WrapHandler(staticHandler)) 95 + e.GET("/favicon.ico", echo.WrapHandler(staticHandler)) 96 + 97 + // actual content 98 + e.GET("/", srv.WebHome) 99 + e.GET("/query", srv.WebQuery) 100 + //e.GET("/at://:rkey", srv.WebRedirect) 101 + e.GET("/account/:atid", srv.WebAccount) 102 + e.GET("/at/:atid", srv.WebRepo) 103 + e.GET("/at/:atid/:collection", srv.WebRepoCollection) 104 + e.GET("/at/:atid/:collection/:rkey", srv.WebRepoRecord) 105 + 106 + // Start the server 107 + slog.Info("starting server", "bind", httpAddress) 108 + go func() { 109 + if err := srv.httpd.ListenAndServe(); err != nil { 110 + if !errors.Is(err, http.ErrServerClosed) { 111 + slog.Error("HTTP server shutting down unexpectedly", "err", err) 112 + } 113 + } 114 + }() 115 + 116 + // Wait for a signal to exit. 117 + slog.Info("registering OS exit signal handler") 118 + quit := make(chan struct{}) 119 + exitSignals := make(chan os.Signal, 1) 120 + signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) 121 + go func() { 122 + sig := <-exitSignals 123 + slog.Info("received OS exit signal", "signal", sig) 124 + 125 + // Shut down the HTTP server 126 + if err := srv.Shutdown(); err != nil { 127 + slog.Error("HTTP server shutdown error", "err", err) 128 + } 129 + 130 + // Trigger the return that causes an exit. 131 + close(quit) 132 + }() 133 + <-quit 134 + slog.Info("graceful shutdown complete") 135 + return nil 136 + } 137 + 138 + type GenericStatus struct { 139 + Daemon string `json:"daemon"` 140 + Status string `json:"status"` 141 + Message string `json:"msg,omitempty"` 142 + } 143 + 144 + func (srv *Server) errorHandler(err error, c echo.Context) { 145 + code := http.StatusInternalServerError 146 + var errorMessage string 147 + if he, ok := err.(*echo.HTTPError); ok { 148 + code = he.Code 149 + errorMessage = fmt.Sprintf("%s", he.Message) 150 + } 151 + if code >= 500 { 152 + slog.Warn("astrolabe-http-internal-error", "err", err) 153 + } 154 + data := pongo2.Context{ 155 + "statusCode": code, 156 + "errorMessage": errorMessage, 157 + } 158 + if !c.Response().Committed { 159 + c.Render(code, "error.html", data) 160 + } 161 + } 162 + 163 + func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 164 + srv.echo.ServeHTTP(rw, req) 165 + } 166 + 167 + func (srv *Server) Shutdown() error { 168 + slog.Info("shutting down") 169 + 170 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 171 + defer cancel() 172 + 173 + return srv.httpd.Shutdown(ctx) 174 + } 175 + 176 + func (s *Server) HandleHealthCheck(c echo.Context) error { 177 + return c.JSON(200, GenericStatus{Status: "ok", Daemon: "astrolabe"}) 178 + }
cmd/astrolabe/static/apple-touch-icon.png

This is a binary file and will not be displayed.

cmd/astrolabe/static/default-avatar.png

This is a binary file and will not be displayed.

cmd/astrolabe/static/favicon-16x16.png

This is a binary file and will not be displayed.

cmd/astrolabe/static/favicon-32x32.png

This is a binary file and will not be displayed.

cmd/astrolabe/static/favicon.ico

This is a binary file and will not be displayed.

cmd/astrolabe/static/favicon.png

This is a binary file and will not be displayed.

+9
cmd/astrolabe/static/robots.txt
··· 1 + # Hello Friends! 2 + # If you are considering bulk or automated crawling, you may want to look in 3 + # to our protocol (API), including a firehose of updates. See: https://atproto.com/ 4 + 5 + # By default, may crawl anything on this domain. HTTP 429 ("backoff") status 6 + # codes are used for rate-limiting. Up to a handful concurrent requests should 7 + # be ok. 8 + User-Agent: * 9 + Allow: /
+24
cmd/astrolabe/templates/account.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block main_content %} 4 + <h2 style="font-family: monospace;">{{ atid }}</h2> 5 + 6 + <table> 7 + <tbody> 8 + <tr><td>DID</td> 9 + <td><code>{{ ident.DID }}</code></td> 10 + <tr><td>Handle</td> 11 + <td><code>{{ ident.Handle }}</code></td> 12 + <tr><td>PDS</td> 13 + <td><code>{{ ident.PDSEndpoint() }}</code></td> 14 + </tbody> 15 + </table> 16 + 17 + <p><a href="/at/{{ atid }}">Repo Index</a> 18 + <p><a href="{{ ident.PDSEndpoint() }}/xrpc/com.atproto.sync.getRepo?did={{ ident.DID }}">Repo CAR Export</a> 19 + 20 + {% if didDocJSON %} 21 + <h4>DID Document</h4> 22 + <pre style="padding: 1em;">{{ didDocJSON }}</pre> 23 + {% endif %} 24 + {% endblock %}
+40
cmd/astrolabe/templates/base.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="referrer" content="origin-when-cross-origin"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1"> 7 + <meta name="color-scheme" content="light dark" /> 8 + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.blue.min.css" /> 9 + <style> 10 + html { position: relative; min-height: 100%; height: auto; } 11 + body { margin-bottom: 3em; } 12 + body > nav { background-color: var(--pico-muted-border-color); } 13 + body > footer { position: absolute; bottom: 0px; padding: 2em; background-color: var(--pico-muted-border-color); } 14 + thead th { font-weight: bold; } 15 + main article { margin: 2.5rem 0; padding: 2rem; } 16 + code { background: none; } 17 + td { padding: 0; } 18 + </style> 19 + <meta name="generator" name="astrolabe"> 20 + <title>{% block head_title %}astrolabe{% endblock %}</title> 21 + </head> 22 + <body> 23 + <nav class="container-fluid"> 24 + <ul> 25 + <li><a href="/"><strong>astrolabe</strong></a></li> 26 + </ul> 27 + <form action="/query" method="get" style="width: 80%;"> 28 + <input type="text" name="q" placeholder="at://..." {% if uri %}value="{{ uri }}"{% endif %} style="margin: 0.5em;"> 29 + </form> 30 + <ul> 31 + <li><a href="https://github.com/bluesky-social/indigo">Code</a></li> 32 + </ul> 33 + </nav> 34 + 35 + <main class="container"> 36 + {% block main_content %}Base Template{% endblock %} 37 + </main> 38 + 39 + </body> 40 + </html>
+14
cmd/astrolabe/templates/error.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block head_title %}Error {{ statusCode }} - astrolabe{% endblock %} 4 + 5 + {% block main_content %} 6 + <br> 7 + <center> 8 + <h1 style="font-size: 8em;">{{ statusCode }}</h1> 9 + <h2 style="font-size: 3em;">Error!</h2> 10 + {% if errorMessage %} 11 + <p><code>{{ errorMessage }}</code></p> 12 + {% endif %} 13 + </center> 14 + {% endblock %}
+5
cmd/astrolabe/templates/home.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block main_content %} 4 + This is the homepage 5 + {% endblock %}
+16
cmd/astrolabe/templates/repo.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block main_content %} 4 + <h2 style="font-family: monospace;">at://{{ atid }}</h2> 5 + 6 + <h4>Index</h4> 7 + <table> 8 + <tbody> 9 + <tr><td><code><a href="/account/{{ atid }}">..</a></code></td> 10 + {% for collection in collections %} 11 + <tr><td><code><a href="/at/{{ atid }}/{{ collection }}">{{ collection }}/</a></code></td> 12 + {% endfor %} 13 + </tbody> 14 + </table> 15 + 16 + {% endblock %}
+19
cmd/astrolabe/templates/repo_collection.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block main_content %} 4 + <h2 style="font-family: monospace;">at://{{ atid }}/{{ collection }}</h2> 5 + 6 + <h4>Index</h4> 7 + <table> 8 + <tbody> 9 + <tr><td><code><a href="/at/{{ atid }}">..</a></code></td> 10 + {% for uri in recordURIs %} 11 + <tr><td><code><a href="/at/{{ atid }}/{{ collection }}/{{ uri.RecordKey() }}">{{ collection }}/{{ uri.RecordKey() }}</a></code></td> 12 + {% endfor %} 13 + {% if cursor != "" %} 14 + <tr><td><code><a href="/at/{{ atid }}/{{ collection }}?cursor={{ cursor }}">[more]</a></code></td> 15 + {% endif %} 16 + </tbody> 17 + </table> 18 + 19 + {% endblock %}
+12
cmd/astrolabe/templates/repo_record.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block main_content %} 4 + <h2 style="font-family: monospace;">at://{{ atid }}/{{ collection }}/{{ rkey }}</h2> 5 + <p><a href="/at/{{ atid }}/{{ collection }}">Back to Collection</a> 6 + 7 + {% if recordJSON %} 8 + <h4>Record JSON</h4> 9 + <pre style="padding: 1em;">{{ recordJSON }}</pre> 10 + {% endif %} 11 + 12 + {% endblock %}