feat(bskyweb): fetch displayName, add headerField USERNAME, Member Name secondary

This commit is contained in:
vineyardbovines
2026-06-25 19:17:12 -04:00
parent 8e51edec15
commit df26fbbbb0
7 changed files with 62 additions and 23 deletions
+10 -7
View File
@@ -112,12 +112,12 @@ func (srv *Server) WebInvitePassPkpass(c echo.Context) error {
return c.JSON(http.StatusServiceUnavailable, echo.Map{"error": "SignerDisabled"}) 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 { if err != nil {
return c.JSON(http.StatusBadGateway, echo.Map{"error": "ProfileFetchFailed"}) 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 { if err != nil {
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "PassBuildFailed"}) return c.JSON(http.StatusInternalServerError, echo.Map{"error": "PassBuildFailed"})
} }
@@ -165,7 +165,7 @@ func (srv *Server) WebInviteWalletHero(c echo.Context) error {
if did == "" { if did == "" {
return c.NoContent(http.StatusBadRequest) 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 { if err != nil {
return c.JSON(http.StatusBadGateway, echo.Map{"error": "ProfileFetchFailed"}) return c.JSON(http.StatusBadGateway, echo.Map{"error": "ProfileFetchFailed"})
} }
@@ -261,16 +261,19 @@ func decodeImage(data []byte) (image.Image, error) {
return img, err 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) pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, did)
if err != nil { if err != nil {
return "", nil, err return "", "", nil, err
} }
handle = pv.Handle handle = pv.Handle
if pv.DisplayName != nil {
displayName = *pv.DisplayName
}
if pv.Avatar != nil { if pv.Avatar != nil {
// SSRF defense: only fetch https:// URLs // SSRF defense: only fetch https:// URLs
if !strings.HasPrefix(*pv.Avatar, "https://") { if !strings.HasPrefix(*pv.Avatar, "https://") {
return handle, nil, nil return handle, displayName, nil, nil
} }
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, *pv.Avatar, nil) req, _ := http.NewRequestWithContext(ctx, http.MethodGet, *pv.Avatar, nil)
client := &http.Client{Timeout: 5 * time.Second} 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
} }
+10 -2
View File
@@ -35,6 +35,7 @@ type passField struct {
} }
type storeCardFields struct { type storeCardFields struct {
HeaderFields []passField `json:"headerFields"`
PrimaryFields []passField `json:"primaryFields"` PrimaryFields []passField `json:"primaryFields"`
SecondaryFields []passField `json:"secondaryFields"` SecondaryFields []passField `json:"secondaryFields"`
BackFields []passField `json:"backFields"` BackFields []passField `json:"backFields"`
@@ -64,10 +65,14 @@ type pkPass struct {
const passTypeIdentifier = "pass.xyz.blueskyweb.app" 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) theme = CoerceTheme(theme)
profileURL := "https://bsky.app/profile/" + handle profileURL := "https://bsky.app/profile/" + handle
atHandle := "@" + handle atHandle := "@" + handle
memberName := displayName
if memberName == "" {
memberName = atHandle
}
p := pkPass{ p := pkPass{
FormatVersion: 1, FormatVersion: 1,
PassTypeIdentifier: passTypeIdentifier, PassTypeIdentifier: passTypeIdentifier,
@@ -80,9 +85,12 @@ func BuildPassJSON(did, handle, theme, teamID string) ([]byte, error) {
LabelColor: "rgb(255, 255, 255)", LabelColor: "rgb(255, 255, 255)",
BackgroundColor: ThemeBackgroundRGB(theme), BackgroundColor: ThemeBackgroundRGB(theme),
StoreCard: storeCardFields{ StoreCard: storeCardFields{
HeaderFields: []passField{
{Key: "username", Label: "USERNAME", Value: atHandle},
},
PrimaryFields: []passField{}, PrimaryFields: []passField{},
SecondaryFields: []passField{ SecondaryFields: []passField{
{Key: "handle", Label: "", Value: atHandle}, {Key: "name", Label: "Member Name", Value: memberName},
}, },
BackFields: []passField{ BackFields: []passField{
{Key: "about", Label: "About", Value: "Scan the QR code to view this Bluesky profile."}, {Key: "about", Label: "About", Value: "Scan the QR code to view this Bluesky profile."},
+1 -1
View File
@@ -38,7 +38,7 @@ func TestThemeBackgroundRGB(t *testing.T) {
func TestBuildPassJSON_Golden(t *testing.T) { func TestBuildPassJSON_Golden(t *testing.T) {
for _, theme := range []string{"dawn", "day", "dusk", "night"} { for _, theme := range []string{"dawn", "day", "dusk", "night"} {
t.Run(theme, func(t *testing.T) { 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 { if err != nil {
t.Fatalf("build: %v", err) t.Fatalf("build: %v", err)
} }
+10 -3
View File
@@ -10,12 +10,19 @@
"labelColor": "rgb(255, 255, 255)", "labelColor": "rgb(255, 255, 255)",
"backgroundColor": "rgb(255, 109, 190)", "backgroundColor": "rgb(255, 109, 190)",
"storeCard": { "storeCard": {
"headerFields": [
{
"key": "username",
"label": "USERNAME",
"value": "@alice.bsky.social"
}
],
"primaryFields": [], "primaryFields": [],
"secondaryFields": [ "secondaryFields": [
{ {
"key": "handle", "key": "name",
"label": "", "label": "Member Name",
"value": "@alice.bsky.social" "value": "Alice"
} }
], ],
"backFields": [ "backFields": [
+10 -3
View File
@@ -10,12 +10,19 @@
"labelColor": "rgb(255, 255, 255)", "labelColor": "rgb(255, 255, 255)",
"backgroundColor": "rgb(117, 175, 255)", "backgroundColor": "rgb(117, 175, 255)",
"storeCard": { "storeCard": {
"headerFields": [
{
"key": "username",
"label": "USERNAME",
"value": "@alice.bsky.social"
}
],
"primaryFields": [], "primaryFields": [],
"secondaryFields": [ "secondaryFields": [
{ {
"key": "handle", "key": "name",
"label": "", "label": "Member Name",
"value": "@alice.bsky.social" "value": "Alice"
} }
], ],
"backFields": [ "backFields": [
+10 -3
View File
@@ -10,12 +10,19 @@
"labelColor": "rgb(255, 255, 255)", "labelColor": "rgb(255, 255, 255)",
"backgroundColor": "rgb(177, 90, 162)", "backgroundColor": "rgb(177, 90, 162)",
"storeCard": { "storeCard": {
"headerFields": [
{
"key": "username",
"label": "USERNAME",
"value": "@alice.bsky.social"
}
],
"primaryFields": [], "primaryFields": [],
"secondaryFields": [ "secondaryFields": [
{ {
"key": "handle", "key": "name",
"label": "", "label": "Member Name",
"value": "@alice.bsky.social" "value": "Alice"
} }
], ],
"backFields": [ "backFields": [
+10 -3
View File
@@ -10,12 +10,19 @@
"labelColor": "rgb(255, 255, 255)", "labelColor": "rgb(255, 255, 255)",
"backgroundColor": "rgb(0, 21, 51)", "backgroundColor": "rgb(0, 21, 51)",
"storeCard": { "storeCard": {
"headerFields": [
{
"key": "username",
"label": "USERNAME",
"value": "@alice.bsky.social"
}
],
"primaryFields": [], "primaryFields": [],
"secondaryFields": [ "secondaryFields": [
{ {
"key": "handle", "key": "name",
"label": "", "label": "Member Name",
"value": "@alice.bsky.social" "value": "Alice"
} }
], ],
"backFields": [ "backFields": [