diff --git a/cmd/astrolabe/README.md b/cmd/astrolabe/README.md new file mode 100644 index 0000000..9d04a03 --- /dev/null +++ b/cmd/astrolabe/README.md @@ -0,0 +1,39 @@ + +astrolabe: basic atproto network data explorer +============================================== + +⚠️ This is a fun little proof-of-concept ⚠️ + + +## Run It + +The recommended way to run `astrolabe` is behind a `caddy` HTTPS server which does automatic on-demand SSL certificate registration (using Let's Encrypt). + +Build and run `astrolabe`: + + go build ./cmd/astrolabe + + # will listen on :8400 by default + ./astrolabe serve + +Create a `Caddyfile`: + +``` +{ + on_demand_tls { + interval 1h + burst 8 + } +} + +:443 { + reverse_proxy localhost:8400 + tls YOUREMAIL@example.com { + on_demand + } +} +``` + +Run `caddy`: + + caddy run diff --git a/cmd/astrolabe/handlers.go b/cmd/astrolabe/handlers.go new file mode 100644 index 0000000..fdfca1a --- /dev/null +++ b/cmd/astrolabe/handlers.go @@ -0,0 +1,247 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/bluesky-social/indigo/api/agnostic" + comatproto "github.com/bluesky-social/indigo/api/atproto" + _ "github.com/bluesky-social/indigo/api/bsky" + "github.com/bluesky-social/indigo/atproto/atdata" + "github.com/bluesky-social/indigo/atproto/identity" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/xrpc" + + "github.com/flosch/pongo2/v6" + "github.com/labstack/echo/v4" +) + +func (srv *Server) WebHome(c echo.Context) error { + info := pongo2.Context{} + return c.Render(http.StatusOK, "home.html", info) +} + +func (srv *Server) WebQuery(c echo.Context) error { + + // parse the q query param, redirect based on that + q := c.QueryParam("q") + if q == "" { + return c.Redirect(http.StatusFound, "/") + } + if strings.HasPrefix(q, "https://") { + q = ParseServiceURL(q) + } + if strings.HasPrefix(q, "at://") { + if strings.HasSuffix(q, "/") { + q = q[0 : len(q)-1] + } + + aturi, err := syntax.ParseATURI(q) + if err != nil { + return err + } + if aturi.RecordKey() != "" { + return c.Redirect(http.StatusFound, fmt.Sprintf("/at/%s/%s/%s", aturi.Authority(), aturi.Collection(), aturi.RecordKey())) + } + if aturi.Collection() != "" { + return c.Redirect(http.StatusFound, fmt.Sprintf("/at/%s/%s", aturi.Authority(), aturi.Collection())) + } + return c.Redirect(http.StatusFound, fmt.Sprintf("/at/%s", aturi.Authority())) + } + if strings.HasPrefix(q, "did:") { + return c.Redirect(http.StatusFound, fmt.Sprintf("/account/%s", q)) + } + _, err := syntax.ParseHandle(q) + if nil == err { + return c.Redirect(http.StatusFound, fmt.Sprintf("/account/%s", q)) + } + return echo.NewHTTPError(400, "failed to parse query") +} + +// e.GET("/account/:atid", srv.WebAccount) +func (srv *Server) WebAccount(c echo.Context) error { + ctx := c.Request().Context() + //req := c.Request() + info := pongo2.Context{} + + atid, err := syntax.ParseAtIdentifier(c.Param("atid")) + if err != nil { + return echo.NewHTTPError(404, "failed to parse handle or DID") + } + + ident, err := srv.dir.Lookup(ctx, *atid) + if err != nil { + // TODO: proper error page? + return err + } + + bdir := identity.BaseDirectory{} + doc, err := bdir.ResolveDID(ctx, ident.DID) + if nil == err { + b, err := json.MarshalIndent(doc, "", " ") + if err != nil { + return err + } + info["didDocJSON"] = string(b) + } + info["atid"] = atid + info["ident"] = ident + info["uri"] = atid + return c.Render(http.StatusOK, "account.html", info) +} + +// e.GET("/at/:atid", srv.WebRepo) +func (srv *Server) WebRepo(c echo.Context) error { + ctx := c.Request().Context() + //req := c.Request() + info := pongo2.Context{} + + atid, err := syntax.ParseAtIdentifier(c.Param("atid")) + if err != nil { + return echo.NewHTTPError(400, "failed to parse handle or DID") + } + + ident, err := srv.dir.Lookup(ctx, *atid) + if err != nil { + // TODO: proper error page? + return err + } + info["atid"] = atid + info["ident"] = ident + info["uri"] = fmt.Sprintf("at://%s", atid) + + // create a new API client to connect to the account's PDS + xrpcc := xrpc.Client{ + Host: ident.PDSEndpoint(), + } + if xrpcc.Host == "" { + return fmt.Errorf("no PDS endpoint for identity") + } + + desc, err := comatproto.RepoDescribeRepo(ctx, &xrpcc, ident.DID.String()) + if err != nil { + return err + } + info["collections"] = desc.Collections + + return c.Render(http.StatusOK, "repo.html", info) +} + +// e.GET("/at/:atid/:collection", srv.WebCollection) +func (srv *Server) WebRepoCollection(c echo.Context) error { + ctx := c.Request().Context() + //req := c.Request() + info := pongo2.Context{} + + atid, err := syntax.ParseAtIdentifier(c.Param("atid")) + if err != nil { + return echo.NewHTTPError(400, "failed to parse handle or DID") + } + + collection, err := syntax.ParseNSID(c.Param("collection")) + if err != nil { + return echo.NewHTTPError(400, "failed to parse collection NSID") + } + + ident, err := srv.dir.Lookup(ctx, *atid) + if err != nil { + // TODO: proper error page? + return err + } + info["atid"] = atid + info["ident"] = ident + info["collection"] = collection + info["uri"] = fmt.Sprintf("at://%s/%s", atid, collection) + + // create a new API client to connect to the account's PDS + xrpcc := xrpc.Client{ + Host: ident.PDSEndpoint(), + } + if xrpcc.Host == "" { + return fmt.Errorf("no PDS endpoint for identity") + } + + cursor := c.QueryParam("cursor") + // collection string, cursor string, limit int64, repo string, reverse bool + resp, err := agnostic.RepoListRecords(ctx, &xrpcc, collection.String(), cursor, 100, ident.DID.String(), false) + if err != nil { + return err + } + recordURIs := make([]syntax.ATURI, len(resp.Records)) + for i, rec := range resp.Records { + aturi, err := syntax.ParseATURI(rec.Uri) + if err != nil { + return err + } + recordURIs[i] = aturi + } + if resp.Cursor != nil && *resp.Cursor != "" { + cursor = *resp.Cursor + } + + info["records"] = resp.Records + info["recordURIs"] = recordURIs + info["cursor"] = cursor + return c.Render(http.StatusOK, "repo_collection.html", info) +} + +// e.GET("/at/:atid/:collection/:rkey", srv.WebRecord) +func (srv *Server) WebRepoRecord(c echo.Context) error { + ctx := c.Request().Context() + //req := c.Request() + info := pongo2.Context{} + + atid, err := syntax.ParseAtIdentifier(c.Param("atid")) + if err != nil { + return echo.NewHTTPError(400, "failed to parse handle or DID") + } + + collection, err := syntax.ParseNSID(c.Param("collection")) + if err != nil { + return echo.NewHTTPError(400, "failed to parse collection NSID") + } + + rkey, err := syntax.ParseRecordKey(c.Param("rkey")) + if err != nil { + return echo.NewHTTPError(400, "failed to parse record key") + } + + ident, err := srv.dir.Lookup(ctx, *atid) + if err != nil { + // TODO: proper error page? + return err + } + info["atid"] = atid + info["ident"] = ident + info["collection"] = collection + info["rkey"] = rkey + info["uri"] = fmt.Sprintf("at://%s/%s/%s", atid, collection, rkey) + + xrpcc := xrpc.Client{ + Host: ident.PDSEndpoint(), + } + resp, err := agnostic.RepoGetRecord(ctx, &xrpcc, "", collection.String(), ident.DID.String(), rkey.String()) + if err != nil { + return echo.NewHTTPError(400, fmt.Sprintf("failed to load record: %s", err)) + } + + if nil == resp.Value { + return fmt.Errorf("empty record in response") + } + + record, err := atdata.UnmarshalJSON(*resp.Value) + if err != nil { + return fmt.Errorf("fetched record was invalid data: %w", err) + } + info["record"] = record + + b, err := json.MarshalIndent(record, "", " ") + if err != nil { + return err + } + info["recordJSON"] = string(b) + + return c.Render(http.StatusOK, "repo_record.html", info) +} diff --git a/cmd/astrolabe/main.go b/cmd/astrolabe/main.go new file mode 100644 index 0000000..b9075b1 --- /dev/null +++ b/cmd/astrolabe/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + slogging "log/slog" + "os" + + "github.com/carlmjohnson/versioninfo" + "github.com/urfave/cli/v2" + + _ "github.com/joho/godotenv/autoload" +) + +var ( + slog = slogging.New(slogging.NewJSONHandler(os.Stdout, nil)) + version = versioninfo.Short() +) + +func main() { + if err := run(os.Args); err != nil { + slog.Error("fatal", "err", err) + os.Exit(-1) + } +} + +func run(args []string) error { + + app := cli.App{ + Name: "astrolabe", + Usage: "public web interface to explore atproto network content", + } + + app.Commands = []*cli.Command{ + &cli.Command{ + Name: "serve", + Usage: "run the server", + Action: serve, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "bind", + Usage: "Specify the local IP/port to bind to", + Required: false, + Value: ":8400", + EnvVars: []string{"ASTROLABE_BIND"}, + }, + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug mode", + Value: false, + Required: false, + EnvVars: []string{"DEBUG"}, + }, + }, + }, + &cli.Command{ + Name: "version", + Usage: "print version", + Action: func(cctx *cli.Context) error { + fmt.Println(version) + return nil + }, + }, + } + + return app.Run(args) +} diff --git a/cmd/astrolabe/parse.go b/cmd/astrolabe/parse.go new file mode 100644 index 0000000..89209f3 --- /dev/null +++ b/cmd/astrolabe/parse.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "strings" +) + +// attempts to parse a service URL to an AT-URI, handle, or DID. if it can't, passes string through as-is +func ParseServiceURL(raw string) string { + parts := strings.Split(raw, "/") + if len(parts) < 3 || parts[0] != "https:" { + return raw + } + if parts[2] == "bsky.app" && len(parts) >= 5 && parts[3] == "profile" { + if len(parts) == 5 { + return parts[4] + } + if len(parts) == 7 && parts[5] == "post" { + return fmt.Sprintf("at://%s/app.bsky.feed.post/%s", parts[4], parts[6]) + } + } + return raw +} diff --git a/cmd/astrolabe/parse_test.go b/cmd/astrolabe/parse_test.go new file mode 100644 index 0000000..c3c65cc --- /dev/null +++ b/cmd/astrolabe/parse_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseServiceURL(t *testing.T) { + assert := assert.New(t) + + testVec := [][]string{ + {"", ""}, + {"atproto.com", "atproto.com"}, + {"https://bsky.app/profile/atproto.com", "atproto.com"}, + {"https://bsky.app/profile/did:plc:ewvi7nxzyoun6zhxrhs64oiz", "did:plc:ewvi7nxzyoun6zhxrhs64oiz"}, + {"https://bsky.app/profile/atproto.com/post/3lffzv6f4o22r", "at://atproto.com/app.bsky.feed.post/3lffzv6f4o22r"}, + } + + for _, pair := range testVec { + assert.Equal(pair[1], ParseServiceURL(pair[0])) + } +} diff --git a/cmd/astrolabe/renderer.go b/cmd/astrolabe/renderer.go new file mode 100644 index 0000000..0fa3cb1 --- /dev/null +++ b/cmd/astrolabe/renderer.go @@ -0,0 +1,85 @@ +package main + +import ( + "bytes" + "embed" + "errors" + "fmt" + "io" + "path/filepath" + + "github.com/flosch/pongo2/v6" + "github.com/labstack/echo/v4" +) + +//go:embed templates/* +var TemplateFS embed.FS + +type RendererLoader struct { + prefix string + fs *embed.FS +} + +func NewRendererLoader(prefix string, fs *embed.FS) pongo2.TemplateLoader { + return &RendererLoader{ + prefix: prefix, + fs: fs, + } +} +func (l *RendererLoader) Abs(_, name string) string { + // TODO: remove this workaround + // Figure out why this method is being called + // twice on template names resulting in a failure to resolve + // the template name. + if filepath.HasPrefix(name, l.prefix) { + return name + } + return filepath.Join(l.prefix, name) +} + +func (l *RendererLoader) Get(path string) (io.Reader, error) { + b, err := l.fs.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading template %q failed: %w", path, err) + } + return bytes.NewReader(b), nil +} + +type Renderer struct { + TemplateSet *pongo2.TemplateSet + Debug bool +} + +func NewRenderer(prefix string, fs *embed.FS, debug bool) *Renderer { + return &Renderer{ + TemplateSet: pongo2.NewSet(prefix, NewRendererLoader(prefix, fs)), + Debug: debug, + } +} + +func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + var ctx pongo2.Context + + if data != nil { + var ok bool + ctx, ok = data.(pongo2.Context) + if !ok { + return errors.New("no pongo2.Context data was passed") + } + } + + var t *pongo2.Template + var err error + + if r.Debug { + t, err = pongo2.FromFile(name) + } else { + t, err = r.TemplateSet.FromFile(name) + } + + if err != nil { + return err + } + + return t.ExecuteWriter(ctx, w) +} diff --git a/cmd/astrolabe/service.go b/cmd/astrolabe/service.go new file mode 100644 index 0000000..f7300c4 --- /dev/null +++ b/cmd/astrolabe/service.go @@ -0,0 +1,178 @@ +package main + +import ( + "context" + "embed" + "errors" + "fmt" + "io/fs" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/bluesky-social/indigo/atproto/identity" + + "github.com/flosch/pongo2/v6" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + slogecho "github.com/samber/slog-echo" + "github.com/urfave/cli/v2" +) + +//go:embed static/* +var StaticFS embed.FS + +type Server struct { + echo *echo.Echo + httpd *http.Server + dir identity.Directory +} + +func serve(cctx *cli.Context) error { + debug := cctx.Bool("debug") + httpAddress := cctx.String("bind") + + e := echo.New() + + // httpd + var ( + httpTimeout = 1 * time.Minute + httpMaxHeaderBytes = 1 * (1024 * 1024) + ) + + srv := &Server{ + echo: e, + dir: identity.DefaultDirectory(), + } + srv.httpd = &http.Server{ + Handler: srv, + Addr: httpAddress, + WriteTimeout: httpTimeout, + ReadTimeout: httpTimeout, + MaxHeaderBytes: httpMaxHeaderBytes, + } + + e.HideBanner = true + e.Use(slogecho.New(slog)) + e.Use(middleware.Recover()) + e.Use(middleware.BodyLimit("64M")) + e.HTTPErrorHandler = srv.errorHandler + e.Renderer = NewRenderer("templates/", &TemplateFS, debug) + e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + HSTSMaxAge: 31536000, // 365 days + // TODO: + // ContentSecurityPolicy + // XSSProtection + })) + + // redirect trailing slash to non-trailing slash. + // all of our current endpoints have no trailing slash. + e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ + RedirectCode: http.StatusFound, + })) + + staticHandler := http.FileServer(func() http.FileSystem { + if debug { + return http.FS(os.DirFS("static")) + } + fsys, err := fs.Sub(StaticFS, "static") + if err != nil { + slog.Error("static template error", "err", err) + os.Exit(-1) + } + return http.FS(fsys) + }()) + + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) + e.GET("/_health", srv.HandleHealthCheck) + + // basic static routes + e.GET("/robots.txt", echo.WrapHandler(staticHandler)) + e.GET("/favicon.ico", echo.WrapHandler(staticHandler)) + + // actual content + e.GET("/", srv.WebHome) + e.GET("/query", srv.WebQuery) + //e.GET("/at://:rkey", srv.WebRedirect) + e.GET("/account/:atid", srv.WebAccount) + e.GET("/at/:atid", srv.WebRepo) + e.GET("/at/:atid/:collection", srv.WebRepoCollection) + e.GET("/at/:atid/:collection/:rkey", srv.WebRepoRecord) + + // Start the server + slog.Info("starting server", "bind", httpAddress) + go func() { + if err := srv.httpd.ListenAndServe(); err != nil { + if !errors.Is(err, http.ErrServerClosed) { + slog.Error("HTTP server shutting down unexpectedly", "err", err) + } + } + }() + + // Wait for a signal to exit. + slog.Info("registering OS exit signal handler") + quit := make(chan struct{}) + exitSignals := make(chan os.Signal, 1) + signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-exitSignals + slog.Info("received OS exit signal", "signal", sig) + + // Shut down the HTTP server + if err := srv.Shutdown(); err != nil { + slog.Error("HTTP server shutdown error", "err", err) + } + + // Trigger the return that causes an exit. + close(quit) + }() + <-quit + slog.Info("graceful shutdown complete") + return nil +} + +type GenericStatus struct { + Daemon string `json:"daemon"` + Status string `json:"status"` + Message string `json:"msg,omitempty"` +} + +func (srv *Server) errorHandler(err error, c echo.Context) { + code := http.StatusInternalServerError + var errorMessage string + if he, ok := err.(*echo.HTTPError); ok { + code = he.Code + errorMessage = fmt.Sprintf("%s", he.Message) + } + if code >= 500 { + slog.Warn("astrolabe-http-internal-error", "err", err) + } + data := pongo2.Context{ + "statusCode": code, + "errorMessage": errorMessage, + } + if !c.Response().Committed { + c.Render(code, "error.html", data) + } +} + +func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + srv.echo.ServeHTTP(rw, req) +} + +func (srv *Server) Shutdown() error { + slog.Info("shutting down") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + return srv.httpd.Shutdown(ctx) +} + +func (s *Server) HandleHealthCheck(c echo.Context) error { + return c.JSON(200, GenericStatus{Status: "ok", Daemon: "astrolabe"}) +} diff --git a/cmd/astrolabe/static/apple-touch-icon.png b/cmd/astrolabe/static/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5ebb6787c999108d56d1b2f2aa4a5f2275ac5e37 GIT binary patch literal 14742 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 literal 0 HcmV?d00001 diff --git a/cmd/astrolabe/static/default-avatar.png b/cmd/astrolabe/static/default-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..c732d942b33e04f1be7a9dced9211ec676de1056 GIT binary patch literal 1624 zcmeAS@N?(olHy`uVBq!ia0y~yVDMpJVDRQ(Vqjnh{qXGw0|T49r;B4q#hkaZbA6_! zN*u3WVsTDN>99>ltFwt^AD@{)p|9<$64|ik3kkxx$-WWOiVti$8`c+@JM9qr4Bi_> zXQ%7x3Wx7}Skr9Tuj+lW%)yyaxK&gCkp-94uZDvb=WFME-Xq8Hx4!z_^Ih*=|F?f` z`F!X5*Sp>=Xl!IWW<5_jm#Oi9@x;fh=Uw{5eN0NfP4kO68=@s`NKTOPgXo@*x7^oZPi zyYSA@d7oEp^-SD6Y43zNDTj2}1dQEMwzseM3#^gy-~aDqTOz00`GeC^tCmVBY+BNm zd;GhZrN>@PD~`L7h`yAl+z@}K`c1{;Bh!zRXXk(Tcz(`~MdqijO|6-zv`Nf5@L! zJ>gUFE~dz)ouL~4jGop_eyX>Z>$-|Y5XY3%4Qh_zI{x-g?c&snl)lfoE?Qc|v2=Cl zjp^&siaOSH?BnRR_PPn%B2v7U-%B?w%UU4I{xEOx(#zEWI+w1l(H2VJ z9jwQvUZBy(}Rs;+JI>Omy;!)7f;oBlq|{?hw-z8`ftfyk}*8XLoikc0-_F+I+9nY`du_h@%8s|8Uu;C~ zysbR(O>0uuihOgOMN{@v*h_BhjsDKlZPno^>`~i#eNnqH^Zu;?Kh9<_*i$#Vf`6^w!w%I*89(wpz)f)$|I31&NSyrN-6;$=bW-eHH&C(6b zvbkKM^~@0@YbA60|L4xFk*sGO?8KSznW1QPu8ko;mG_j>&;E(d|Vi*WTYGzUk?TZxb}l z?rqzgwQf=0&g}A)Sr3+5l}d~2r|66H&kj;MQU2lI>oeD;Oe)&G@wd*>O`%$fCet_% z={&K#;uIPN3&^!b!L zqo>~Gb>Ai@igC^Tm+aCSxc6j2tHU`ZzuE&LtaqDR{dTH3yV^2qmz@@!t$T3J&k3v- z|E_cl^{LM)%v2`77wE~4TU5Zb_r{(OgBkAwu9&P;nz&Va z>efzWw`ZfU9!8& z&67FGkz3|X$jydTV)y@^J0}0)ZFNy*mG%pTJ+rFf=N|0a9`W+=enCD6F6FOB~ zh2=fpge*G5vHimzm4^2I7j*~TR-2SQ$a~yByQ$=xgw-FTmcqK8liZJgI?NJz{4?WW o#NpsVp>H!!d~90o^kjd*Yh$nGg-&}J7#J8lUHx3vIVCg!0O3p$mjD0& literal 0 HcmV?d00001 diff --git a/cmd/astrolabe/static/favicon-16x16.png b/cmd/astrolabe/static/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..4267562b808197d5b00b58f3ffdedde4b689b3c2 GIT binary patch literal 316 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7nLS+`LoEE4h8S`kauAr~ znXdTa@fud{jRGv(2OAO;mAJb6KQwFIxNmharl>PLL?mg_?z=PZ?wu)Mo4xSuwS-IU zk?ahmw*TLL4EZm4V)Zw_9g3=KWe!P`l`giYpY&fLwBnN=<4$G6Et{2g@tesDsPY}? zvWV-LSUfc(zG`o4!Cn=Eq&I60z5AV~vww+-8dHUA@YcC)-#Ri`6wFST7b~VQI?UBP z_U8I0Ys35%3tpOhvQ7G^f8Y~)!-A6Vf+P=Ug_CHvJE`*K V@_u*pU|?Wi@O1TaS?83{1OOS_e`^2$ literal 0 HcmV?d00001 diff --git a/cmd/astrolabe/static/favicon-32x32.png b/cmd/astrolabe/static/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..869f6df5fc6bdd2c78f2ac91679fb6814f999485 GIT binary patch literal 769 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANM!Cp=voLn7SQhVRV^aTNJ? z?x=|thh59mt-TMtJvLl+Di9Uyb#!tO5o~?npD(#vVPA`YK(@C0A+1{~XDZU(&E5Tc z-dtnj@|&8QZqBu=zyDKx$>z8EkComnIILR{!}_Ok-=YfHf4@FP`wQ;dE??*S|Hn`L zW!8r{`94nlr*iE;Zar)Ck25jrKOZjsbCcVzsfcR}cl^FDPj1dI{`XK~dBg+mziKR} z^&d*d-)ZY;H{*apnF2}VvG9j!;jSyANoi9X!T5Z+7f-?nuvEv zWYsV49q*g$Z@txZj1ErwIxn&Qfm6z@%&FX4JQE}hjz=UWx$P?9;E!zdoojl>rNTrb zKxvv*)Wv+ujS|d^i;u5qXl;Bd6+7culEw>8f2r7j=Iv$qJ7#WB7P%l6UFx`dNyOil zl(YIJ9B;H(QbgCVE*IJ3SJkF+aPuT?k?U%TuL6&nw%9s1dPFoFe#N`59|wVvq6EYR*NG<|*i0k8ARYPr*$$BWLNdh$nm~)O+W164QsIPo3e$=4;Xn!Z!DEi1>8;y2_Ji%KcnffBD=9 zzh>(wfA4KQU+wdp)8BkbHmhp9yJ5~U;Ry|ozwUW`z4+1BJ=0{@dL3J2zvlTlS;o7H e^S-_QS+9J7zr7+tu!n(xfx*+&&t;ucLK6T@oM(;z literal 0 HcmV?d00001 diff --git a/cmd/astrolabe/static/favicon.ico b/cmd/astrolabe/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b4290a797a9f7ae4d14373ff1608be81d5940725 GIT binary patch literal 15406 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 aVb=d72Jin73{tQ(19I;u9t{CXLjVA0l*gw4 literal 0 HcmV?d00001 diff --git a/cmd/astrolabe/static/favicon.png b/cmd/astrolabe/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..61cf7c943b6ac083ab0752f283391e37b39c1221 GIT binary patch literal 6405 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 Q6Eu|X>FVdQ&MBb@0K66k1ONa4 literal 0 HcmV?d00001 diff --git a/cmd/astrolabe/static/robots.txt b/cmd/astrolabe/static/robots.txt new file mode 100644 index 0000000..4f8510d --- /dev/null +++ b/cmd/astrolabe/static/robots.txt @@ -0,0 +1,9 @@ +# Hello Friends! +# If you are considering bulk or automated crawling, you may want to look in +# to our protocol (API), including a firehose of updates. See: https://atproto.com/ + +# By default, may crawl anything on this domain. HTTP 429 ("backoff") status +# codes are used for rate-limiting. Up to a handful concurrent requests should +# be ok. +User-Agent: * +Allow: / diff --git a/cmd/astrolabe/templates/account.html b/cmd/astrolabe/templates/account.html new file mode 100644 index 0000000..dd27bc3 --- /dev/null +++ b/cmd/astrolabe/templates/account.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block main_content %} +

