···5151 [streamer] Looks like <1>@{ $handle } is offline</1>, but they recommend checking out:
5252 *[default] Looks like <1>@{ $handle } is offline</1>, but we recommend checking out:
5353}
5454-user-offline-no-recommendations =
5454+user-offline-no-recommendations =
5555 Looks like <1>@{ $handle } is offline</1> right now.
5656 Check back later.
5757streaming-title = streaming { $title }
···6060 [1] 1 viewer
6161 *[other] { $count } viewers
6262}
6363+6464+## PDS Host Selector
6565+pds-selector-title = New to the Atmosphere?
6666+pds-selector-description = You'll need to select a PDS (Personal Data Server) to access apps on the Atmosphere, such as Bluesky, Tangled, and Spark.
6767+pds-selector-custom-label = Another PDS
6868+pds-selector-custom-description = Enter your own PDS host URL
6969+pds-selector-custom-url-label = Custom PDS URL
7070+pds-selector-custom-url-placeholder = https://pds.example.com
7171+pds-selector-learn-more = Learn more about self-hosting
7272+pds-selector-info = Each host has their own policies and reliability standards. Your ATProto data lives on the host you choose and you can migrate later. Note: Streamplace has its own moderation rules - you can be banned from Streamplace regardless of which host you choose.
7373+pds-selector-read-policies = Read { $label }'s <tosLink>Terms of Service</tosLink> and <privacyLink>Privacy Policy</privacyLink> before continuing.
7474+pds-selector-handle-policy-checkbox = I have read and agree to the <policyLink>handle policy</policyLink>
···55import * as React from "react";
66import { Platform, TextInput, type TextInputProps } from "react-native";
77import { bg, borders, flex, p, text } from "../../lib/theme/atoms";
88+import { useTheme } from "../../ui";
89910const Textarea = React.forwardRef<TextInput, TextInputProps>(
1011 ({ style, multiline = true, numberOfLines = 4, ...props }, ref) => {
1212+ let th = useTheme();
1113 // Detect if we're inside a bottom sheet
1214 let isInBottomSheet = false;
1315 try {
···3840 { borderRadius: 10 },
3941 style,
4042 ]}
4343+ autoComplete={props.autoComplete || "off"}
4444+ textContentType={props.textContentType || "none"}
4145 multiline={multiline}
4246 numberOfLines={numberOfLines}
4347 textAlignVertical="top"
4848+ placeholderTextColor={th.theme.colors.textMuted}
4449 {...props}
4550 />
4651 );
+1
js/components/src/hooks/index.ts
···11// barrel file :)
22+export * from "./useAQState";
23export * from "./useAvatars";
34export * from "./useCameraToggle";
45export * from "./useDocumentTitle";
···4141export * from "./components/stream-notification";
4242export * from "./lib/stream-notifications";
43434444+export * from "./utils/did";
4445export * from "./utils/format-handle";
45464647export { DanmuOverlay } from "./components/danmu/danmu-overlay";
···8080 chatProfile: (message as any).chatProfile,
8181 replyTo: (message as any).replyTo,
8282 deleted: message.deleted,
8383+ badges: message.badges,
8384 };
8485 state = reduceChat(state, [hydrated], [], []);
8586 } else if (PlaceStreamSegment.isRecord(message)) {
+3
js/components/src/player-store/player-state.tsx
···6363 ingestAutoStart?: boolean;
6464 setIngestAutoStart?: (autoStart: boolean) => void;
65656666+ /** stop ingest process, again with a slight delay to allow UI to update */
6767+ stopIngest: () => void;
6868+6669 /** Timestamp (number) when ingest started, or null if not started */
6770 ingestStarted: number | null;
6871
···11+---
22+title: badges system
33+description: user badges for chat messages
44+---
55+66+## Overview
77+88+Badges appear next to usernames in chat messages. they're small icons that indicate status (streamer, mod, vip, etc.). There will be max 3 badges shown at once. One of the badges is server-based (e.g. streamer, mod, node staff badge), but the other two can be selected from a pool of cosmetic badges (such as subscription badges, event badges et al.). These cosmetic badges are cryptographically signed by the issuing party, and all the user needs to do is apply them to their chat profile. Note that certain badges may appear/disappear based on the current streamer's chat tktk.
99+1010+## Lexicon schemas
1111+1212+We have three relevant lexicons.
1313+1414+1. **`place.stream.badge.defs`** - badge definitions and view model
1515+1616+ - defines known badge types: `mod`, `streamer`, `vip`
1717+ - `badgeView` object: `{badgeType, issuer, recipient, signature?}`
1818+1919+2. **`place.stream.badge.issuance`** - record of badge grant
2020+2121+ - stored as atproto record (key: tid)
2222+ - issued by streamer or other authorized entity
2323+ - example: streamer issues vip badge to a user
2424+2525+3. **`place.stream.badge.display`** - user's badge selection
2626+ - user-controlled record defining which badges to show
2727+ - array of up to 3 `badgeSelection` objects
2828+ - first slot server-controlled (mod/streamer/staff), second slot is streamer-specific (vip, subscription), third slot is user-set (event, staff2, node subscription, etc.)
2929+3030+:::note
3131+This may get changed to be in the user's chat profile? Maybe we could have a "main" chat profile and a streamer-specific profile?
3232+:::
3333+3434+## TODO
3535+3636+- [ ] implement cryptographic signatures for badge issuance
3737+- [ ] implement badge issuance ui (streamer grants vip badges)
3838+- [ ] implement badge selection ui (users choose which badges to display)
3939+- [ ] add more badge types (subscriber, founder, staff, etc)
···11+---
22+title: place.stream.badge.defs
33+description: Reference for the place.stream.badge.defs lexicon
44+---
55+66+**Lexicon Version:** 1
77+88+## Definitions
99+1010+<a name="badgeview"></a>
1111+1212+### `badgeView`
1313+1414+**Type:** `object`
1515+1616+View of a badge record, with fields resolved for display. If the DID in issuer is not the current streamplace node, the signature field shall be required.
1717+1818+**Properties:**
1919+2020+| Name | Type | Req'd | Description | Constraints |
2121+| ----------- | -------- | ----- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
2222+| `badgeType` | `string` | โ | | Known Values: `place.stream.badge.defs#mod`, `place.stream.badge.defs#streamer` |
2323+| `issuer` | `string` | โ | DID of the badge issuer. | Format: `did` |
2424+| `recipient` | `string` | โ | DID of the badge recipient. | Format: `did` |
2525+| `signature` | `string` | โ | TODO: Cryptographic signature of the badge (of a place.stream.key). | |
2626+2727+---
2828+2929+<a name="mod"></a>
3030+3131+### `mod`
3232+3333+**Type:** `token`
3434+3535+This user is a moderator. Displayed with a sword icon.
3636+3737+---
3838+3939+<a name="streamer"></a>
4040+4141+### `streamer`
4242+4343+**Type:** `token`
4444+4545+This user is the streamer. Displayed with a star icon.
4646+4747+---
4848+4949+<a name="vip"></a>
5050+5151+### `vip`
5252+5353+**Type:** `token`
5454+5555+This user is a very important person.
5656+5757+---
5858+5959+## Lexicon Source
6060+6161+```json
6262+{
6363+ "lexicon": 1,
6464+ "id": "place.stream.badge.defs",
6565+ "defs": {
6666+ "badgeView": {
6767+ "type": "object",
6868+ "required": ["badgeType", "issuer", "recipient"],
6969+ "description": "View of a badge record, with fields resolved for display. If the DID in issuer is not the current streamplace node, the signature field shall be required.",
7070+ "properties": {
7171+ "badgeType": {
7272+ "type": "string",
7373+ "knownValues": [
7474+ "place.stream.badge.defs#mod",
7575+ "place.stream.badge.defs#streamer"
7676+ ]
7777+ },
7878+ "issuer": {
7979+ "type": "string",
8080+ "format": "did",
8181+ "description": "DID of the badge issuer."
8282+ },
8383+ "recipient": {
8484+ "type": "string",
8585+ "format": "did",
8686+ "description": "DID of the badge recipient."
8787+ },
8888+ "signature": {
8989+ "type": "string",
9090+ "description": "TODO: Cryptographic signature of the badge (of a place.stream.key)."
9191+ }
9292+ }
9393+ },
9494+ "mod": {
9595+ "type": "token",
9696+ "description": "This user is a moderator. Displayed with a sword icon."
9797+ },
9898+ "streamer": {
9999+ "type": "token",
100100+ "description": "This user is the streamer. Displayed with a star icon."
101101+ },
102102+ "vip": {
103103+ "type": "token",
104104+ "description": "This user is a very important person."
105105+ }
106106+ }
107107+}
108108+```
···11+---
22+title: place.stream.badge.display
33+description: Reference for the place.stream.badge.display lexicon
44+---
55+66+**Lexicon Version:** 1
77+88+## Definitions
99+1010+<a name="main"></a>
1111+1212+### `main`
1313+1414+**Type:** `record`
1515+1616+Record issuing a badge to a user.
1717+1818+**Record Properties:**
1919+2020+| Name | Type | Req'd | Description | Constraints |
2121+| -------- | --------------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------- | ------------ |
2222+| `badges` | Array of [`#badgeSelection`](#badgeselection) | โ | Up to 3 badge tokens to display with the message. First badge is server-controlled, remaining badges are user-settable. | Max Items: 3 |
2323+2424+---
2525+2626+<a name="badgeselection"></a>
2727+2828+### `badgeSelection`
2929+3030+**Type:** `object`
3131+3232+A badge selected for display. May be a full badgeView from the server, or a token representing a badge type that the client can look up for display info.
3333+3434+**Properties:**
3535+3636+| Name | Type | Req'd | Description | Constraints |
3737+| ----------- | -------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
3838+| `badgeType` | `string` | โ | | Known Values: `place.stream.badge.defs#mod`, `place.stream.badge.defs#vip` |
3939+| `issuance` | `string` | โ | URI of the badge issuance record (place.stream.badge.issuance) that represents this badge. Required if badgeType is not recognized. | Format: `at-uri` |
4040+4141+---
4242+4343+## Lexicon Source
4444+4545+```json
4646+{
4747+ "lexicon": 1,
4848+ "id": "place.stream.badge.display",
4949+ "defs": {
5050+ "main": {
5151+ "type": "record",
5252+ "description": "Record issuing a badge to a user.",
5353+ "record": {
5454+ "type": "object",
5555+ "required": ["badges"],
5656+ "properties": {
5757+ "badges": {
5858+ "type": "array",
5959+ "description": "Up to 3 badge tokens to display with the message. First badge is server-controlled, remaining badges are user-settable.",
6060+ "maxLength": 3,
6161+ "items": {
6262+ "type": "ref",
6363+ "ref": "#badgeSelection"
6464+ }
6565+ }
6666+ }
6767+ }
6868+ },
6969+ "badgeSelection": {
7070+ "type": "object",
7171+ "description": "A badge selected for display. May be a full badgeView from the server, or a token representing a badge type that the client can look up for display info.",
7272+ "required": ["badgeType"],
7373+ "properties": {
7474+ "badgeType": {
7575+ "type": "string",
7676+ "knownValues": [
7777+ "place.stream.badge.defs#mod",
7878+ "place.stream.badge.defs#vip"
7979+ ]
8080+ },
8181+ "issuance": {
8282+ "type": "string",
8383+ "format": "at-uri",
8484+ "description": "URI of the badge issuance record (place.stream.badge.issuance) that represents this badge. Required if badgeType is not recognized."
8585+ }
8686+ }
8787+ }
8888+ }
8989+}
9090+```
···11+{
22+ "lexicon": 1,
33+ "id": "com.atproto.sync.getRepo",
44+ "defs": {
55+ "main": {
66+ "type": "query",
77+ "description": "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.",
88+ "parameters": {
99+ "type": "params",
1010+ "required": ["did"],
1111+ "properties": {
1212+ "did": {
1313+ "type": "string",
1414+ "format": "did",
1515+ "description": "The DID of the repo."
1616+ },
1717+ "since": {
1818+ "type": "string",
1919+ "format": "tid",
2020+ "description": "The revision ('rev') of the repo to create a diff from."
2121+ }
2222+ }
2323+ },
2424+ "output": {
2525+ "encoding": "application/vnd.ipld.car"
2626+ },
2727+ "errors": [
2828+ { "name": "RepoNotFound" },
2929+ { "name": "RepoTakendown" },
3030+ { "name": "RepoSuspended" },
3131+ { "name": "RepoDeactivated" }
3232+ ]
3333+ }
3434+ }
3535+}
+46
lexicons/place/stream/badge/defs.json
···11+{
22+ "lexicon": 1,
33+ "id": "place.stream.badge.defs",
44+ "defs": {
55+ "badgeView": {
66+ "type": "object",
77+ "required": ["badgeType", "issuer", "recipient"],
88+ "description": "View of a badge record, with fields resolved for display. If the DID in issuer is not the current streamplace node, the signature field shall be required.",
99+ "properties": {
1010+ "badgeType": {
1111+ "type": "string",
1212+ "knownValues": [
1313+ "place.stream.badge.defs#mod",
1414+ "place.stream.badge.defs#streamer"
1515+ ]
1616+ },
1717+ "issuer": {
1818+ "type": "string",
1919+ "format": "did",
2020+ "description": "DID of the badge issuer."
2121+ },
2222+ "recipient": {
2323+ "type": "string",
2424+ "format": "did",
2525+ "description": "DID of the badge recipient."
2626+ },
2727+ "signature": {
2828+ "type": "string",
2929+ "description": "TODO: Cryptographic signature of the badge (of a place.stream.key)."
3030+ }
3131+ }
3232+ },
3333+ "mod": {
3434+ "type": "token",
3535+ "description": "This user is a moderator. Displayed with a sword icon."
3636+ },
3737+ "streamer": {
3838+ "type": "token",
3939+ "description": "This user is the streamer. Displayed with a star icon."
4040+ },
4141+ "vip": {
4242+ "type": "token",
4343+ "description": "This user is a very important person."
4444+ }
4545+ }
4646+}
+44
lexicons/place/stream/badge/display.json
···11+{
22+ "lexicon": 1,
33+ "id": "place.stream.badge.display",
44+ "defs": {
55+ "main": {
66+ "type": "record",
77+ "description": "Record issuing a badge to a user.",
88+ "record": {
99+ "type": "object",
1010+ "required": ["badges"],
1111+ "properties": {
1212+ "badges": {
1313+ "type": "array",
1414+ "description": "Up to 3 badge tokens to display with the message. First badge is server-controlled, remaining badges are user-settable.",
1515+ "maxLength": 3,
1616+ "items": {
1717+ "type": "ref",
1818+ "ref": "#badgeSelection"
1919+ }
2020+ }
2121+ }
2222+ }
2323+ },
2424+ "badgeSelection": {
2525+ "type": "object",
2626+ "description": "A badge selected for display. May be a full badgeView from the server, or a token representing a badge type that the client can look up for display info.",
2727+ "required": ["badgeType"],
2828+ "properties": {
2929+ "badgeType": {
3030+ "type": "string",
3131+ "knownValues": [
3232+ "place.stream.badge.defs#mod",
3333+ "place.stream.badge.defs#vip"
3434+ ]
3535+ },
3636+ "issuance": {
3737+ "type": "string",
3838+ "format": "at-uri",
3939+ "description": "URI of the badge issuance record (place.stream.badge.issuance) that represents this badge. Required if badgeType is not recognized."
4040+ }
4141+ }
4242+ }
4343+ }
4444+}
···2525 "deleted": {
2626 "type": "boolean",
2727 "description": "If true, this message has been deleted or labeled and should be cleared from the cache"
2828+ },
2929+ "badges": {
3030+ "type": "array",
3131+ "description": "Up to 3 badge tokens to display with the message. First badge is server-controlled, remaining badges are user-settable. Tokens are looked up in badges.json for display info.",
3232+ "maxLength": 3,
3333+ "items": {
3434+ "type": "ref",
3535+ "ref": "place.stream.badge.defs#badgeView"
3636+ }
2837 }
2938 }
3039 }
···11+package atproto
22+33+import (
44+ "context"
55+ "fmt"
66+77+ "stream.place/streamplace/pkg/constants"
88+ "stream.place/streamplace/pkg/log"
99+ "stream.place/streamplace/pkg/model"
1010+ "stream.place/streamplace/pkg/streamplace"
1111+)
1212+1313+// AddModBadgeIfApplicable checks if a message author has mod permissions for the streamer
1414+// and adds a mod or streamer badge as the first badge (server-controlled).
1515+// - If the author is the streamer, adds a "streamer" badge
1616+// - If the author has moderation permissions, adds a "mod" badge
1717+func AddModBadgeIfApplicable(ctx context.Context, message *streamplace.ChatDefs_MessageView, streamerDID string, issuerDID string, m model.Model) error {
1818+ if message == nil {
1919+ return fmt.Errorf("message is nil")
2020+ }
2121+2222+ authorDID := message.Author.Did
2323+2424+ var badge *streamplace.BadgeDefs_BadgeView
2525+2626+ // Check if author is the streamer
2727+ if authorDID == streamerDID {
2828+ badge = &streamplace.BadgeDefs_BadgeView{
2929+ BadgeType: constants.BadgeTypeStreamer,
3030+ Issuer: issuerDID,
3131+ Recipient: authorDID,
3232+ }
3333+ } else {
3434+ // Check if author has any moderation permissions for the streamer
3535+ delegations, err := m.GetModerationDelegations(ctx, streamerDID, authorDID)
3636+ if err != nil {
3737+ log.Error(ctx, "failed to get moderation delegations", "err", err, "authorDID", authorDID, "streamerDID", streamerDID)
3838+ return err
3939+ }
4040+4141+ // If the author has any delegations (meaning they're a moderator), add a mod badge
4242+ if len(delegations) > 0 {
4343+ badge = &streamplace.BadgeDefs_BadgeView{
4444+ BadgeType: constants.BadgeTypeMod,
4545+ Issuer: issuerDID,
4646+ Recipient: authorDID,
4747+ }
4848+ }
4949+ }
5050+5151+ // Prepend the badge if one was created (server-controlled badge is first)
5252+ if badge != nil {
5353+ if message.Badges == nil {
5454+ message.Badges = []*streamplace.BadgeDefs_BadgeView{badge}
5555+ } else {
5656+ message.Badges = append([]*streamplace.BadgeDefs_BadgeView{badge}, message.Badges...)
5757+ }
5858+ }
5959+6060+ return nil
6161+}
+100
pkg/atproto/badges_test.go
···11+package atproto
22+33+import (
44+ "context"
55+ "testing"
66+ "time"
77+88+ bsky "github.com/bluesky-social/indigo/api/bsky"
99+ "github.com/bluesky-social/indigo/atproto/syntax"
1010+ "github.com/bluesky-social/indigo/util"
1111+ "github.com/stretchr/testify/require"
1212+ "stream.place/streamplace/pkg/model"
1313+ "stream.place/streamplace/pkg/streamplace"
1414+)
1515+1616+func TestAddModBadge(t *testing.T) {
1717+ ctx := context.Background()
1818+1919+ mod, err := model.MakeDB(":memory:")
2020+ require.NoError(t, err)
2121+2222+ streamerDID := "did:plc:streamer"
2323+ moderatorDID := "did:plc:moderator"
2424+ issuerDID := "did:web:example.com"
2525+2626+ // Create a chat message
2727+ message := &streamplace.ChatDefs_MessageView{
2828+ LexiconTypeID: "place.stream.chat.defs#messageView",
2929+ Uri: "at://test/place.stream.chat.message/123",
3030+ Cid: "test-cid",
3131+ Author: &bsky.ActorDefs_ProfileViewBasic{
3232+ Did: moderatorDID,
3333+ Handle: "moderator.test",
3434+ },
3535+ IndexedAt: "2024-01-01T00:00:00Z",
3636+ }
3737+3838+ t.Run("no badge when user is not a moderator", func(t *testing.T) {
3939+ msg := *message // copy
4040+ err := AddModBadgeIfApplicable(ctx, &msg, streamerDID, issuerDID, mod)
4141+ require.NoError(t, err)
4242+ require.Nil(t, msg.Badges, "should not have badges when user is not a moderator")
4343+ })
4444+4545+ t.Run("adds streamer badge when user is the streamer", func(t *testing.T) {
4646+ msg := *message // copy
4747+ msg.Author = &bsky.ActorDefs_ProfileViewBasic{
4848+ Did: streamerDID,
4949+ Handle: "streamer.test",
5050+ }
5151+ err := AddModBadgeIfApplicable(ctx, &msg, streamerDID, issuerDID, mod)
5252+ require.NoError(t, err)
5353+ require.Len(t, msg.Badges, 1, "should have 1 badge when user is the streamer")
5454+ require.Equal(t, "place.stream.badge.defs#streamer", msg.Badges[0].BadgeType)
5555+ require.Equal(t, issuerDID, msg.Badges[0].Issuer)
5656+ require.Equal(t, streamerDID, msg.Badges[0].Recipient)
5757+ })
5858+5959+ t.Run("adds mod badge when user has moderation permissions", func(t *testing.T) {
6060+ // Grant moderation permissions to the moderator
6161+ perm := &streamplace.ModerationPermission{
6262+ LexiconTypeID: "place.stream.moderation.permission",
6363+ Moderator: moderatorDID,
6464+ Permissions: []string{"ban", "hide"},
6565+ CreatedAt: time.Now().Format(util.ISO8601),
6666+ }
6767+ aturi, err := syntax.ParseATURI("at://" + streamerDID + "/place.stream.moderation.permission/test123")
6868+ require.NoError(t, err)
6969+7070+ // Sync the permission to the model
7171+ err = mod.CreateModerationDelegation(ctx, perm, aturi)
7272+ require.NoError(t, err)
7373+7474+ msg := *message // copy
7575+ err = AddModBadgeIfApplicable(ctx, &msg, streamerDID, issuerDID, mod)
7676+ require.NoError(t, err)
7777+ require.Len(t, msg.Badges, 1, "should have 1 badge when user is a moderator")
7878+ require.Equal(t, "place.stream.badges.badge#mod", msg.Badges[0].BadgeType)
7979+ require.Equal(t, issuerDID, msg.Badges[0].Issuer)
8080+ require.Equal(t, moderatorDID, msg.Badges[0].Recipient)
8181+ })
8282+8383+ t.Run("prepends mod badge to existing badges", func(t *testing.T) {
8484+ // Create message with existing user-settable badge
8585+ msg := *message // copy
8686+ msg.Badges = []*streamplace.BadgeDefs_BadgeView{
8787+ {
8888+ BadgeType: "place.stream.badges.badge#vip",
8989+ Issuer: "did:web:other.com",
9090+ Recipient: moderatorDID,
9191+ },
9292+ }
9393+9494+ err = AddModBadgeIfApplicable(ctx, &msg, streamerDID, issuerDID, mod)
9595+ require.NoError(t, err)
9696+ require.Len(t, msg.Badges, 2, "should have 2 badges")
9797+ require.Equal(t, "place.stream.badges.badge#mod", msg.Badges[0].BadgeType, "mod badge should be first")
9898+ require.Equal(t, "place.stream.badges.badge#vip", msg.Badges[1].BadgeType, "vip badge should be second")
9999+ })
100100+}
···5656 Build *BuildFlags
5757 DataDir string
5858 DBURL string
5959+ LocalDBURL string
5960 EthAccountAddr string
6061 EthKeystorePath string
6162 EthPassword string
···242243 cli.StringSliceFlag(fs, &cli.AdminDIDs, "admin-dids", []string{}, "comma-separated list of DIDs that are authorized to modify branding and other admin operations")
243244 cli.StringSliceFlag(fs, &cli.Syndicate, "syndicate", []string{}, "list of DIDs that we should rebroadcast ('*' for everybody)")
244245 fs.BoolVar(&cli.PlayerTelemetry, "player-telemetry", true, "enable player telemetry")
246246+ fs.StringVar(&cli.LocalDBURL, "local-db-url", "sqlite://$SP_DATA_DIR/localdb.sqlite", "URL of the local database to use for storing local data")
247247+ cli.dataDirFlags = append(cli.dataDirFlags, &cli.LocalDBURL)
245248246249 fs.Bool("external-signing", true, "DEPRECATED, does nothing.")
247250 fs.Bool("insecure", false, "DEPRECATED, does nothing.")
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+// Lexicon schema: place.stream.badge.defs
44+55+package streamplace
66+77+// BadgeDefs_BadgeView is a "badgeView" in the place.stream.badge.defs schema.
88+//
99+// View of a badge record, with fields resolved for display. If the DID in issuer is not the current streamplace node, the signature field shall be required.
1010+type BadgeDefs_BadgeView struct {
1111+ BadgeType string `json:"badgeType" cborgen:"badgeType"`
1212+ // issuer: DID of the badge issuer.
1313+ Issuer string `json:"issuer" cborgen:"issuer"`
1414+ // recipient: DID of the badge recipient.
1515+ Recipient string `json:"recipient" cborgen:"recipient"`
1616+ // signature: TODO: Cryptographic signature of the badge (of a place.stream.key).
1717+ Signature *string `json:"signature,omitempty" cborgen:"signature,omitempty"`
1818+}
+28
pkg/streamplace/badgedisplay.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+// Lexicon schema: place.stream.badge.display
44+55+package streamplace
66+77+import (
88+ lexutil "github.com/bluesky-social/indigo/lex/util"
99+)
1010+1111+func init() {
1212+ lexutil.RegisterType("place.stream.badge.display", &BadgeDisplay{})
1313+}
1414+1515+type BadgeDisplay struct {
1616+ LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.badge.display"`
1717+ // badges: Up to 3 badge tokens to display with the message. First badge is server-controlled, remaining badges are user-settable.
1818+ Badges []*BadgeDisplay_BadgeSelection `json:"badges" cborgen:"badges"`
1919+}
2020+2121+// BadgeDisplay_BadgeSelection is a "badgeSelection" in the place.stream.badge.display schema.
2222+//
2323+// A badge selected for display. May be a full badgeView from the server, or a token representing a badge type that the client can look up for display info.
2424+type BadgeDisplay_BadgeSelection struct {
2525+ BadgeType string `json:"badgeType" cborgen:"badgeType"`
2626+ // issuance: URI of the badge issuance record (place.stream.badge.issuance) that represents this badge. Required if badgeType is not recognized.
2727+ Issuance *string `json:"issuance,omitempty" cborgen:"issuance,omitempty"`
2828+}
+22
pkg/streamplace/badgeissuance.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+// Lexicon schema: place.stream.badge.issuance
44+55+package streamplace
66+77+import (
88+ lexutil "github.com/bluesky-social/indigo/lex/util"
99+)
1010+1111+func init() {
1212+ lexutil.RegisterType("place.stream.badge.issuance", &BadgeIssuance{})
1313+}
1414+1515+type BadgeIssuance struct {
1616+ LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.badge.issuance"`
1717+ BadgeType string `json:"badgeType" cborgen:"badgeType"`
1818+ // recipient: DID of the badge recipient.
1919+ Recipient string `json:"recipient" cborgen:"recipient"`
2020+ // signature: TODO: Cryptographic signature of the badge (of a place.stream.key).
2121+ Signature string `json:"signature" cborgen:"signature"`
2222+}
+527
pkg/streamplace/cbor_gen.go
···5966596659675967 return nil
59685968}
59695969+func (t *BadgeIssuance) MarshalCBOR(w io.Writer) error {
59705970+ if t == nil {
59715971+ _, err := w.Write(cbg.CborNull)
59725972+ return err
59735973+ }
59745974+59755975+ cw := cbg.NewCborWriter(w)
59765976+59775977+ if _, err := cw.Write([]byte{164}); err != nil {
59785978+ return err
59795979+ }
59805980+59815981+ // t.LexiconTypeID (string) (string)
59825982+ if len("$type") > 1000000 {
59835983+ return xerrors.Errorf("Value in field \"$type\" was too long")
59845984+ }
59855985+59865986+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
59875987+ return err
59885988+ }
59895989+ if _, err := cw.WriteString(string("$type")); err != nil {
59905990+ return err
59915991+ }
59925992+59935993+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.badge.issuance"))); err != nil {
59945994+ return err
59955995+ }
59965996+ if _, err := cw.WriteString(string("place.stream.badge.issuance")); err != nil {
59975997+ return err
59985998+ }
59995999+60006000+ // t.BadgeType (string) (string)
60016001+ if len("badgeType") > 1000000 {
60026002+ return xerrors.Errorf("Value in field \"badgeType\" was too long")
60036003+ }
60046004+60056005+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("badgeType"))); err != nil {
60066006+ return err
60076007+ }
60086008+ if _, err := cw.WriteString(string("badgeType")); err != nil {
60096009+ return err
60106010+ }
60116011+60126012+ if len(t.BadgeType) > 1000000 {
60136013+ return xerrors.Errorf("Value in field t.BadgeType was too long")
60146014+ }
60156015+60166016+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.BadgeType))); err != nil {
60176017+ return err
60186018+ }
60196019+ if _, err := cw.WriteString(string(t.BadgeType)); err != nil {
60206020+ return err
60216021+ }
60226022+60236023+ // t.Recipient (string) (string)
60246024+ if len("recipient") > 1000000 {
60256025+ return xerrors.Errorf("Value in field \"recipient\" was too long")
60266026+ }
60276027+60286028+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("recipient"))); err != nil {
60296029+ return err
60306030+ }
60316031+ if _, err := cw.WriteString(string("recipient")); err != nil {
60326032+ return err
60336033+ }
60346034+60356035+ if len(t.Recipient) > 1000000 {
60366036+ return xerrors.Errorf("Value in field t.Recipient was too long")
60376037+ }
60386038+60396039+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Recipient))); err != nil {
60406040+ return err
60416041+ }
60426042+ if _, err := cw.WriteString(string(t.Recipient)); err != nil {
60436043+ return err
60446044+ }
60456045+60466046+ // t.Signature (string) (string)
60476047+ if len("signature") > 1000000 {
60486048+ return xerrors.Errorf("Value in field \"signature\" was too long")
60496049+ }
60506050+60516051+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("signature"))); err != nil {
60526052+ return err
60536053+ }
60546054+ if _, err := cw.WriteString(string("signature")); err != nil {
60556055+ return err
60566056+ }
60576057+60586058+ if len(t.Signature) > 1000000 {
60596059+ return xerrors.Errorf("Value in field t.Signature was too long")
60606060+ }
60616061+60626062+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Signature))); err != nil {
60636063+ return err
60646064+ }
60656065+ if _, err := cw.WriteString(string(t.Signature)); err != nil {
60666066+ return err
60676067+ }
60686068+ return nil
60696069+}
60706070+60716071+func (t *BadgeIssuance) UnmarshalCBOR(r io.Reader) (err error) {
60726072+ *t = BadgeIssuance{}
60736073+60746074+ cr := cbg.NewCborReader(r)
60756075+60766076+ maj, extra, err := cr.ReadHeader()
60776077+ if err != nil {
60786078+ return err
60796079+ }
60806080+ defer func() {
60816081+ if err == io.EOF {
60826082+ err = io.ErrUnexpectedEOF
60836083+ }
60846084+ }()
60856085+60866086+ if maj != cbg.MajMap {
60876087+ return fmt.Errorf("cbor input should be of type map")
60886088+ }
60896089+60906090+ if extra > cbg.MaxLength {
60916091+ return fmt.Errorf("BadgeIssuance: map struct too large (%d)", extra)
60926092+ }
60936093+60946094+ n := extra
60956095+60966096+ nameBuf := make([]byte, 9)
60976097+ for i := uint64(0); i < n; i++ {
60986098+ nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
60996099+ if err != nil {
61006100+ return err
61016101+ }
61026102+61036103+ if !ok {
61046104+ // Field doesn't exist on this type, so ignore it
61056105+ if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
61066106+ return err
61076107+ }
61086108+ continue
61096109+ }
61106110+61116111+ switch string(nameBuf[:nameLen]) {
61126112+ // t.LexiconTypeID (string) (string)
61136113+ case "$type":
61146114+61156115+ {
61166116+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
61176117+ if err != nil {
61186118+ return err
61196119+ }
61206120+61216121+ t.LexiconTypeID = string(sval)
61226122+ }
61236123+ // t.BadgeType (string) (string)
61246124+ case "badgeType":
61256125+61266126+ {
61276127+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
61286128+ if err != nil {
61296129+ return err
61306130+ }
61316131+61326132+ t.BadgeType = string(sval)
61336133+ }
61346134+ // t.Recipient (string) (string)
61356135+ case "recipient":
61366136+61376137+ {
61386138+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
61396139+ if err != nil {
61406140+ return err
61416141+ }
61426142+61436143+ t.Recipient = string(sval)
61446144+ }
61456145+ // t.Signature (string) (string)
61466146+ case "signature":
61476147+61486148+ {
61496149+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
61506150+ if err != nil {
61516151+ return err
61526152+ }
61536153+61546154+ t.Signature = string(sval)
61556155+ }
61566156+61576157+ default:
61586158+ // Field doesn't exist on this type, so ignore it
61596159+ if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
61606160+ return err
61616161+ }
61626162+ }
61636163+ }
61646164+61656165+ return nil
61666166+}
61676167+func (t *BadgeDisplay) MarshalCBOR(w io.Writer) error {
61686168+ if t == nil {
61696169+ _, err := w.Write(cbg.CborNull)
61706170+ return err
61716171+ }
61726172+61736173+ cw := cbg.NewCborWriter(w)
61746174+61756175+ if _, err := cw.Write([]byte{162}); err != nil {
61766176+ return err
61776177+ }
61786178+61796179+ // t.LexiconTypeID (string) (string)
61806180+ if len("$type") > 1000000 {
61816181+ return xerrors.Errorf("Value in field \"$type\" was too long")
61826182+ }
61836183+61846184+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
61856185+ return err
61866186+ }
61876187+ if _, err := cw.WriteString(string("$type")); err != nil {
61886188+ return err
61896189+ }
61906190+61916191+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.badge.display"))); err != nil {
61926192+ return err
61936193+ }
61946194+ if _, err := cw.WriteString(string("place.stream.badge.display")); err != nil {
61956195+ return err
61966196+ }
61976197+61986198+ // t.Badges ([]*streamplace.BadgeDisplay_BadgeSelection) (slice)
61996199+ if len("badges") > 1000000 {
62006200+ return xerrors.Errorf("Value in field \"badges\" was too long")
62016201+ }
62026202+62036203+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("badges"))); err != nil {
62046204+ return err
62056205+ }
62066206+ if _, err := cw.WriteString(string("badges")); err != nil {
62076207+ return err
62086208+ }
62096209+62106210+ if len(t.Badges) > 8192 {
62116211+ return xerrors.Errorf("Slice value in field t.Badges was too long")
62126212+ }
62136213+62146214+ if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Badges))); err != nil {
62156215+ return err
62166216+ }
62176217+ for _, v := range t.Badges {
62186218+ if err := v.MarshalCBOR(cw); err != nil {
62196219+ return err
62206220+ }
62216221+62226222+ }
62236223+ return nil
62246224+}
62256225+62266226+func (t *BadgeDisplay) UnmarshalCBOR(r io.Reader) (err error) {
62276227+ *t = BadgeDisplay{}
62286228+62296229+ cr := cbg.NewCborReader(r)
62306230+62316231+ maj, extra, err := cr.ReadHeader()
62326232+ if err != nil {
62336233+ return err
62346234+ }
62356235+ defer func() {
62366236+ if err == io.EOF {
62376237+ err = io.ErrUnexpectedEOF
62386238+ }
62396239+ }()
62406240+62416241+ if maj != cbg.MajMap {
62426242+ return fmt.Errorf("cbor input should be of type map")
62436243+ }
62446244+62456245+ if extra > cbg.MaxLength {
62466246+ return fmt.Errorf("BadgeDisplay: map struct too large (%d)", extra)
62476247+ }
62486248+62496249+ n := extra
62506250+62516251+ nameBuf := make([]byte, 6)
62526252+ for i := uint64(0); i < n; i++ {
62536253+ nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
62546254+ if err != nil {
62556255+ return err
62566256+ }
62576257+62586258+ if !ok {
62596259+ // Field doesn't exist on this type, so ignore it
62606260+ if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
62616261+ return err
62626262+ }
62636263+ continue
62646264+ }
62656265+62666266+ switch string(nameBuf[:nameLen]) {
62676267+ // t.LexiconTypeID (string) (string)
62686268+ case "$type":
62696269+62706270+ {
62716271+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
62726272+ if err != nil {
62736273+ return err
62746274+ }
62756275+62766276+ t.LexiconTypeID = string(sval)
62776277+ }
62786278+ // t.Badges ([]*streamplace.BadgeDisplay_BadgeSelection) (slice)
62796279+ case "badges":
62806280+62816281+ maj, extra, err = cr.ReadHeader()
62826282+ if err != nil {
62836283+ return err
62846284+ }
62856285+62866286+ if extra > 8192 {
62876287+ return fmt.Errorf("t.Badges: array too large (%d)", extra)
62886288+ }
62896289+62906290+ if maj != cbg.MajArray {
62916291+ return fmt.Errorf("expected cbor array")
62926292+ }
62936293+62946294+ if extra > 0 {
62956295+ t.Badges = make([]*BadgeDisplay_BadgeSelection, extra)
62966296+ }
62976297+62986298+ for i := 0; i < int(extra); i++ {
62996299+ {
63006300+ var maj byte
63016301+ var extra uint64
63026302+ var err error
63036303+ _ = maj
63046304+ _ = extra
63056305+ _ = err
63066306+63076307+ {
63086308+63096309+ b, err := cr.ReadByte()
63106310+ if err != nil {
63116311+ return err
63126312+ }
63136313+ if b != cbg.CborNull[0] {
63146314+ if err := cr.UnreadByte(); err != nil {
63156315+ return err
63166316+ }
63176317+ t.Badges[i] = new(BadgeDisplay_BadgeSelection)
63186318+ if err := t.Badges[i].UnmarshalCBOR(cr); err != nil {
63196319+ return xerrors.Errorf("unmarshaling t.Badges[i] pointer: %w", err)
63206320+ }
63216321+ }
63226322+63236323+ }
63246324+63256325+ }
63266326+ }
63276327+63286328+ default:
63296329+ // Field doesn't exist on this type, so ignore it
63306330+ if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
63316331+ return err
63326332+ }
63336333+ }
63346334+ }
63356335+63366336+ return nil
63376337+}
63386338+func (t *BadgeDisplay_BadgeSelection) MarshalCBOR(w io.Writer) error {
63396339+ if t == nil {
63406340+ _, err := w.Write(cbg.CborNull)
63416341+ return err
63426342+ }
63436343+63446344+ cw := cbg.NewCborWriter(w)
63456345+ fieldCount := 2
63466346+63476347+ if t.Issuance == nil {
63486348+ fieldCount--
63496349+ }
63506350+63516351+ if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
63526352+ return err
63536353+ }
63546354+63556355+ // t.Issuance (string) (string)
63566356+ if t.Issuance != nil {
63576357+63586358+ if len("issuance") > 1000000 {
63596359+ return xerrors.Errorf("Value in field \"issuance\" was too long")
63606360+ }
63616361+63626362+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("issuance"))); err != nil {
63636363+ return err
63646364+ }
63656365+ if _, err := cw.WriteString(string("issuance")); err != nil {
63666366+ return err
63676367+ }
63686368+63696369+ if t.Issuance == nil {
63706370+ if _, err := cw.Write(cbg.CborNull); err != nil {
63716371+ return err
63726372+ }
63736373+ } else {
63746374+ if len(*t.Issuance) > 1000000 {
63756375+ return xerrors.Errorf("Value in field t.Issuance was too long")
63766376+ }
63776377+63786378+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Issuance))); err != nil {
63796379+ return err
63806380+ }
63816381+ if _, err := cw.WriteString(string(*t.Issuance)); err != nil {
63826382+ return err
63836383+ }
63846384+ }
63856385+ }
63866386+63876387+ // t.BadgeType (string) (string)
63886388+ if len("badgeType") > 1000000 {
63896389+ return xerrors.Errorf("Value in field \"badgeType\" was too long")
63906390+ }
63916391+63926392+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("badgeType"))); err != nil {
63936393+ return err
63946394+ }
63956395+ if _, err := cw.WriteString(string("badgeType")); err != nil {
63966396+ return err
63976397+ }
63986398+63996399+ if len(t.BadgeType) > 1000000 {
64006400+ return xerrors.Errorf("Value in field t.BadgeType was too long")
64016401+ }
64026402+64036403+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.BadgeType))); err != nil {
64046404+ return err
64056405+ }
64066406+ if _, err := cw.WriteString(string(t.BadgeType)); err != nil {
64076407+ return err
64086408+ }
64096409+ return nil
64106410+}
64116411+64126412+func (t *BadgeDisplay_BadgeSelection) UnmarshalCBOR(r io.Reader) (err error) {
64136413+ *t = BadgeDisplay_BadgeSelection{}
64146414+64156415+ cr := cbg.NewCborReader(r)
64166416+64176417+ maj, extra, err := cr.ReadHeader()
64186418+ if err != nil {
64196419+ return err
64206420+ }
64216421+ defer func() {
64226422+ if err == io.EOF {
64236423+ err = io.ErrUnexpectedEOF
64246424+ }
64256425+ }()
64266426+64276427+ if maj != cbg.MajMap {
64286428+ return fmt.Errorf("cbor input should be of type map")
64296429+ }
64306430+64316431+ if extra > cbg.MaxLength {
64326432+ return fmt.Errorf("BadgeDisplay_BadgeSelection: map struct too large (%d)", extra)
64336433+ }
64346434+64356435+ n := extra
64366436+64376437+ nameBuf := make([]byte, 9)
64386438+ for i := uint64(0); i < n; i++ {
64396439+ nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
64406440+ if err != nil {
64416441+ return err
64426442+ }
64436443+64446444+ if !ok {
64456445+ // Field doesn't exist on this type, so ignore it
64466446+ if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
64476447+ return err
64486448+ }
64496449+ continue
64506450+ }
64516451+64526452+ switch string(nameBuf[:nameLen]) {
64536453+ // t.Issuance (string) (string)
64546454+ case "issuance":
64556455+64566456+ {
64576457+ b, err := cr.ReadByte()
64586458+ if err != nil {
64596459+ return err
64606460+ }
64616461+ if b != cbg.CborNull[0] {
64626462+ if err := cr.UnreadByte(); err != nil {
64636463+ return err
64646464+ }
64656465+64666466+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
64676467+ if err != nil {
64686468+ return err
64696469+ }
64706470+64716471+ t.Issuance = (*string)(&sval)
64726472+ }
64736473+ }
64746474+ // t.BadgeType (string) (string)
64756475+ case "badgeType":
64766476+64776477+ {
64786478+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
64796479+ if err != nil {
64806480+ return err
64816481+ }
64826482+64836483+ t.BadgeType = string(sval)
64846484+ }
64856485+64866486+ default:
64876487+ // Field doesn't exist on this type, so ignore it
64886488+ if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
64896489+ return err
64906490+ }
64916491+ }
64926492+ }
64936493+64946494+ return nil
64956495+}
+4-2
pkg/streamplace/chatdefs.go
···1616type ChatDefs_MessageView struct {
1717 LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.chat.defs#messageView"`
1818 Author *appbsky.ActorDefs_ProfileViewBasic `json:"author" cborgen:"author"`
1919- ChatProfile *ChatProfile `json:"chatProfile,omitempty" cborgen:"chatProfile,omitempty"`
2020- Cid string `json:"cid" cborgen:"cid"`
1919+ // badges: Up to 3 badge tokens to display with the message. First badge is server-controlled, remaining badges are user-settable. Tokens are looked up in badges.json for display info.
2020+ Badges []*BadgeDefs_BadgeView `json:"badges,omitempty" cborgen:"badges,omitempty"`
2121+ ChatProfile *ChatProfile `json:"chatProfile,omitempty" cborgen:"chatProfile,omitempty"`
2222+ Cid string `json:"cid" cborgen:"cid"`
2123 // deleted: If true, this message has been deleted or labeled and should be cleared from the cache
2224 Deleted *bool `json:"deleted,omitempty" cborgen:"deleted,omitempty"`
2325 IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`