about summary refs log tree commit diff
path: root/bskyweb/cmd
diff options
context:
space:
mode:
authorJake Gold <52801504+Jacob2161@users.noreply.github.com>2023-03-20 14:41:15 -0700
committerGitHub <noreply@github.com>2023-03-20 14:41:15 -0700
commit67e4882bb372bc45d178e3eacc409cdbf60f1344 (patch)
tree13e3c015e91b5009597d9b610a9c6a40764cbfcc /bskyweb/cmd
parentd8f4475696094382e2196ce259af358b65c849fb (diff)
downloadvoidsky-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.go68
-rw-r--r--bskyweb/cmd/bskyweb/main.go68
-rw-r--r--bskyweb/cmd/bskyweb/renderer.go82
-rw-r--r--bskyweb/cmd/bskyweb/server.go108
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)
 }