{{ atid }}

+ + + + + + + + + + +
DID{{ ident.DID }}
Handle{{ ident.Handle }}
PDS{{ ident.PDSEndpoint() }}
+ +

Repo Index +

Repo CAR Export + +{% if didDocJSON %} +

DID Document

+
{{ didDocJSON }}
+{% endif %} +{% endblock %} diff --git a/cmd/astrolabe/templates/base.html b/cmd/astrolabe/templates/base.html new file mode 100644 index 0000000..a7e4202 --- /dev/null +++ b/cmd/astrolabe/templates/base.html @@ -0,0 +1,40 @@ + + + + + + + + + + + {% block head_title %}astrolabe{% endblock %} + + + + +
+ {% block main_content %}Base Template{% endblock %} +
+ + + diff --git a/cmd/astrolabe/templates/error.html b/cmd/astrolabe/templates/error.html new file mode 100644 index 0000000..08ea1c6 --- /dev/null +++ b/cmd/astrolabe/templates/error.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block head_title %}Error {{ statusCode }} - astrolabe{% endblock %} + +{% block main_content %} +
+
+

{{ statusCode }}

+

Error!

+ {% if errorMessage %} +

{{ errorMessage }}

+ {% endif %} +
+{% endblock %} diff --git a/cmd/astrolabe/templates/home.html b/cmd/astrolabe/templates/home.html new file mode 100644 index 0000000..dd5a8b3 --- /dev/null +++ b/cmd/astrolabe/templates/home.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block main_content %} +

