Bluesky app fork with some witchin' additions 💫

bskyweb additions (#296)

Add some minor bskyweb improvements, Mailmodo endpoint, Dockerfile for bskyweb, container image push

authored by

Jake Gold and committed by
GitHub
67e4882b d8f44756

+458 -84
+54
.github/workflows/build-and-push-bskyweb-aws.yaml
··· 1 + name: build-and-push-bskyweb-aws 2 + on: 3 + push: 4 + branches: 5 + - main 6 + - jake/bskyweb-additions 7 + env: 8 + REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} 9 + USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} 10 + PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }} 11 + IMAGE_NAME: bskyweb 12 + 13 + jobs: 14 + bskyweb-container-aws: 15 + runs-on: ubuntu-latest 16 + permissions: 17 + contents: read 18 + packages: write 19 + id-token: write 20 + 21 + steps: 22 + - name: Checkout repository 23 + uses: actions/checkout@v3 24 + 25 + - name: Setup Docker buildx 26 + uses: docker/setup-buildx-action@v1 27 + 28 + - name: Log into registry ${{ env.REGISTRY }} 29 + uses: docker/login-action@v2 30 + with: 31 + registry: ${{ env.REGISTRY }} 32 + username: ${{ env.USERNAME}} 33 + password: ${{ env.PASSWORD }} 34 + 35 + - name: Extract Docker metadata 36 + id: meta 37 + uses: docker/metadata-action@v4 38 + with: 39 + images: | 40 + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 41 + tags: | 42 + type=sha,enable=true,priority=100,prefix=,suffix=,format=long 43 + 44 + - name: Build and push Docker image 45 + id: build-and-push 46 + uses: docker/build-push-action@v4 47 + with: 48 + context: . 49 + push: ${{ github.event_name != 'pull_request' }} 50 + file: ./Dockerfile 51 + tags: ${{ steps.meta.outputs.tags }} 52 + labels: ${{ steps.meta.outputs.labels }} 53 + cache-from: type=gha 54 + cache-to: type=gha,mode=max
+56
.github/workflows/build-and-push-bskyweb-ghcr.yaml
··· 1 + name: build-and-push-bskyweb-ghcr 2 + on: 3 + push: 4 + branches: 5 + - main 6 + - jake/bskyweb-additions 7 + env: 8 + REGISTRY: ghcr.io 9 + USERNAME: ${{ github.actor }} 10 + PASSWORD: ${{ secrets.GITHUB_TOKEN }} 11 + 12 + # github.repository as <account>/<repo> 13 + IMAGE_NAME: ${{ github.repository }} 14 + 15 + jobs: 16 + bskyweb-container-ghcr: 17 + runs-on: ubuntu-latest 18 + permissions: 19 + contents: read 20 + packages: write 21 + id-token: write 22 + 23 + steps: 24 + - name: Checkout repository 25 + uses: actions/checkout@v3 26 + 27 + - name: Setup Docker buildx 28 + uses: docker/setup-buildx-action@v1 29 + 30 + - name: Log into registry ${{ env.REGISTRY }} 31 + uses: docker/login-action@v2 32 + with: 33 + registry: ${{ env.REGISTRY }} 34 + username: ${{ env.USERNAME }} 35 + password: ${{ env.PASSWORD }} 36 + 37 + - name: Extract Docker metadata 38 + id: meta 39 + uses: docker/metadata-action@v4 40 + with: 41 + images: | 42 + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 43 + tags: | 44 + type=sha,enable=true,priority=100,prefix=bskyweb:,suffix=,format=long 45 + 46 + - name: Build and push Docker image 47 + id: build-and-push 48 + uses: docker/build-push-action@v4 49 + with: 50 + context: . 51 + push: ${{ github.event_name != 'pull_request' }} 52 + file: ./Dockerfile 53 + tags: ${{ steps.meta.outputs.tags }} 54 + labels: ${{ steps.meta.outputs.labels }} 55 + cache-from: type=gha 56 + cache-to: type=gha,mode=max
+3
.prettierignore
··· 6 6 /bskyweb/templates 7 7 /dist/ 8 8 /.watchmanconfig 9 + 10 + web/index.html 11 + web-build/*
+75
Dockerfile
··· 1 + FROM golang:1.20-bullseye AS build-env 2 + 3 + WORKDIR /usr/src/social-app 4 + 5 + ENV DEBIAN_FRONTEND=noninteractive 6 + 7 + # Node 8 + ENV NODE_VERSION=18 9 + ENV NVM_DIR=/usr/share/nvm 10 + 11 + # Go 12 + ENV GODEBUG="netdns=go" 13 + ENV GOOS="linux" 14 + ENV GOARCH="amd64" 15 + ENV CGO_ENABLED=1 16 + 17 + COPY . . 18 + 19 + # 20 + # Generate the Javascript webpack. 21 + # 22 + RUN mkdir --parents $NVM_DIR && \ 23 + wget \ 24 + --output-document=/tmp/nvm-install.sh \ 25 + https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh && \ 26 + bash /tmp/nvm-install.sh 27 + 28 + RUN \. "$NVM_DIR/nvm.sh" && \ 29 + nvm install $NODE_VERSION && \ 30 + nvm use $NODE_VERSION && \ 31 + npm install --global yarn && \ 32 + yarn && \ 33 + yarn build-web 34 + 35 + # DEBUG 36 + RUN find ./bskyweb/static && find ./web-build/static 37 + 38 + # Copy the bundle js files. 39 + RUN cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/ 40 + 41 + # 42 + # Generate the bksyweb Go binary. 43 + # 44 + RUN cd bskyweb/ && \ 45 + go mod download && \ 46 + go mod verify 47 + 48 + RUN cd bskyweb/ && \ 49 + go build \ 50 + -v \ 51 + -trimpath \ 52 + -tags timetzdata \ 53 + -o /bskyweb \ 54 + ./cmd/bskyweb 55 + 56 + FROM debian:bullseye-slim 57 + 58 + ENV GODEBUG=netdns=go 59 + ENV TZ=Etc/UTC 60 + ENV DEBIAN_FRONTEND=noninteractive 61 + 62 + RUN apt-get update && apt-get install --yes \ 63 + dumb-init \ 64 + ca-certificates 65 + 66 + ENTRYPOINT ["dumb-init", "--"] 67 + 68 + WORKDIR /bskyweb 69 + COPY --from=build-env /bskyweb /usr/bin/bskyweb 70 + 71 + CMD ["/usr/bin/bskyweb"] 72 + 73 + LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app 74 + LABEL org.opencontainers.image.description="bsky.app Web App" 75 + LABEL org.opencontainers.image.licenses=MIT
+3 -2
bskyweb/.gitignore
··· 1 - /static/bundle.web.js 1 + .env 2 + 3 + # Don't check in the binary. 2 4 /bskyweb 3 - .env
+68
bskyweb/cmd/bskyweb/mailmodo.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "crypto/sha256" 7 + "encoding/json" 8 + "fmt" 9 + "net/http" 10 + "time" 11 + ) 12 + 13 + type Mailmodo struct { 14 + httpClient *http.Client 15 + APIKey string 16 + BaseURL string 17 + } 18 + 19 + func NewMailmodo(apiKey string) *Mailmodo { 20 + return &Mailmodo{ 21 + APIKey: apiKey, 22 + BaseURL: "https://api.mailmodo.com/api/v1", 23 + httpClient: &http.Client{}, 24 + } 25 + } 26 + 27 + func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod string, data any) error { 28 + endpoint := fmt.Sprintf("%s/%s", m.BaseURL, apiMethod) 29 + js, err := json.Marshal(data) 30 + if err != nil { 31 + return fmt.Errorf("Mailmodo JSON encoding failed: %w", err) 32 + } 33 + req, err := http.NewRequestWithContext(ctx, httpMethod, endpoint, bytes.NewBuffer(js)) 34 + if err != nil { 35 + return fmt.Errorf("Mailmodo HTTP creating request %s %s failed: %w", httpMethod, apiMethod, err) 36 + } 37 + req.Header.Set("mmApiKey", m.APIKey) 38 + req.Header.Set("Content-Type", "application/json") 39 + 40 + res, err := m.httpClient.Do(req) 41 + if err != nil { 42 + return fmt.Errorf("Mailmodo HTTP making request %s %s failed: %w", httpMethod, apiMethod, err) 43 + } 44 + defer res.Body.Close() 45 + 46 + status := struct { 47 + Success bool `json:"success"` 48 + Message string `json:"message"` 49 + }{} 50 + if err := json.NewDecoder(res.Body).Decode(&status); err != nil { 51 + return fmt.Errorf("Mailmodo HTTP parsing response %s %s failed: %w", httpMethod, apiMethod, err) 52 + } 53 + if !status.Success { 54 + return fmt.Errorf("Mailmodo API response %s %s failed: %s", httpMethod, apiMethod, status.Message) 55 + } 56 + return nil 57 + } 58 + 59 + func (m *Mailmodo) AddToList(ctx context.Context, listName, email string) error { 60 + return m.request(ctx, "POST", "addToList", map[string]any{ 61 + "listName": listName, 62 + "email": email, 63 + "data": map[string]any{ 64 + "email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))), 65 + }, 66 + "created_at": time.Now().UTC().Format(time.RFC3339), 67 + }) 68 + }
+46 -22
bskyweb/cmd/bskyweb/main.go
··· 35 35 Usage: "web server for bsky.app web app (SPA)", 36 36 } 37 37 38 - app.Flags = []cli.Flag{ 39 - &cli.StringFlag{ 40 - Name: "pds-host", 41 - Usage: "method, hostname, and port of PDS instance", 42 - Value: "http://localhost:4849", 43 - EnvVars: []string{"ATP_PDS_HOST"}, 44 - }, 45 - &cli.StringFlag{ 46 - Name: "handle", 47 - Usage: "for PDS login", 48 - Required: true, 49 - EnvVars: []string{"ATP_AUTH_HANDLE"}, 50 - }, 51 - &cli.StringFlag{ 52 - Name: "password", 53 - Usage: "for PDS login", 54 - Required: true, 55 - EnvVars: []string{"ATP_AUTH_PASSWORD"}, 56 - }, 57 - // TODO: local IP/port to bind on 58 - } 59 - 60 38 app.Commands = []*cli.Command{ 61 39 &cli.Command{ 62 40 Name: "serve", 63 41 Usage: "run the server", 64 42 Action: serve, 43 + Flags: []cli.Flag{ 44 + &cli.StringFlag{ 45 + Name: "pds-host", 46 + Usage: "method, hostname, and port of PDS instance", 47 + Value: "http://localhost:4849", 48 + EnvVars: []string{"ATP_PDS_HOST"}, 49 + }, 50 + &cli.StringFlag{ 51 + Name: "handle", 52 + Usage: "for PDS login", 53 + Required: true, 54 + EnvVars: []string{"ATP_AUTH_HANDLE"}, 55 + }, 56 + &cli.StringFlag{ 57 + Name: "password", 58 + Usage: "for PDS login", 59 + Required: true, 60 + EnvVars: []string{"ATP_AUTH_PASSWORD"}, 61 + }, 62 + &cli.StringFlag{ 63 + Name: "mailmodo-api-key", 64 + Usage: "Mailmodo API key", 65 + Required: false, 66 + EnvVars: []string{"MAILMODO_API_KEY"}, 67 + }, 68 + &cli.StringFlag{ 69 + Name: "mailmodo-list-name", 70 + Usage: "Mailmodo contact list to add email addresses to", 71 + Required: false, 72 + EnvVars: []string{"MAILMODO_LIST_NAME"}, 73 + }, 74 + &cli.StringFlag{ 75 + Name: "http-address", 76 + Usage: "Specify the local IP/port to bind to", 77 + Required: false, 78 + Value: ":8100", 79 + EnvVars: []string{"HTTP_ADDRESS"}, 80 + }, 81 + &cli.BoolFlag{ 82 + Name: "debug", 83 + Usage: "Enable debug mode", 84 + Value: false, 85 + Required: false, 86 + EnvVars: []string{"DEBUG"}, 87 + }, 88 + }, 65 89 }, 66 90 } 67 91 app.RunAndExitOnError()
+82
bskyweb/cmd/bskyweb/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 + type RendererLoader struct { 16 + prefix string 17 + fs *embed.FS 18 + } 19 + 20 + func NewRendererLoader(prefix string, fs *embed.FS) pongo2.TemplateLoader { 21 + return &RendererLoader{ 22 + prefix: prefix, 23 + fs: fs, 24 + } 25 + } 26 + func (l *RendererLoader) Abs(_, name string) string { 27 + // TODO: remove this workaround 28 + // Figure out why this method is being called 29 + // twice on template names resulting in a failure to resolve 30 + // the template name. 31 + if filepath.HasPrefix(name, l.prefix) { 32 + return name 33 + } 34 + return filepath.Join(l.prefix, name) 35 + } 36 + 37 + func (l *RendererLoader) Get(path string) (io.Reader, error) { 38 + b, err := l.fs.ReadFile(path) 39 + if err != nil { 40 + return nil, fmt.Errorf("reading template %q failed: %w", path, err) 41 + } 42 + return bytes.NewReader(b), nil 43 + } 44 + 45 + type Renderer struct { 46 + TemplateSet *pongo2.TemplateSet 47 + Debug bool 48 + } 49 + 50 + func NewRenderer(prefix string, fs *embed.FS, debug bool) *Renderer { 51 + return &Renderer{ 52 + TemplateSet: pongo2.NewSet(prefix, NewRendererLoader(prefix, fs)), 53 + Debug: debug, 54 + } 55 + } 56 + 57 + func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 58 + var ctx pongo2.Context 59 + 60 + if data != nil { 61 + var ok bool 62 + ctx, ok = data.(pongo2.Context) 63 + if !ok { 64 + return errors.New("no pongo2.Context data was passed") 65 + } 66 + } 67 + 68 + var t *pongo2.Template 69 + var err error 70 + 71 + if r.Debug { 72 + t, err = pongo2.FromFile(name) 73 + } else { 74 + t, err = r.TemplateSet.FromFile(name) 75 + } 76 + 77 + if err != nil { 78 + return err 79 + } 80 + 81 + return t.ExecuteWriter(ctx, w) 82 + }
+53 -55
bskyweb/cmd/bskyweb/server.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "errors" 6 5 "fmt" 7 - "io" 6 + "io/fs" 8 7 "net/http" 8 + "os" 9 + "strings" 9 10 10 11 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 12 appbsky "github.com/bluesky-social/indigo/api/bsky" 12 13 cliutil "github.com/bluesky-social/indigo/cmd/gosky/util" 13 14 "github.com/bluesky-social/indigo/xrpc" 15 + "github.com/bluesky-social/social-app/bskyweb" 14 16 15 17 "github.com/flosch/pongo2/v6" 16 18 "github.com/labstack/echo/v4" ··· 18 20 "github.com/urfave/cli/v2" 19 21 ) 20 22 21 - // TODO: embed templates in executable 22 - 23 - type Renderer struct { 24 - Debug bool 25 - } 26 - 27 - func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 28 - 29 - var ctx pongo2.Context 30 - 31 - if data != nil { 32 - var ok bool 33 - ctx, ok = data.(pongo2.Context) 34 - 35 - if !ok { 36 - return errors.New("no pongo2.Context data was passed...") 37 - } 38 - } 39 - 40 - var t *pongo2.Template 41 - var err error 42 - 43 - if r.Debug { 44 - t, err = pongo2.FromFile(name) 45 - } else { 46 - t, err = pongo2.FromCache(name) 47 - } 48 - 49 - if err != nil { 50 - return err 51 - } 52 - 53 - return t.ExecuteWriter(ctx, w) 54 - } 55 - 56 23 type Server struct { 57 24 xrpcc *xrpc.Client 58 25 } 59 26 60 27 func serve(cctx *cli.Context) error { 28 + debug := cctx.Bool("debug") 29 + httpAddress := cctx.String("http-address") 30 + pdsHost := cctx.String("pds-host") 31 + atpHandle := cctx.String("handle") 32 + atpPassword := cctx.String("password") 33 + mailmodoAPIKey := cctx.String("mailmodo-api-key") 34 + mailmodoListName := cctx.String("mailmodo-list-name") 35 + 36 + // Mailmodo client. 37 + mailmodo := NewMailmodo(mailmodoAPIKey) 61 38 62 39 // create a new session 63 40 // TODO: does this work with no auth at all? 64 41 xrpcc := &xrpc.Client{ 65 42 Client: cliutil.NewHttpClient(), 66 - Host: cctx.String("pds-host"), 43 + Host: pdsHost, 67 44 Auth: &xrpc.AuthInfo{ 68 - Handle: cctx.String("handle"), 45 + Handle: atpHandle, 69 46 }, 70 47 } 71 48 72 49 auth, err := comatproto.SessionCreate(context.TODO(), xrpcc, &comatproto.SessionCreate_Input{ 73 50 Identifier: &xrpcc.Auth.Handle, 74 - Password: cctx.String("password"), 51 + Password: atpPassword, 75 52 }) 76 53 if err != nil { 77 54 return err ··· 82 59 xrpcc.Auth.Handle = auth.Handle 83 60 84 61 server := Server{xrpcc} 62 + 63 + staticHandler := http.FileServer(func() http.FileSystem { 64 + if debug { 65 + return http.FS(os.DirFS("static")) 66 + } 67 + fsys, err := fs.Sub(bskyweb.StaticFS, "static") 68 + if err != nil { 69 + log.Fatal(err) 70 + } 71 + return http.FS(fsys) 72 + }()) 85 73 86 74 e := echo.New() 87 75 e.HideBanner = true 88 76 e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ 77 + // Don't log requests for static content. 78 + Skipper: func(c echo.Context) bool { 79 + return strings.HasPrefix(c.Request().URL.Path, "/static") 80 + }, 89 81 Format: "method=${method} path=${uri} status=${status} latency=${latency_human}\n", 90 82 })) 91 - e.Renderer = Renderer{Debug: true} 83 + e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug) 92 84 e.HTTPErrorHandler = customHTTPErrorHandler 93 85 94 86 // configure routes 95 - e.File("/robots.txt", "static/robots.txt") 96 - e.Static("/static", "static") 97 - e.Static("/static/js", "../web-build/static/js") 98 - 87 + e.GET("/robots.txt", echo.WrapHandler(staticHandler)) 88 + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) 99 89 e.GET("/", server.WebHome) 100 90 101 91 // generic routes ··· 118 108 e.GET("/profile/:handle/post/:rkey/downvoted-by", server.WebGeneric) 119 109 e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric) 120 110 121 - bind := "localhost:8100" 122 - log.Infof("starting server bind=%s", bind) 123 - return e.Start(bind) 111 + // Mailmodo 112 + e.POST("/waitlist", func(c echo.Context) error { 113 + email := strings.TrimSpace(c.FormValue("email")) 114 + if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, email); err != nil { 115 + return err 116 + } 117 + return c.JSON(http.StatusOK, map[string]bool{"success": true}) 118 + }) 119 + 120 + log.Infof("starting server address=%s", httpAddress) 121 + return e.Start(httpAddress) 124 122 } 125 123 126 124 func customHTTPErrorHandler(err error, c echo.Context) { ··· 132 130 data := pongo2.Context{ 133 131 "statusCode": code, 134 132 } 135 - c.Render(code, "templates/error.html", data) 133 + c.Render(code, "error.html", data) 136 134 } 137 135 138 136 // handler for endpoint that have no specific server-side handling 139 137 func (srv *Server) WebGeneric(c echo.Context) error { 140 138 data := pongo2.Context{} 141 - return c.Render(http.StatusOK, "templates/base.html", data) 139 + return c.Render(http.StatusOK, "base.html", data) 142 140 } 143 141 144 142 func (srv *Server) WebHome(c echo.Context) error { 145 143 data := pongo2.Context{} 146 - return c.Render(http.StatusOK, "templates/home.html", data) 144 + return c.Render(http.StatusOK, "home.html", data) 147 145 } 148 146 149 147 func (srv *Server) WebPost(c echo.Context) error { ··· 152 150 rkey := c.Param("rkey") 153 151 // sanity check argument 154 152 if len(handle) > 4 && len(handle) < 128 && len(rkey) > 0 { 155 - ctx := context.TODO() 153 + ctx := c.Request().Context() 156 154 // requires two fetches: first fetch profile (!) 157 155 pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) 158 156 if err != nil { ··· 172 170 } 173 171 174 172 } 175 - return c.Render(http.StatusOK, "templates/post.html", data) 173 + return c.Render(http.StatusOK, "post.html", data) 176 174 } 177 175 178 176 func (srv *Server) WebProfile(c echo.Context) error { ··· 180 178 handle := c.Param("handle") 181 179 // sanity check argument 182 180 if len(handle) > 4 && len(handle) < 128 { 183 - ctx := context.TODO() 181 + ctx := c.Request().Context() 184 182 pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) 185 183 if err != nil { 186 184 log.Warnf("failed to fetch handle: %s\t%v", handle, err) ··· 189 187 } 190 188 } 191 189 192 - return c.Render(http.StatusOK, "templates/profile.html", data) 190 + return c.Render(http.StatusOK, "profile.html", data) 193 191 }
+1 -1
bskyweb/go.mod
··· 4 4 5 5 require ( 6 6 github.com/bluesky-social/indigo v0.0.0-20230307000525-294e33e70185 7 + github.com/flosch/pongo2/v6 v6.0.0 7 8 github.com/ipfs/go-log v1.0.5 8 9 github.com/joho/godotenv v1.5.1 9 10 github.com/labstack/echo/v4 v4.10.2 ··· 13 14 require ( 14 15 github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 15 16 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect 16 - github.com/flosch/pongo2/v6 v6.0.0 // indirect 17 17 github.com/go-logr/logr v1.2.3 // indirect 18 18 github.com/go-logr/stdr v1.2.2 // indirect 19 19 github.com/goccy/go-json v0.10.0 // indirect
+4 -2
bskyweb/go.sum
··· 1 1 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 2 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 3 3 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 4 - github.com/bluesky-social/indigo v0.0.0-20230306194356-5958f14d5152 h1:7fHM+tQHJN5lsMU8FvV4bNuWpD0Dd+pAUSuoLYdcYIQ= 5 - github.com/bluesky-social/indigo v0.0.0-20230306194356-5958f14d5152/go.mod h1:xy2hI4NMC6fgUefSJcCst6E0yo9Xbfd97aF27lgHyHE= 6 4 github.com/bluesky-social/indigo v0.0.0-20230307000525-294e33e70185 h1:WnaOpRFWE8Tmw0IeXEEthsqBZtNG6/niokmWANv/aEU= 7 5 github.com/bluesky-social/indigo v0.0.0-20230307000525-294e33e70185/go.mod h1:xy2hI4NMC6fgUefSJcCst6E0yo9Xbfd97aF27lgHyHE= 8 6 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= ··· 64 62 github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 65 63 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 66 64 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 65 + github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 67 66 github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 68 67 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 69 68 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 69 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 70 70 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 71 71 github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= 72 72 github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= ··· 129 129 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 130 130 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 131 131 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 132 + github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 132 133 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 133 134 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 134 135 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= ··· 266 267 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 267 268 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 268 269 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 270 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 269 271 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 270 272 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 271 273 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+6
bskyweb/static.go
··· 1 + package bskyweb 2 + 3 + import "embed" 4 + 5 + //go:embed static/* 6 + var StaticFS embed.FS
+1
bskyweb/static/js/.gitkeep
··· 1 + NOOP
+6
bskyweb/templates.go
··· 1 + package bskyweb 2 + 3 + import "embed" 4 + 5 + //go:embed templates/* 6 + var TemplateFS embed.FS
-2
bskyweb/templates/scripts.html
··· 1 - <script defer="defer" src="/static/js/412.e47ad7b9.js"></script> 2 - <script defer="defer" src="/static/js/main.f526ceaa.js"></script>