From f29ac5e7deeb2f768a592ec22dd36a04b250e1f0 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 27 May 2026 17:41:33 +0300 Subject: [PATCH] Add /c/ handler to bskyweb --- bskyweb/cmd/bskyweb/main.go | 6 +++ bskyweb/cmd/bskyweb/server.go | 68 ++++++++++++++++++++++++++++--- bskyweb/templates/chatinvite.html | 25 ++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 bskyweb/templates/chatinvite.html diff --git a/bskyweb/cmd/bskyweb/main.go b/bskyweb/cmd/bskyweb/main.go index da8a30953d..c3ded18e7a 100644 --- a/bskyweb/cmd/bskyweb/main.go +++ b/bskyweb/cmd/bskyweb/main.go @@ -46,6 +46,12 @@ func run(args []string) { Required: false, EnvVars: []string{"OGCARD_HOST"}, }, + &cli.StringFlag{ + Name: "chat-host", + Usage: "scheme, hostname, and port of chat appview", + Required: false, + EnvVars: []string{"CHAT_HOST"}, + }, &cli.StringFlag{ Name: "http-address", Usage: "Specify the local IP/port to bind to", diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 44e79bd3fb..4a4ba2fde3 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -17,11 +17,13 @@ import ( "net/url" "os" "os/signal" + "regexp" "strings" "syscall" "time" appbsky "github.com/bluesky-social/indigo/api/bsky" + chat "github.com/bluesky-social/indigo/api/chat" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/util/cliutil" "github.com/bluesky-social/indigo/xrpc" @@ -37,10 +39,11 @@ import ( ) type Server struct { - echo *echo.Echo - httpd *http.Server - xrpcc *xrpc.Client - cfg *Config + echo *echo.Echo + httpd *http.Server + xrpcc *xrpc.Client + chatXrpcc *xrpc.Client + cfg *Config ipccClient http.Client @@ -54,6 +57,7 @@ type Config struct { debug bool httpAddress string appviewHost string + chatHost string ogcardHost string linkHost string ipccHost string @@ -64,6 +68,7 @@ func serve(cctx *cli.Context) error { debug := cctx.Bool("debug") httpAddress := cctx.String("http-address") appviewHost := cctx.String("appview-host") + chatHost := cctx.String("chat-host") ogcardHost := cctx.String("ogcard-host") linkHost := cctx.String("link-host") ipccHost := cctx.String("ipcc-host") @@ -83,6 +88,15 @@ func serve(cctx *cli.Context) error { Host: appviewHost, } + // optional client for the chat appview, used by /c/ for OG previews. + var chatXrpcc *xrpc.Client + if chatHost != "" { + chatXrpcc = &xrpc.Client{ + Client: cliutil.NewHttpClient(), + Host: chatHost, + } + } + // httpd var ( httpTimeout = 2 * time.Minute @@ -106,12 +120,14 @@ func serve(cctx *cli.Context) error { // server // server := &Server{ - echo: e, - xrpcc: xrpcc, + echo: e, + xrpcc: xrpcc, + chatXrpcc: chatXrpcc, cfg: &Config{ debug: debug, httpAddress: httpAddress, appviewHost: appviewHost, + chatHost: chatHost, ogcardHost: ogcardHost, linkHost: linkHost, ipccHost: ipccHost, @@ -349,6 +365,9 @@ func serve(cctx *cli.Context) error { e.GET("/starter-pack-short/:code", server.WebGeneric) e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack) + // chat invites + e.GET("/c/:code", server.WebChatInvite) + // bookmarks e.GET("/saved", server.WebGeneric) @@ -644,6 +663,43 @@ func (srv *Server) WebStarterPack(c echo.Context) error { return c.Render(http.StatusOK, "starterpack.html", data) } +// chatInviteCodeRe is a permissive sanity check on the join code so we don't +// proxy obviously-bad input to the chat appview. Codes are opaque per the +// lexicon, but in practice URL-safe and short. +var chatInviteCodeRe = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`) + +func (srv *Server) WebChatInvite(c echo.Context) error { + req := c.Request() + ctx := req.Context() + data := srv.NewTemplateContext() + data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) + + code := c.Param("code") + if !chatInviteCodeRe.MatchString(code) { + return c.Render(http.StatusOK, "chatinvite.html", data) + } + if srv.chatXrpcc == nil { + return c.Render(http.StatusOK, "chatinvite.html", data) + } + + out, err := chat.GroupGetJoinLinkPreviews(ctx, srv.chatXrpcc, []string{code}) + if err != nil { + log.Errorf("failed to fetch chat invite preview for code=%s: %v", code, err) + return c.Render(http.StatusOK, "chatinvite.html", data) + } + if len(out.JoinLinkPreviews) == 0 { + return c.Render(http.StatusOK, "chatinvite.html", data) + } + preview := out.JoinLinkPreviews[0] + + data["title"] = preview.Name + if srv.cfg.ogcardHost != "" { + // bskyogcard registers this route as /chat-invite/:code, not /c/:code. + data["imgThumbUrl"] = fmt.Sprintf("%s/chat-invite/%s", srv.cfg.ogcardHost, code) + } + return c.Render(http.StatusOK, "chatinvite.html", data) +} + func (srv *Server) WebProfile(c echo.Context) error { ctx := c.Request().Context() data := srv.NewTemplateContext() diff --git a/bskyweb/templates/chatinvite.html b/bskyweb/templates/chatinvite.html new file mode 100644 index 0000000000..63ae03ee39 --- /dev/null +++ b/bskyweb/templates/chatinvite.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block html_head_extra -%} + {%- if requestURI %} + + {% endif -%} + {%- if imgThumbUrl %} + + + {%- else -%} + + + {% endif -%} + + {%- if title %} + + + {%- else -%} + + + {% endif -%} + + + +{%- endblock %}