this repo has no description
0
fork

Configure Feed

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

remove astrolabe (moved to cobalt)

+1 -808
+1 -35
cmd/astrolabe/README.md
··· 2 2 astrolabe: basic atproto network data explorer 3 3 ============================================== 4 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 5 + **NOTE: this proof-of-concept has moved to [cobalt](https://tangled.org/@bnewbold.net/cobalt/tree/main/cmd/astrolabe)**
-247
cmd/astrolabe/handlers.go
··· 1 - package main 2 - 3 - import ( 4 - "encoding/json" 5 - "fmt" 6 - "net/http" 7 - "strings" 8 - 9 - "github.com/bluesky-social/indigo/api/agnostic" 10 - comatproto "github.com/bluesky-social/indigo/api/atproto" 11 - _ "github.com/bluesky-social/indigo/api/bsky" 12 - "github.com/bluesky-social/indigo/atproto/atdata" 13 - "github.com/bluesky-social/indigo/atproto/identity" 14 - "github.com/bluesky-social/indigo/atproto/syntax" 15 - "github.com/bluesky-social/indigo/xrpc" 16 - 17 - "github.com/flosch/pongo2/v6" 18 - "github.com/labstack/echo/v4" 19 - ) 20 - 21 - func (srv *Server) WebHome(c echo.Context) error { 22 - info := pongo2.Context{} 23 - return c.Render(http.StatusOK, "home.html", info) 24 - } 25 - 26 - func (srv *Server) WebQuery(c echo.Context) error { 27 - 28 - // parse the q query param, redirect based on that 29 - q := c.QueryParam("q") 30 - if q == "" { 31 - return c.Redirect(http.StatusFound, "/") 32 - } 33 - if strings.HasPrefix(q, "https://") { 34 - q = ParseServiceURL(q) 35 - } 36 - if strings.HasPrefix(q, "at://") { 37 - if strings.HasSuffix(q, "/") { 38 - q = q[0 : len(q)-1] 39 - } 40 - 41 - aturi, err := syntax.ParseATURI(q) 42 - if err != nil { 43 - return err 44 - } 45 - if aturi.RecordKey() != "" { 46 - return c.Redirect(http.StatusFound, fmt.Sprintf("/at/%s/%s/%s", aturi.Authority(), aturi.Collection(), aturi.RecordKey())) 47 - } 48 - if aturi.Collection() != "" { 49 - return c.Redirect(http.StatusFound, fmt.Sprintf("/at/%s/%s", aturi.Authority(), aturi.Collection())) 50 - } 51 - return c.Redirect(http.StatusFound, fmt.Sprintf("/at/%s", aturi.Authority())) 52 - } 53 - if strings.HasPrefix(q, "did:") { 54 - return c.Redirect(http.StatusFound, fmt.Sprintf("/account/%s", q)) 55 - } 56 - _, err := syntax.ParseHandle(q) 57 - if nil == err { 58 - return c.Redirect(http.StatusFound, fmt.Sprintf("/account/%s", q)) 59 - } 60 - return echo.NewHTTPError(400, "failed to parse query") 61 - } 62 - 63 - // e.GET("/account/:atid", srv.WebAccount) 64 - func (srv *Server) WebAccount(c echo.Context) error { 65 - ctx := c.Request().Context() 66 - //req := c.Request() 67 - info := pongo2.Context{} 68 - 69 - atid, err := syntax.ParseAtIdentifier(c.Param("atid")) 70 - if err != nil { 71 - return echo.NewHTTPError(404, "failed to parse handle or DID") 72 - } 73 - 74 - ident, err := srv.dir.Lookup(ctx, *atid) 75 - if err != nil { 76 - // TODO: proper error page? 77 - return err 78 - } 79 - 80 - bdir := identity.BaseDirectory{} 81 - doc, err := bdir.ResolveDID(ctx, ident.DID) 82 - if nil == err { 83 - b, err := json.MarshalIndent(doc, "", " ") 84 - if err != nil { 85 - return err 86 - } 87 - info["didDocJSON"] = string(b) 88 - } 89 - info["atid"] = atid 90 - info["ident"] = ident 91 - info["uri"] = atid 92 - return c.Render(http.StatusOK, "account.html", info) 93 - } 94 - 95 - // e.GET("/at/:atid", srv.WebRepo) 96 - func (srv *Server) WebRepo(c echo.Context) error { 97 - ctx := c.Request().Context() 98 - //req := c.Request() 99 - info := pongo2.Context{} 100 - 101 - atid, err := syntax.ParseAtIdentifier(c.Param("atid")) 102 - if err != nil { 103 - return echo.NewHTTPError(400, "failed to parse handle or DID") 104 - } 105 - 106 - ident, err := srv.dir.Lookup(ctx, *atid) 107 - if err != nil { 108 - // TODO: proper error page? 109 - return err 110 - } 111 - info["atid"] = atid 112 - info["ident"] = ident 113 - info["uri"] = fmt.Sprintf("at://%s", atid) 114 - 115 - // create a new API client to connect to the account's PDS 116 - xrpcc := xrpc.Client{ 117 - Host: ident.PDSEndpoint(), 118 - } 119 - if xrpcc.Host == "" { 120 - return fmt.Errorf("no PDS endpoint for identity") 121 - } 122 - 123 - desc, err := comatproto.RepoDescribeRepo(ctx, &xrpcc, ident.DID.String()) 124 - if err != nil { 125 - return err 126 - } 127 - info["collections"] = desc.Collections 128 - 129 - return c.Render(http.StatusOK, "repo.html", info) 130 - } 131 - 132 - // e.GET("/at/:atid/:collection", srv.WebCollection) 133 - func (srv *Server) WebRepoCollection(c echo.Context) error { 134 - ctx := c.Request().Context() 135 - //req := c.Request() 136 - info := pongo2.Context{} 137 - 138 - atid, err := syntax.ParseAtIdentifier(c.Param("atid")) 139 - if err != nil { 140 - return echo.NewHTTPError(400, "failed to parse handle or DID") 141 - } 142 - 143 - collection, err := syntax.ParseNSID(c.Param("collection")) 144 - if err != nil { 145 - return echo.NewHTTPError(400, "failed to parse collection NSID") 146 - } 147 - 148 - ident, err := srv.dir.Lookup(ctx, *atid) 149 - if err != nil { 150 - // TODO: proper error page? 151 - return err 152 - } 153 - info["atid"] = atid 154 - info["ident"] = ident 155 - info["collection"] = collection 156 - info["uri"] = fmt.Sprintf("at://%s/%s", atid, collection) 157 - 158 - // create a new API client to connect to the account's PDS 159 - xrpcc := xrpc.Client{ 160 - Host: ident.PDSEndpoint(), 161 - } 162 - if xrpcc.Host == "" { 163 - return fmt.Errorf("no PDS endpoint for identity") 164 - } 165 - 166 - cursor := c.QueryParam("cursor") 167 - // collection string, cursor string, limit int64, repo string, reverse bool 168 - resp, err := agnostic.RepoListRecords(ctx, &xrpcc, collection.String(), cursor, 100, ident.DID.String(), false) 169 - if err != nil { 170 - return err 171 - } 172 - recordURIs := make([]syntax.ATURI, len(resp.Records)) 173 - for i, rec := range resp.Records { 174 - aturi, err := syntax.ParseATURI(rec.Uri) 175 - if err != nil { 176 - return err 177 - } 178 - recordURIs[i] = aturi 179 - } 180 - if resp.Cursor != nil && *resp.Cursor != "" { 181 - cursor = *resp.Cursor 182 - } 183 - 184 - info["records"] = resp.Records 185 - info["recordURIs"] = recordURIs 186 - info["cursor"] = cursor 187 - return c.Render(http.StatusOK, "repo_collection.html", info) 188 - } 189 - 190 - // e.GET("/at/:atid/:collection/:rkey", srv.WebRecord) 191 - func (srv *Server) WebRepoRecord(c echo.Context) error { 192 - ctx := c.Request().Context() 193 - //req := c.Request() 194 - info := pongo2.Context{} 195 - 196 - atid, err := syntax.ParseAtIdentifier(c.Param("atid")) 197 - if err != nil { 198 - return echo.NewHTTPError(400, "failed to parse handle or DID") 199 - } 200 - 201 - collection, err := syntax.ParseNSID(c.Param("collection")) 202 - if err != nil { 203 - return echo.NewHTTPError(400, "failed to parse collection NSID") 204 - } 205 - 206 - rkey, err := syntax.ParseRecordKey(c.Param("rkey")) 207 - if err != nil { 208 - return echo.NewHTTPError(400, "failed to parse record key") 209 - } 210 - 211 - ident, err := srv.dir.Lookup(ctx, *atid) 212 - if err != nil { 213 - // TODO: proper error page? 214 - return err 215 - } 216 - info["atid"] = atid 217 - info["ident"] = ident 218 - info["collection"] = collection 219 - info["rkey"] = rkey 220 - info["uri"] = fmt.Sprintf("at://%s/%s/%s", atid, collection, rkey) 221 - 222 - xrpcc := xrpc.Client{ 223 - Host: ident.PDSEndpoint(), 224 - } 225 - resp, err := agnostic.RepoGetRecord(ctx, &xrpcc, "", collection.String(), ident.DID.String(), rkey.String()) 226 - if err != nil { 227 - return echo.NewHTTPError(400, fmt.Sprintf("failed to load record: %s", err)) 228 - } 229 - 230 - if nil == resp.Value { 231 - return fmt.Errorf("empty record in response") 232 - } 233 - 234 - record, err := atdata.UnmarshalJSON(*resp.Value) 235 - if err != nil { 236 - return fmt.Errorf("fetched record was invalid data: %w", err) 237 - } 238 - info["record"] = record 239 - 240 - b, err := json.MarshalIndent(record, "", " ") 241 - if err != nil { 242 - return err 243 - } 244 - info["recordJSON"] = string(b) 245 - 246 - return c.Render(http.StatusOK, "repo_record.html", info) 247 - }
-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 - }
-23
cmd/astrolabe/parse.go
··· 1 - package main 2 - 3 - import ( 4 - "fmt" 5 - "strings" 6 - ) 7 - 8 - // attempts to parse a service URL to an AT-URI, handle, or DID. if it can't, passes string through as-is 9 - func ParseServiceURL(raw string) string { 10 - parts := strings.Split(raw, "/") 11 - if len(parts) < 3 || parts[0] != "https:" { 12 - return raw 13 - } 14 - if parts[2] == "bsky.app" && len(parts) >= 5 && parts[3] == "profile" { 15 - if len(parts) == 5 { 16 - return parts[4] 17 - } 18 - if len(parts) == 7 && parts[5] == "post" { 19 - return fmt.Sprintf("at://%s/app.bsky.feed.post/%s", parts[4], parts[6]) 20 - } 21 - } 22 - return raw 23 - }
-23
cmd/astrolabe/parse_test.go
··· 1 - package main 2 - 3 - import ( 4 - "testing" 5 - 6 - "github.com/stretchr/testify/assert" 7 - ) 8 - 9 - func TestParseServiceURL(t *testing.T) { 10 - assert := assert.New(t) 11 - 12 - testVec := [][]string{ 13 - {"", ""}, 14 - {"atproto.com", "atproto.com"}, 15 - {"https://bsky.app/profile/atproto.com", "atproto.com"}, 16 - {"https://bsky.app/profile/did:plc:ewvi7nxzyoun6zhxrhs64oiz", "did:plc:ewvi7nxzyoun6zhxrhs64oiz"}, 17 - {"https://bsky.app/profile/atproto.com/post/3lffzv6f4o22r", "at://atproto.com/app.bsky.feed.post/3lffzv6f4o22r"}, 18 - } 19 - 20 - for _, pair := range testVec { 21 - assert.Equal(pair[1], ParseServiceURL(pair[0])) 22 - } 23 - }
-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 - }
-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.zinc.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/tree/main/cmd/astrolabe">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 %}
-17
cmd/astrolabe/templates/home.html
··· 1 - {% extends "base.html" %} 2 - 3 - {% block main_content %} 4 - <h2>astrolabe: AT Protocol Repository Browser</h2> 5 - 6 - <p>This is a tool for browsing <a href="https://atproto.com">AT Protocol</a> ("atproto") repositories and records. You can enter an account identifier or full URI to view content as JSON. It works by fetching data directly from account PDS instances: the data itself is not hosted by this service. 7 - 8 - <p>Examples: 9 - <ul> 10 - <li>Account Handle: <code><a href="/account/bnewbold.net">bnewbold.net</a></code></li> 11 - <li>Account DID: <code><a href="/account/did:plc:44ybard66vv44zksje25o7dz">did:plc:44ybard66vv44zksje25o7dz</a></code></li> 12 - <li>Collection: <code><a href="/at/bnewbold.net/app.bsky.feed.post">at://bnewbold.net/app.bsky.feed.post</a></code></li> 13 - <li>Record: <code><a href="/at/did:plc:44ybard66vv44zksje25o7dz/app.bsky.actor.profile/self">at://bnewbold.net/app.bsky.actor.profile/self</a></code></li> 14 - </ul> 15 - 16 - <p>Other similar services include <a href="https://atproto-browser.vercel.app/">atproto-browser.vercel.app</a> and <a href="https://pdsls.dev/">pdsls.dev</a>. 17 - {% 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 %}