feat(bskyweb): build Apple pass.json for invite passes
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var themeBgRGB = map[string]string{
|
||||||
|
"dawn": "rgb(255, 109, 190)",
|
||||||
|
"day": "rgb(117, 175, 255)",
|
||||||
|
"dusk": "rgb(177, 90, 162)",
|
||||||
|
"night": "rgb(0, 21, 51)",
|
||||||
|
}
|
||||||
|
|
||||||
|
func CoerceTheme(s string) string {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "dawn", "day", "dusk", "night":
|
||||||
|
return strings.ToLower(s)
|
||||||
|
default:
|
||||||
|
return "day"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ThemeBackgroundRGB(theme string) string {
|
||||||
|
if v, ok := themeBgRGB[theme]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return themeBgRGB["day"]
|
||||||
|
}
|
||||||
|
|
||||||
|
type passField struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type storeCardFields struct {
|
||||||
|
PrimaryFields []passField `json:"primaryFields"`
|
||||||
|
SecondaryFields []passField `json:"secondaryFields"`
|
||||||
|
BackFields []passField `json:"backFields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type barcode struct {
|
||||||
|
Format string `json:"format"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
MessageEncoding string `json:"messageEncoding"`
|
||||||
|
AltText string `json:"altText"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pkPass struct {
|
||||||
|
FormatVersion int `json:"formatVersion"`
|
||||||
|
PassTypeIdentifier string `json:"passTypeIdentifier"`
|
||||||
|
SerialNumber string `json:"serialNumber"`
|
||||||
|
TeamIdentifier string `json:"teamIdentifier"`
|
||||||
|
OrganizationName string `json:"organizationName"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
LogoText string `json:"logoText"`
|
||||||
|
ForegroundColor string `json:"foregroundColor"`
|
||||||
|
LabelColor string `json:"labelColor"`
|
||||||
|
BackgroundColor string `json:"backgroundColor"`
|
||||||
|
StoreCard storeCardFields `json:"storeCard"`
|
||||||
|
Barcodes []barcode `json:"barcodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const passTypeIdentifier = "pass.app.bsky.invite"
|
||||||
|
|
||||||
|
func BuildPassJSON(did, handle, theme, teamID string) ([]byte, error) {
|
||||||
|
theme = CoerceTheme(theme)
|
||||||
|
profileURL := "https://bsky.app/profile/" + handle
|
||||||
|
atHandle := "@" + handle
|
||||||
|
p := pkPass{
|
||||||
|
FormatVersion: 1,
|
||||||
|
PassTypeIdentifier: passTypeIdentifier,
|
||||||
|
SerialNumber: did + "-" + theme + "-v1",
|
||||||
|
TeamIdentifier: teamID,
|
||||||
|
OrganizationName: "Bluesky",
|
||||||
|
Description: "Bluesky invite - " + atHandle,
|
||||||
|
LogoText: "Bluesky",
|
||||||
|
ForegroundColor: "rgb(255, 255, 255)",
|
||||||
|
LabelColor: "rgb(255, 255, 255)",
|
||||||
|
BackgroundColor: ThemeBackgroundRGB(theme),
|
||||||
|
StoreCard: storeCardFields{
|
||||||
|
PrimaryFields: []passField{{Key: "handle", Label: "Handle", Value: atHandle}},
|
||||||
|
SecondaryFields: []passField{{Key: "url", Label: "Profile", Value: "bsky.app/profile/" + handle}},
|
||||||
|
BackFields: []passField{
|
||||||
|
{Key: "about", Label: "About", Value: "Scan the QR to open this profile in any browser, or share the URL below."},
|
||||||
|
{Key: "url2", Label: "Profile URL", Value: profileURL},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Barcodes: []barcode{{
|
||||||
|
Format: "PKBarcodeFormatQR",
|
||||||
|
Message: profileURL,
|
||||||
|
MessageEncoding: "iso-8859-1",
|
||||||
|
AltText: atHandle,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
return json.MarshalIndent(p, "", " ")
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCoerceTheme(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
"dawn": "dawn", "day": "day", "dusk": "dusk", "night": "night",
|
||||||
|
"": "day",
|
||||||
|
"garbage": "day",
|
||||||
|
"DAY": "day", // case-insensitive
|
||||||
|
}
|
||||||
|
for in, want := range cases {
|
||||||
|
if got := CoerceTheme(in); got != want {
|
||||||
|
t.Errorf("CoerceTheme(%q) = %q, want %q", in, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThemeBackgroundRGB(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
"dawn": "rgb(255, 109, 190)",
|
||||||
|
"day": "rgb(117, 175, 255)",
|
||||||
|
"dusk": "rgb(177, 90, 162)",
|
||||||
|
"night": "rgb(0, 21, 51)",
|
||||||
|
}
|
||||||
|
for in, want := range cases {
|
||||||
|
if got := ThemeBackgroundRGB(in); got != want {
|
||||||
|
t.Errorf("ThemeBackgroundRGB(%q) = %q, want %q", in, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("build: %v", err)
|
||||||
|
}
|
||||||
|
golden := filepath.Join("testdata", "pass-"+theme+".golden.json")
|
||||||
|
if os.Getenv("UPDATE_GOLDEN") == "1" {
|
||||||
|
if err := os.WriteFile(golden, got, 0o644); err != nil {
|
||||||
|
t.Fatalf("write golden: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
want, err := os.ReadFile(golden)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read golden: %v", err)
|
||||||
|
}
|
||||||
|
var gotMap, wantMap map[string]any
|
||||||
|
_ = json.Unmarshal(got, &gotMap)
|
||||||
|
_ = json.Unmarshal(want, &wantMap)
|
||||||
|
if !mapsEqual(gotMap, wantMap) {
|
||||||
|
t.Errorf("pass.json drift; rerun with UPDATE_GOLDEN=1 to update.\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapsEqual(a, b map[string]any) bool {
|
||||||
|
ab, _ := json.Marshal(a)
|
||||||
|
bb, _ := json.Marshal(b)
|
||||||
|
return string(ab) == string(bb)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"passTypeIdentifier": "pass.app.bsky.invite",
|
||||||
|
"serialNumber": "did:plc:abc123-dawn-v1",
|
||||||
|
"teamIdentifier": "TEAMID00",
|
||||||
|
"organizationName": "Bluesky",
|
||||||
|
"description": "Bluesky invite - @alice.bsky.social",
|
||||||
|
"logoText": "Bluesky",
|
||||||
|
"foregroundColor": "rgb(255, 255, 255)",
|
||||||
|
"labelColor": "rgb(255, 255, 255)",
|
||||||
|
"backgroundColor": "rgb(255, 109, 190)",
|
||||||
|
"storeCard": {
|
||||||
|
"primaryFields": [
|
||||||
|
{
|
||||||
|
"key": "handle",
|
||||||
|
"label": "Handle",
|
||||||
|
"value": "@alice.bsky.social"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secondaryFields": [
|
||||||
|
{
|
||||||
|
"key": "url",
|
||||||
|
"label": "Profile",
|
||||||
|
"value": "bsky.app/profile/alice.bsky.social"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"backFields": [
|
||||||
|
{
|
||||||
|
"key": "about",
|
||||||
|
"label": "About",
|
||||||
|
"value": "Scan the QR to open this profile in any browser, or share the URL below."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "url2",
|
||||||
|
"label": "Profile URL",
|
||||||
|
"value": "https://bsky.app/profile/alice.bsky.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"barcodes": [
|
||||||
|
{
|
||||||
|
"format": "PKBarcodeFormatQR",
|
||||||
|
"message": "https://bsky.app/profile/alice.bsky.social",
|
||||||
|
"messageEncoding": "iso-8859-1",
|
||||||
|
"altText": "@alice.bsky.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"passTypeIdentifier": "pass.app.bsky.invite",
|
||||||
|
"serialNumber": "did:plc:abc123-day-v1",
|
||||||
|
"teamIdentifier": "TEAMID00",
|
||||||
|
"organizationName": "Bluesky",
|
||||||
|
"description": "Bluesky invite - @alice.bsky.social",
|
||||||
|
"logoText": "Bluesky",
|
||||||
|
"foregroundColor": "rgb(255, 255, 255)",
|
||||||
|
"labelColor": "rgb(255, 255, 255)",
|
||||||
|
"backgroundColor": "rgb(117, 175, 255)",
|
||||||
|
"storeCard": {
|
||||||
|
"primaryFields": [
|
||||||
|
{
|
||||||
|
"key": "handle",
|
||||||
|
"label": "Handle",
|
||||||
|
"value": "@alice.bsky.social"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secondaryFields": [
|
||||||
|
{
|
||||||
|
"key": "url",
|
||||||
|
"label": "Profile",
|
||||||
|
"value": "bsky.app/profile/alice.bsky.social"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"backFields": [
|
||||||
|
{
|
||||||
|
"key": "about",
|
||||||
|
"label": "About",
|
||||||
|
"value": "Scan the QR to open this profile in any browser, or share the URL below."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "url2",
|
||||||
|
"label": "Profile URL",
|
||||||
|
"value": "https://bsky.app/profile/alice.bsky.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"barcodes": [
|
||||||
|
{
|
||||||
|
"format": "PKBarcodeFormatQR",
|
||||||
|
"message": "https://bsky.app/profile/alice.bsky.social",
|
||||||
|
"messageEncoding": "iso-8859-1",
|
||||||
|
"altText": "@alice.bsky.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"passTypeIdentifier": "pass.app.bsky.invite",
|
||||||
|
"serialNumber": "did:plc:abc123-dusk-v1",
|
||||||
|
"teamIdentifier": "TEAMID00",
|
||||||
|
"organizationName": "Bluesky",
|
||||||
|
"description": "Bluesky invite - @alice.bsky.social",
|
||||||
|
"logoText": "Bluesky",
|
||||||
|
"foregroundColor": "rgb(255, 255, 255)",
|
||||||
|
"labelColor": "rgb(255, 255, 255)",
|
||||||
|
"backgroundColor": "rgb(177, 90, 162)",
|
||||||
|
"storeCard": {
|
||||||
|
"primaryFields": [
|
||||||
|
{
|
||||||
|
"key": "handle",
|
||||||
|
"label": "Handle",
|
||||||
|
"value": "@alice.bsky.social"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secondaryFields": [
|
||||||
|
{
|
||||||
|
"key": "url",
|
||||||
|
"label": "Profile",
|
||||||
|
"value": "bsky.app/profile/alice.bsky.social"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"backFields": [
|
||||||
|
{
|
||||||
|
"key": "about",
|
||||||
|
"label": "About",
|
||||||
|
"value": "Scan the QR to open this profile in any browser, or share the URL below."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "url2",
|
||||||
|
"label": "Profile URL",
|
||||||
|
"value": "https://bsky.app/profile/alice.bsky.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"barcodes": [
|
||||||
|
{
|
||||||
|
"format": "PKBarcodeFormatQR",
|
||||||
|
"message": "https://bsky.app/profile/alice.bsky.social",
|
||||||
|
"messageEncoding": "iso-8859-1",
|
||||||
|
"altText": "@alice.bsky.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"passTypeIdentifier": "pass.app.bsky.invite",
|
||||||
|
"serialNumber": "did:plc:abc123-night-v1",
|
||||||
|
"teamIdentifier": "TEAMID00",
|
||||||
|
"organizationName": "Bluesky",
|
||||||
|
"description": "Bluesky invite - @alice.bsky.social",
|
||||||
|
"logoText": "Bluesky",
|
||||||
|
"foregroundColor": "rgb(255, 255, 255)",
|
||||||
|
"labelColor": "rgb(255, 255, 255)",
|
||||||
|
"backgroundColor": "rgb(0, 21, 51)",
|
||||||
|
"storeCard": {
|
||||||
|
"primaryFields": [
|
||||||
|
{
|
||||||
|
"key": "handle",
|
||||||
|
"label": "Handle",
|
||||||
|
"value": "@alice.bsky.social"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secondaryFields": [
|
||||||
|
{
|
||||||
|
"key": "url",
|
||||||
|
"label": "Profile",
|
||||||
|
"value": "bsky.app/profile/alice.bsky.social"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"backFields": [
|
||||||
|
{
|
||||||
|
"key": "about",
|
||||||
|
"label": "About",
|
||||||
|
"value": "Scan the QR to open this profile in any browser, or share the URL below."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "url2",
|
||||||
|
"label": "Profile URL",
|
||||||
|
"value": "https://bsky.app/profile/alice.bsky.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"barcodes": [
|
||||||
|
{
|
||||||
|
"format": "PKBarcodeFormatQR",
|
||||||
|
"message": "https://bsky.app/profile/alice.bsky.social",
|
||||||
|
"messageEncoding": "iso-8859-1",
|
||||||
|
"altText": "@alice.bsky.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user