diff options
author | Jake Gold <52801504+Jacob2161@users.noreply.github.com> | 2023-03-20 14:41:15 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-20 14:41:15 -0700 |
commit | 67e4882bb372bc45d178e3eacc409cdbf60f1344 (patch) | |
tree | 13e3c015e91b5009597d9b610a9c6a40764cbfcc /bskyweb/cmd | |
parent | d8f4475696094382e2196ce259af358b65c849fb (diff) | |
download | voidsky-67e4882bb372bc45d178e3eacc409cdbf60f1344.tar.zst |
bskyweb additions (#296)
Add some minor bskyweb improvements, Mailmodo endpoint, Dockerfile for bskyweb, container image push
Diffstat (limited to 'bskyweb/cmd')
-rw-r--r-- | bskyweb/cmd/bskyweb/mailmodo.go | 68 | ||||
-rw-r--r-- | bskyweb/cmd/bskyweb/main.go | 68 | ||||
-rw-r--r-- | bskyweb/cmd/bskyweb/renderer.go | 82 | ||||
-rw-r--r-- | bskyweb/cmd/bskyweb/server.go | 108 |
4 files changed, 249 insertions, 77 deletions
diff --git a/bskyweb/cmd/bskyweb/mailmodo.go b/bskyweb/cmd/bskyweb/mailmodo.go new file mode 100644 index 000000000..67ee6a2d6 --- /dev/null +++ b/bskyweb/cmd/bskyweb/mailmodo.go @@ -0,0 +1,68 @@ +package main + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "net/http" + "time" +) + +type Mailmodo struct { + httpClient *http.Client + APIKey string + BaseURL string +} + +func NewMailmodo(apiKey string) *Mailmodo { + return &Mailmodo{ + APIKey: apiKey, + BaseURL: "https://api.mailmodo.com/api/v1", + httpClient: &http.Client{}, + } +} + +func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod string, data any) error { + endpoint := fmt.Sprintf("%s/%s", m.BaseURL, apiMethod) + js, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("Mailmodo JSON encoding failed: %w", err) + } + req, err := http.NewRequestWithContext(ctx, httpMethod, endpoint, bytes.NewBuffer(js)) + if err != nil { + return fmt.Errorf("Mailmodo HTTP creating request %s %s failed: %w", httpMethod, apiMethod, err) + } + req.Header.Set("mmApiKey", m.APIKey) + req.Header.Set("Content-Type", "application/json") + + res, err := m.httpClient.Do(req) + if err != nil { + return fmt.Errorf("Mailmodo HTTP making request %s %s failed: %w", httpMethod, apiMethod, err) + } + defer res.Body.Close() + + status := struct { + Success bool `json:"success"` + Message string `json:"message"` + }{} + if err := json.NewDecoder(res.Body).Decode(&status); err != nil { + return fmt.Errorf("Mailmodo HTTP parsing response %s %s failed: %w", httpMethod, apiMethod, err) + } + if !status.Success { + return fmt.Errorf("Mailmodo API response %s %s failed: %s", httpMethod, apiMethod, status.Message) + } + return nil +} + +func (m *Mailmodo) AddToList(ctx context.Context, listName, email string) error { + return m.request(ctx, "POST", "addToList", map[string]any{ + "listName": listName, + "email": email, + "data": map[string]any{ + "email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))), + }, + "created_at": time.Now().UTC().Format(time.RFC3339), + }) +} diff --git a/bskyweb/cmd/bskyweb/main.go b/bskyweb/cmd/bskyweb/main.go index 4b691b686..fd1429902 100644 --- a/bskyweb/cmd/bskyweb/main.go +++ b/bskyweb/cmd/bskyweb/main.go @@ -35,33 +35,57 @@ func run(args []string) { Usage: "web server for bsky.app web app (SPA)", } - app.Flags = []cli.Flag{ - &cli.StringFlag{ - Name: "pds-host", - Usage: "method, hostname, and port of PDS instance", - Value: "http://localhost:4849", - EnvVars: []string{"ATP_PDS_HOST"}, - }, - &cli.StringFlag{ - Name: "handle", - Usage: "for PDS login", - Required: true, - EnvVars: []string{"ATP_AUTH_HANDLE"}, - }, - &cli.StringFlag{ - Name: "password", - Usage: "for PDS login", - Required: true, - EnvVars: []string{"ATP_AUTH_PASSWORD"}, - }, - // TODO: local IP/port to bind on - } - app.Commands = []*cli.Command{ &cli.Command{ Name: "serve", Usage: "run the server", Action: serve, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "pds-host", + Usage: "method, hostname, and port of PDS instance", + Value: "http://localhost:4849", + EnvVars: []string{"ATP_PDS_HOST"}, + }, + &cli.StringFlag{ + Name: "handle", + Usage: "for PDS login", + Required: true, + EnvVars: []string{"ATP_AUTH_HANDLE"}, + }, + &cli.StringFlag{ + Name: "password", + Usage: "for PDS login", + Required: true, + EnvVars: []string{"ATP_AUTH_PASSWORD"}, + }, + &cli.StringFlag{ + Name: "mailmodo-api-key", + Usage: "Mailmodo API key", + Required: false, + EnvVars: []string{"MAILMODO_API_KEY"}, + }, + &cli.StringFlag{ + Name: "mailmodo-list-name", + Usage: "Mailmodo contact list to add email addresses to", + Required: false, + EnvVars: []string{"MAILMODO_LIST_NAME"}, + }, + &cli.StringFlag{ + Name: "http-address", + Usage: "Specify the local IP/port to bind to", + Required: false, + Value: ":8100", + EnvVars: []string{"HTTP_ADDRESS"}, + }, + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug mode", + Value: false, + Required: false, + EnvVars: []string{"DEBUG"}, + }, + }, }, } app.RunAndExitOnError() diff --git a/bskyweb/cmd/bskyweb/renderer.go b/bskyweb/cmd/bskyweb/renderer.go new file mode 100644 index 000000000..4bf8b80c5 --- /dev/null +++ b/bskyweb/cmd/bskyweb/renderer.go @@ -0,0 +1,82 @@ +package main + +import ( + "bytes" + "embed" + "errors" + "fmt" + "io" + "path/filepath" + + "github.com/flosch/pongo2/v6" + "github.com/labstack/echo/v4" +) + +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/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 7ae0fb54f..efa5f2057 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -2,15 +2,17 @@ package main import ( "context" - "errors" "fmt" - "io" + "io/fs" "net/http" + "os" + "strings" comatproto "github.com/bluesky-social/indigo/api/atproto" appbsky "github.com/bluesky-social/indigo/api/bsky" cliutil "github.com/bluesky-social/indigo/cmd/gosky/util" "github.com/bluesky-social/indigo/xrpc" + "github.com/bluesky-social/social-app/bskyweb" "github.com/flosch/pongo2/v6" "github.com/labstack/echo/v4" @@ -18,60 +20,35 @@ import ( "github.com/urfave/cli/v2" ) -// TODO: embed templates in executable - -type Renderer struct { - Debug bool -} - -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 = pongo2.FromCache(name) - } - - if err != nil { - return err - } - - return t.ExecuteWriter(ctx, w) -} - type Server struct { xrpcc *xrpc.Client } func serve(cctx *cli.Context) error { + debug := cctx.Bool("debug") + httpAddress := cctx.String("http-address") + pdsHost := cctx.String("pds-host") + atpHandle := cctx.String("handle") + atpPassword := cctx.String("password") + mailmodoAPIKey := cctx.String("mailmodo-api-key") + mailmodoListName := cctx.String("mailmodo-list-name") + + // Mailmodo client. + mailmodo := NewMailmodo(mailmodoAPIKey) // create a new session // TODO: does this work with no auth at all? xrpcc := &xrpc.Client{ Client: cliutil.NewHttpClient(), - Host: cctx.String("pds-host"), + Host: pdsHost, Auth: &xrpc.AuthInfo{ - Handle: cctx.String("handle"), + Handle: atpHandle, }, } auth, err := comatproto.SessionCreate(context.TODO(), xrpcc, &comatproto.SessionCreate_Input{ Identifier: &xrpcc.Auth.Handle, - Password: cctx.String("password"), + Password: atpPassword, }) if err != nil { return err @@ -83,19 +60,32 @@ func serve(cctx *cli.Context) error { server := Server{xrpcc} + staticHandler := http.FileServer(func() http.FileSystem { + if debug { + return http.FS(os.DirFS("static")) + } + fsys, err := fs.Sub(bskyweb.StaticFS, "static") + if err != nil { + log.Fatal(err) + } + return http.FS(fsys) + }()) + e := echo.New() e.HideBanner = true e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ + // Don't log requests for static content. + Skipper: func(c echo.Context) bool { + return strings.HasPrefix(c.Request().URL.Path, "/static") + }, Format: "method=${method} path=${uri} status=${status} latency=${latency_human}\n", })) - e.Renderer = Renderer{Debug: true} + e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug) e.HTTPErrorHandler = customHTTPErrorHandler // configure routes - e.File("/robots.txt", "static/robots.txt") - e.Static("/static", "static") - e.Static("/static/js", "../web-build/static/js") - + e.GET("/robots.txt", echo.WrapHandler(staticHandler)) + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) e.GET("/", server.WebHome) // generic routes @@ -118,9 +108,17 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handle/post/:rkey/downvoted-by", server.WebGeneric) e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric) - bind := "localhost:8100" - log.Infof("starting server bind=%s", bind) - return e.Start(bind) + // Mailmodo + e.POST("/waitlist", func(c echo.Context) error { + email := strings.TrimSpace(c.FormValue("email")) + if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, email); err != nil { + return err + } + return c.JSON(http.StatusOK, map[string]bool{"success": true}) + }) + + log.Infof("starting server address=%s", httpAddress) + return e.Start(httpAddress) } func customHTTPErrorHandler(err error, c echo.Context) { @@ -132,18 +130,18 @@ func customHTTPErrorHandler(err error, c echo.Context) { data := pongo2.Context{ "statusCode": code, } - c.Render(code, "templates/error.html", data) + c.Render(code, "error.html", data) } // handler for endpoint that have no specific server-side handling func (srv *Server) WebGeneric(c echo.Context) error { data := pongo2.Context{} - return c.Render(http.StatusOK, "templates/base.html", data) + return c.Render(http.StatusOK, "base.html", data) } func (srv *Server) WebHome(c echo.Context) error { data := pongo2.Context{} - return c.Render(http.StatusOK, "templates/home.html", data) + return c.Render(http.StatusOK, "home.html", data) } func (srv *Server) WebPost(c echo.Context) error { @@ -152,7 +150,7 @@ func (srv *Server) WebPost(c echo.Context) error { rkey := c.Param("rkey") // sanity check argument if len(handle) > 4 && len(handle) < 128 && len(rkey) > 0 { - ctx := context.TODO() + ctx := c.Request().Context() // requires two fetches: first fetch profile (!) pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) if err != nil { @@ -172,7 +170,7 @@ func (srv *Server) WebPost(c echo.Context) error { } } - return c.Render(http.StatusOK, "templates/post.html", data) + return c.Render(http.StatusOK, "post.html", data) } func (srv *Server) WebProfile(c echo.Context) error { @@ -180,7 +178,7 @@ func (srv *Server) WebProfile(c echo.Context) error { handle := c.Param("handle") // sanity check argument if len(handle) > 4 && len(handle) < 128 { - ctx := context.TODO() + ctx := c.Request().Context() pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle) if err != nil { log.Warnf("failed to fetch handle: %s\t%v", handle, err) @@ -189,5 +187,5 @@ func (srv *Server) WebProfile(c echo.Context) error { } } - return c.Render(http.StatusOK, "templates/profile.html", data) + return c.Render(http.StatusOK, "profile.html", data) } |