From df26fbbbb03dd13c826e263220644a2ccabb166a Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Thu, 25 Jun 2026 19:17:12 -0400 Subject: [PATCH] feat(bskyweb): fetch displayName, add headerField USERNAME, Member Name secondary --- bskyweb/cmd/bskyweb/invitepass.go | 17 ++++++++++------- bskyweb/cmd/bskyweb/passdata.go | 14 +++++++++++--- bskyweb/cmd/bskyweb/passdata_test.go | 2 +- .../cmd/bskyweb/testdata/pass-dawn.golden.json | 13 ++++++++++--- .../cmd/bskyweb/testdata/pass-day.golden.json | 13 ++++++++++--- .../cmd/bskyweb/testdata/pass-dusk.golden.json | 13 ++++++++++--- .../cmd/bskyweb/testdata/pass-night.golden.json | 13 ++++++++++--- 7 files changed, 62 insertions(+), 23 deletions(-) diff --git a/bskyweb/cmd/bskyweb/invitepass.go b/bskyweb/cmd/bskyweb/invitepass.go index 371208a78e..63bafb0009 100644 --- a/bskyweb/cmd/bskyweb/invitepass.go +++ b/bskyweb/cmd/bskyweb/invitepass.go @@ -112,12 +112,12 @@ func (srv *Server) WebInvitePassPkpass(c echo.Context) error { return c.JSON(http.StatusServiceUnavailable, echo.Map{"error": "SignerDisabled"}) } - handle, avatarBytes, err := srv.fetchProfile(c.Request().Context(), did) + handle, displayName, avatarBytes, err := srv.fetchProfile(c.Request().Context(), did) if err != nil { return c.JSON(http.StatusBadGateway, echo.Map{"error": "ProfileFetchFailed"}) } - passJSON, err := BuildPassJSON(did, handle, theme, srv.cfg.InvitePass.TeamID) + passJSON, err := BuildPassJSON(did, handle, displayName, theme, srv.cfg.InvitePass.TeamID) if err != nil { return c.JSON(http.StatusInternalServerError, echo.Map{"error": "PassBuildFailed"}) } @@ -165,7 +165,7 @@ func (srv *Server) WebInviteWalletHero(c echo.Context) error { if did == "" { return c.NoContent(http.StatusBadRequest) } - handle, avatarBytes, err := srv.fetchProfile(c.Request().Context(), did) + handle, _, avatarBytes, err := srv.fetchProfile(c.Request().Context(), did) if err != nil { return c.JSON(http.StatusBadGateway, echo.Map{"error": "ProfileFetchFailed"}) } @@ -261,16 +261,19 @@ func decodeImage(data []byte) (image.Image, error) { return img, err } -func (srv *Server) fetchProfile(ctx context.Context, did string) (handle string, avatarBytes []byte, err error) { +func (srv *Server) fetchProfile(ctx context.Context, did string) (handle, displayName string, avatarBytes []byte, err error) { pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, did) if err != nil { - return "", nil, err + return "", "", nil, err } handle = pv.Handle + if pv.DisplayName != nil { + displayName = *pv.DisplayName + } if pv.Avatar != nil { // SSRF defense: only fetch https:// URLs if !strings.HasPrefix(*pv.Avatar, "https://") { - return handle, nil, nil + return handle, displayName, nil, nil } req, _ := http.NewRequestWithContext(ctx, http.MethodGet, *pv.Avatar, nil) client := &http.Client{Timeout: 5 * time.Second} @@ -282,5 +285,5 @@ func (srv *Server) fetchProfile(ctx context.Context, did string) (handle string, } } } - return handle, avatarBytes, nil + return handle, displayName, avatarBytes, nil } diff --git a/bskyweb/cmd/bskyweb/passdata.go b/bskyweb/cmd/bskyweb/passdata.go index 5df50ed52b..c38b4b8a9d 100644 --- a/bskyweb/cmd/bskyweb/passdata.go +++ b/bskyweb/cmd/bskyweb/passdata.go @@ -35,6 +35,7 @@ type passField struct { } type storeCardFields struct { + HeaderFields []passField `json:"headerFields"` PrimaryFields []passField `json:"primaryFields"` SecondaryFields []passField `json:"secondaryFields"` BackFields []passField `json:"backFields"` @@ -64,10 +65,14 @@ type pkPass struct { const passTypeIdentifier = "pass.xyz.blueskyweb.app" -func BuildPassJSON(did, handle, theme, teamID string) ([]byte, error) { +func BuildPassJSON(did, handle, displayName, theme, teamID string) ([]byte, error) { theme = CoerceTheme(theme) profileURL := "https://bsky.app/profile/" + handle atHandle := "@" + handle + memberName := displayName + if memberName == "" { + memberName = atHandle + } p := pkPass{ FormatVersion: 1, PassTypeIdentifier: passTypeIdentifier, @@ -80,9 +85,12 @@ func BuildPassJSON(did, handle, theme, teamID string) ([]byte, error) { LabelColor: "rgb(255, 255, 255)", BackgroundColor: ThemeBackgroundRGB(theme), StoreCard: storeCardFields{ - PrimaryFields: []passField{}, + HeaderFields: []passField{ + {Key: "username", Label: "USERNAME", Value: atHandle}, + }, + PrimaryFields: []passField{}, SecondaryFields: []passField{ - {Key: "handle", Label: "", Value: atHandle}, + {Key: "name", Label: "Member Name", Value: memberName}, }, BackFields: []passField{ {Key: "about", Label: "About", Value: "Scan the QR code to view this Bluesky profile."}, diff --git a/bskyweb/cmd/bskyweb/passdata_test.go b/bskyweb/cmd/bskyweb/passdata_test.go index f9771ccd0f..ea7b886994 100644 --- a/bskyweb/cmd/bskyweb/passdata_test.go +++ b/bskyweb/cmd/bskyweb/passdata_test.go @@ -38,7 +38,7 @@ func TestThemeBackgroundRGB(t *testing.T) { func TestBuildPassJSON_Golden(t *testing.T) { for _, theme := range []string{"dawn", "day", "dusk", "night"} { t.Run(theme, func(t *testing.T) { - got, err := BuildPassJSON("did:plc:abc123", "alice.bsky.social", theme, "TEAMID00") + got, err := BuildPassJSON("did:plc:abc123", "alice.bsky.social", "Alice", theme, "TEAMID00") if err != nil { t.Fatalf("build: %v", err) } diff --git a/bskyweb/cmd/bskyweb/testdata/pass-dawn.golden.json b/bskyweb/cmd/bskyweb/testdata/pass-dawn.golden.json index ca3d482839..2be6908123 100644 --- a/bskyweb/cmd/bskyweb/testdata/pass-dawn.golden.json +++ b/bskyweb/cmd/bskyweb/testdata/pass-dawn.golden.json @@ -10,12 +10,19 @@ "labelColor": "rgb(255, 255, 255)", "backgroundColor": "rgb(255, 109, 190)", "storeCard": { + "headerFields": [ + { + "key": "username", + "label": "USERNAME", + "value": "@alice.bsky.social" + } + ], "primaryFields": [], "secondaryFields": [ { - "key": "handle", - "label": "", - "value": "@alice.bsky.social" + "key": "name", + "label": "Member Name", + "value": "Alice" } ], "backFields": [ diff --git a/bskyweb/cmd/bskyweb/testdata/pass-day.golden.json b/bskyweb/cmd/bskyweb/testdata/pass-day.golden.json index 8a388bc65d..be93001016 100644 --- a/bskyweb/cmd/bskyweb/testdata/pass-day.golden.json +++ b/bskyweb/cmd/bskyweb/testdata/pass-day.golden.json @@ -10,12 +10,19 @@ "labelColor": "rgb(255, 255, 255)", "backgroundColor": "rgb(117, 175, 255)", "storeCard": { + "headerFields": [ + { + "key": "username", + "label": "USERNAME", + "value": "@alice.bsky.social" + } + ], "primaryFields": [], "secondaryFields": [ { - "key": "handle", - "label": "", - "value": "@alice.bsky.social" + "key": "name", + "label": "Member Name", + "value": "Alice" } ], "backFields": [ diff --git a/bskyweb/cmd/bskyweb/testdata/pass-dusk.golden.json b/bskyweb/cmd/bskyweb/testdata/pass-dusk.golden.json index 14eff5eb7c..a5aa6e0726 100644 --- a/bskyweb/cmd/bskyweb/testdata/pass-dusk.golden.json +++ b/bskyweb/cmd/bskyweb/testdata/pass-dusk.golden.json @@ -10,12 +10,19 @@ "labelColor": "rgb(255, 255, 255)", "backgroundColor": "rgb(177, 90, 162)", "storeCard": { + "headerFields": [ + { + "key": "username", + "label": "USERNAME", + "value": "@alice.bsky.social" + } + ], "primaryFields": [], "secondaryFields": [ { - "key": "handle", - "label": "", - "value": "@alice.bsky.social" + "key": "name", + "label": "Member Name", + "value": "Alice" } ], "backFields": [ diff --git a/bskyweb/cmd/bskyweb/testdata/pass-night.golden.json b/bskyweb/cmd/bskyweb/testdata/pass-night.golden.json index 8bcfd7a318..38b7219aef 100644 --- a/bskyweb/cmd/bskyweb/testdata/pass-night.golden.json +++ b/bskyweb/cmd/bskyweb/testdata/pass-night.golden.json @@ -10,12 +10,19 @@ "labelColor": "rgb(255, 255, 255)", "backgroundColor": "rgb(0, 21, 51)", "storeCard": { + "headerFields": [ + { + "key": "username", + "label": "USERNAME", + "value": "@alice.bsky.social" + } + ], "primaryFields": [], "secondaryFields": [ { - "key": "handle", - "label": "", - "value": "@alice.bsky.social" + "key": "name", + "label": "Member Name", + "value": "Alice" } ], "backFields": [