astrolabe: AT Protocol Repository Browser

+ +

This is a tool for browsing AT Protocol ("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. + +

Examples: +

+ +

Other similar services include atproto-browser.vercel.app and pdsls.dev. +{% endblock %} diff --git a/cmd/astrolabe/templates/repo.html b/cmd/astrolabe/templates/repo.html new file mode 100644 index 0000000..d139ec8 --- /dev/null +++ b/cmd/astrolabe/templates/repo.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block main_content %} +

at://{{ atid }}

+ +

Index

+ + + + {% for collection in collections %} + + {% endfor %} + +
..
{{ collection }}/
+ +{% endblock %} diff --git a/cmd/astrolabe/templates/repo_collection.html b/cmd/astrolabe/templates/repo_collection.html new file mode 100644 index 0000000..802b135 --- /dev/null +++ b/cmd/astrolabe/templates/repo_collection.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block main_content %} +

at://{{ atid }}/{{ collection }}

+ +

Index

+ + + + {% for uri in recordURIs %} + + {% endfor %} + {% if cursor != "" %} + + {% endif %} + +
..
{{ collection }}/{{ uri.RecordKey() }}
[more]
+ +{% endblock %} diff --git a/cmd/astrolabe/templates/repo_record.html b/cmd/astrolabe/templates/repo_record.html new file mode 100644 index 0000000..09da3c5 --- /dev/null +++ b/cmd/astrolabe/templates/repo_record.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block main_content %} +

at://{{ atid }}/{{ collection }}/{{ rkey }}

+

Back to Collection + +{% if recordJSON %} +

Record JSON

+
{{ recordJSON }}
+{% endif %} + +{% endblock %}