Live video on the AT Protocol

badge start!!

+1221 -13
js/components/assets/badges/live.png

This is a binary file and will not be displayed.

js/components/assets/badges/mod.png

This is a binary file and will not be displayed.

js/components/assets/badges/vip.png

This is a binary file and will not be displayed.

+24 -1
js/components/src/components/chat/chat-message.tsx
··· 4 4 Mention, 5 5 } from "@atproto/api/dist/client/types/app/bsky/richtext/facet"; 6 6 import { memo, useCallback } from "react"; 7 - import { Linking, View } from "react-native"; 7 + import { Image, Linking, View } from "react-native"; 8 8 import { ChatMessageViewHydrated } from "streamplace"; 9 9 import { RichtextSegment, segmentize } from "../../lib/facet"; 10 10 import { borders, flex, gap, ml, mr, opacity, pl } from "../../lib/theme/atoms"; ··· 23 23 }>; 24 24 } 25 25 26 + import { zero } from "../.."; 26 27 import { useLivestreamStore } from "../../livestream-store"; 27 28 import { Text } from "../ui/text"; 28 29 ··· 164 165 style={{ 165 166 fontVariant: ["tabular-nums"], 166 167 color: colors.gray[400], 168 + width: 44, 167 169 }} 168 170 > 169 171 {formatTime(item.record.createdAt)} 170 172 </Text> 171 173 )} 174 + {item.badges?.length ? ( 175 + <View style={[zero.layout.flex.align.end]}> 176 + {item.badges.map((badge, index) => ( 177 + <View style={{ height: 3 }} key={`badge-${index}`}> 178 + {badge.badgeType === "place.stream.badge.defs#mod" ? ( 179 + <Image 180 + source={require("../../../assets/badges/mod.png")} 181 + height={16} 182 + /> 183 + ) : badge.badgeType === "place.stream.badge.defs#streamer" ? ( 184 + <Image 185 + source={require("../../../assets/badges/live.png")} 186 + style={{ height: 20, width: 20, marginTop: 3 }} 187 + /> 188 + ) : ( 189 + <Image source={require("../../../assets/badges/vip.png")} /> 190 + )} 191 + </View> 192 + ))} 193 + </View> 194 + ) : null} 172 195 <Text 173 196 weight="bold" 174 197 color="default"
+1
js/components/src/livestream-store/websocket-consumer.tsx
··· 80 80 chatProfile: (message as any).chatProfile, 81 81 replyTo: (message as any).replyTo, 82 82 deleted: message.deleted, 83 + badges: message.badges, 83 84 }; 84 85 state = reduceChat(state, [hydrated], [], []); 85 86 } else if (PlaceStreamSegment.isRecord(message)) {
+108
js/docs/src/content/docs/lex-reference/badge/place-stream-badge-defs.md
··· 1 + --- 2 + title: place.stream.badge.defs 3 + description: Reference for the place.stream.badge.defs lexicon 4 + --- 5 + 6 + **Lexicon Version:** 1 7 + 8 + ## Definitions 9 + 10 + <a name="badgeview"></a> 11 + 12 + ### `badgeView` 13 + 14 + **Type:** `object` 15 + 16 + 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. 17 + 18 + **Properties:** 19 + 20 + | Name | Type | Req'd | Description | Constraints | 21 + | ----------- | -------- | ----- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------- | 22 + | `badgeType` | `string` | ✅ | | Known Values: `place.stream.badge.defs#mod`, `place.stream.badge.defs#streamer` | 23 + | `issuer` | `string` | ✅ | DID of the badge issuer. | Format: `did` | 24 + | `recipient` | `string` | ✅ | DID of the badge recipient. | Format: `did` | 25 + | `signature` | `string` | ❌ | TODO: Cryptographic signature of the badge (of a place.stream.key). | | 26 + 27 + --- 28 + 29 + <a name="mod"></a> 30 + 31 + ### `mod` 32 + 33 + **Type:** `token` 34 + 35 + This user is a moderator. Displayed with a sword icon. 36 + 37 + --- 38 + 39 + <a name="streamer"></a> 40 + 41 + ### `streamer` 42 + 43 + **Type:** `token` 44 + 45 + This user is the streamer. Displayed with a star icon. 46 + 47 + --- 48 + 49 + <a name="vip"></a> 50 + 51 + ### `vip` 52 + 53 + **Type:** `token` 54 + 55 + This user is a very important person. 56 + 57 + --- 58 + 59 + ## Lexicon Source 60 + 61 + ```json 62 + { 63 + "lexicon": 1, 64 + "id": "place.stream.badge.defs", 65 + "defs": { 66 + "badgeView": { 67 + "type": "object", 68 + "required": ["badgeType", "issuer", "recipient"], 69 + "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.", 70 + "properties": { 71 + "badgeType": { 72 + "type": "string", 73 + "knownValues": [ 74 + "place.stream.badge.defs#mod", 75 + "place.stream.badge.defs#streamer" 76 + ] 77 + }, 78 + "issuer": { 79 + "type": "string", 80 + "format": "did", 81 + "description": "DID of the badge issuer." 82 + }, 83 + "recipient": { 84 + "type": "string", 85 + "format": "did", 86 + "description": "DID of the badge recipient." 87 + }, 88 + "signature": { 89 + "type": "string", 90 + "description": "TODO: Cryptographic signature of the badge (of a place.stream.key)." 91 + } 92 + } 93 + }, 94 + "mod": { 95 + "type": "token", 96 + "description": "This user is a moderator. Displayed with a sword icon." 97 + }, 98 + "streamer": { 99 + "type": "token", 100 + "description": "This user is the streamer. Displayed with a star icon." 101 + }, 102 + "vip": { 103 + "type": "token", 104 + "description": "This user is a very important person." 105 + } 106 + } 107 + } 108 + ```
+90
js/docs/src/content/docs/lex-reference/badge/place-stream-badge-display.md
··· 1 + --- 2 + title: place.stream.badge.display 3 + description: Reference for the place.stream.badge.display lexicon 4 + --- 5 + 6 + **Lexicon Version:** 1 7 + 8 + ## Definitions 9 + 10 + <a name="main"></a> 11 + 12 + ### `main` 13 + 14 + **Type:** `record` 15 + 16 + Record issuing a badge to a user. 17 + 18 + **Record Properties:** 19 + 20 + | Name | Type | Req'd | Description | Constraints | 21 + | -------- | --------------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------- | ------------ | 22 + | `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 | 23 + 24 + --- 25 + 26 + <a name="badgeselection"></a> 27 + 28 + ### `badgeSelection` 29 + 30 + **Type:** `object` 31 + 32 + 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. 33 + 34 + **Properties:** 35 + 36 + | Name | Type | Req'd | Description | Constraints | 37 + | ----------- | -------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | 38 + | `badgeType` | `string` | ✅ | | Known Values: `place.stream.badge.defs#mod`, `place.stream.badge.defs#vip` | 39 + | `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` | 40 + 41 + --- 42 + 43 + ## Lexicon Source 44 + 45 + ```json 46 + { 47 + "lexicon": 1, 48 + "id": "place.stream.badge.display", 49 + "defs": { 50 + "main": { 51 + "type": "record", 52 + "description": "Record issuing a badge to a user.", 53 + "record": { 54 + "type": "object", 55 + "required": ["badges"], 56 + "properties": { 57 + "badges": { 58 + "type": "array", 59 + "description": "Up to 3 badge tokens to display with the message. First badge is server-controlled, remaining badges are user-settable.", 60 + "maxLength": 3, 61 + "items": { 62 + "type": "ref", 63 + "ref": "#badgeSelection" 64 + } 65 + } 66 + } 67 + } 68 + }, 69 + "badgeSelection": { 70 + "type": "object", 71 + "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.", 72 + "required": ["badgeType"], 73 + "properties": { 74 + "badgeType": { 75 + "type": "string", 76 + "knownValues": [ 77 + "place.stream.badge.defs#mod", 78 + "place.stream.badge.defs#vip" 79 + ] 80 + }, 81 + "issuance": { 82 + "type": "string", 83 + "format": "at-uri", 84 + "description": "URI of the badge issuance record (place.stream.badge.issuance) that represents this badge. Required if badgeType is not recognized." 85 + } 86 + } 87 + } 88 + } 89 + } 90 + ```
+63
js/docs/src/content/docs/lex-reference/badge/place-stream-badge-issuance.md
··· 1 + --- 2 + title: place.stream.badge.issuance 3 + description: Reference for the place.stream.badge.issuance lexicon 4 + --- 5 + 6 + **Lexicon Version:** 1 7 + 8 + ## Definitions 9 + 10 + <a name="main"></a> 11 + 12 + ### `main` 13 + 14 + **Type:** `record` 15 + 16 + Record issuing a badge to a user. 17 + 18 + **Record Key:** `tid` 19 + 20 + **Record Properties:** 21 + 22 + | Name | Type | Req'd | Description | Constraints | 23 + | ----------- | -------- | ----- | ------------------------------------------------------------------- | ------------------------------------------- | 24 + | `badgeType` | `string` | ✅ | | Known Values: `place.stream.badge.defs#vip` | 25 + | `recipient` | `string` | ✅ | DID of the badge recipient. | Format: `did` | 26 + | `signature` | `string` | ✅ | TODO: Cryptographic signature of the badge (of a place.stream.key). | | 27 + 28 + --- 29 + 30 + ## Lexicon Source 31 + 32 + ```json 33 + { 34 + "lexicon": 1, 35 + "id": "place.stream.badge.issuance", 36 + "defs": { 37 + "main": { 38 + "type": "record", 39 + "key": "tid", 40 + "description": "Record issuing a badge to a user.", 41 + "record": { 42 + "type": "object", 43 + "required": ["badgeType", "recipient", "signature"], 44 + "properties": { 45 + "badgeType": { 46 + "type": "string", 47 + "knownValues": ["place.stream.badge.defs#vip"] 48 + }, 49 + "recipient": { 50 + "type": "string", 51 + "format": "did", 52 + "description": "DID of the badge recipient." 53 + }, 54 + "signature": { 55 + "type": "string", 56 + "description": "TODO: Cryptographic signature of the badge (of a place.stream.key)." 57 + } 58 + } 59 + } 60 + } 61 + } 62 + } 63 + ```
+20 -10
js/docs/src/content/docs/lex-reference/chat/place-stream-chat-defs.md
··· 15 15 16 16 **Properties:** 17 17 18 - | Name | Type | Req'd | Description | Constraints | 19 - | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | -------------------------------------------------------------------------------------- | ------------------ | 20 - | `uri` | `string` | ✅ | | Format: `at-uri` | 21 - | `cid` | `string` | ✅ | | Format: `cid` | 22 - | `author` | [`app.bsky.actor.defs#profileViewBasic`](https://github.com/bluesky-social/atproto/tree/main/lexicons/app/bsky/actor/defs.json#profileViewBasic) | ✅ | | | 23 - | `record` | `unknown` | ✅ | | | 24 - | `indexedAt` | `string` | ✅ | | Format: `datetime` | 25 - | `chatProfile` | [`place.stream.chat.profile`](/lex-reference/place-stream-chat-profile) | ❌ | | | 26 - | `replyTo` | Union of:<br/>&nbsp;&nbsp;[`#messageView`](#messageview) | ❌ | | | 27 - | `deleted` | `boolean` | ❌ | If true, this message has been deleted or labeled and should be cleared from the cache | | 18 + | Name | Type | Req'd | Description | Constraints | 19 + | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | 20 + | `uri` | `string` | ✅ | | Format: `at-uri` | 21 + | `cid` | `string` | ✅ | | Format: `cid` | 22 + | `author` | [`app.bsky.actor.defs#profileViewBasic`](https://github.com/bluesky-social/atproto/tree/main/lexicons/app/bsky/actor/defs.json#profileViewBasic) | ✅ | | | 23 + | `record` | `unknown` | ✅ | | | 24 + | `indexedAt` | `string` | ✅ | | Format: `datetime` | 25 + | `chatProfile` | [`place.stream.chat.profile`](/lex-reference/place-stream-chat-profile) | ❌ | | | 26 + | `replyTo` | Union of:<br/>&nbsp;&nbsp;[`#messageView`](#messageview) | ❌ | | | 27 + | `deleted` | `boolean` | ❌ | If true, this message has been deleted or labeled and should be cleared from the cache | | 28 + | `badges` | Array of [`place.stream.badge.defs#badgeView`](/lex-reference/place-stream-badge-defs#badgeview) | ❌ | 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. | Max Items: 3 | 28 29 29 30 --- 30 31 ··· 69 70 "deleted": { 70 71 "type": "boolean", 71 72 "description": "If true, this message has been deleted or labeled and should be cleared from the cache" 73 + }, 74 + "badges": { 75 + "type": "array", 76 + "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.", 77 + "maxLength": 3, 78 + "items": { 79 + "type": "ref", 80 + "ref": "place.stream.badge.defs#badgeView" 81 + } 72 82 } 73 83 } 74 84 }
+46
lexicons/place/stream/badge/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.stream.badge.defs", 4 + "defs": { 5 + "badgeView": { 6 + "type": "object", 7 + "required": ["badgeType", "issuer", "recipient"], 8 + "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.", 9 + "properties": { 10 + "badgeType": { 11 + "type": "string", 12 + "knownValues": [ 13 + "place.stream.badge.defs#mod", 14 + "place.stream.badge.defs#streamer" 15 + ] 16 + }, 17 + "issuer": { 18 + "type": "string", 19 + "format": "did", 20 + "description": "DID of the badge issuer." 21 + }, 22 + "recipient": { 23 + "type": "string", 24 + "format": "did", 25 + "description": "DID of the badge recipient." 26 + }, 27 + "signature": { 28 + "type": "string", 29 + "description": "TODO: Cryptographic signature of the badge (of a place.stream.key)." 30 + } 31 + } 32 + }, 33 + "mod": { 34 + "type": "token", 35 + "description": "This user is a moderator. Displayed with a sword icon." 36 + }, 37 + "streamer": { 38 + "type": "token", 39 + "description": "This user is the streamer. Displayed with a star icon." 40 + }, 41 + "vip": { 42 + "type": "token", 43 + "description": "This user is a very important person." 44 + } 45 + } 46 + }
+44
lexicons/place/stream/badge/display.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.stream.badge.display", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "Record issuing a badge to a user.", 8 + "record": { 9 + "type": "object", 10 + "required": ["badges"], 11 + "properties": { 12 + "badges": { 13 + "type": "array", 14 + "description": "Up to 3 badge tokens to display with the message. First badge is server-controlled, remaining badges are user-settable.", 15 + "maxLength": 3, 16 + "items": { 17 + "type": "ref", 18 + "ref": "#badgeSelection" 19 + } 20 + } 21 + } 22 + } 23 + }, 24 + "badgeSelection": { 25 + "type": "object", 26 + "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.", 27 + "required": ["badgeType"], 28 + "properties": { 29 + "badgeType": { 30 + "type": "string", 31 + "knownValues": [ 32 + "place.stream.badge.defs#mod", 33 + "place.stream.badge.defs#vip" 34 + ] 35 + }, 36 + "issuance": { 37 + "type": "string", 38 + "format": "at-uri", 39 + "description": "URI of the badge issuance record (place.stream.badge.issuance) that represents this badge. Required if badgeType is not recognized." 40 + } 41 + } 42 + } 43 + } 44 + }
+30
lexicons/place/stream/badge/issuance.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.stream.badge.issuance", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "description": "Record issuing a badge to a user.", 9 + "record": { 10 + "type": "object", 11 + "required": ["badgeType", "recipient", "signature"], 12 + "properties": { 13 + "badgeType": { 14 + "type": "string", 15 + "knownValues": ["place.stream.badge.defs#vip"] 16 + }, 17 + "recipient": { 18 + "type": "string", 19 + "format": "did", 20 + "description": "DID of the badge recipient." 21 + }, 22 + "signature": { 23 + "type": "string", 24 + "description": "TODO: Cryptographic signature of the badge (of a place.stream.key)." 25 + } 26 + } 27 + } 28 + } 29 + } 30 + }
+9
lexicons/place/stream/chat/defs.json
··· 25 25 "deleted": { 26 26 "type": "boolean", 27 27 "description": "If true, this message has been deleted or labeled and should be cleared from the cache" 28 + }, 29 + "badges": { 30 + "type": "array", 31 + "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.", 32 + "maxLength": 3, 33 + "items": { 34 + "type": "ref", 35 + "ref": "place.stream.badge.defs#badgeView" 36 + } 28 37 } 29 38 } 30 39 }
+9
pkg/api/websocket.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 + "fmt" 6 7 "net" 7 8 "net/http" 8 9 "time" ··· 12 13 "github.com/gorilla/websocket" 13 14 "github.com/julienschmidt/httprouter" 14 15 16 + "stream.place/streamplace/pkg/atproto" 15 17 apierrors "stream.place/streamplace/pkg/errors" 16 18 "stream.place/streamplace/pkg/log" 17 19 "stream.place/streamplace/pkg/renditions" ··· 237 239 log.Error(ctx, "could not get chat messages", "error", err) 238 240 return 239 241 } 242 + 243 + // Add mod badges to messages 244 + issuerDID := fmt.Sprintf("did:web:%s", a.CLI.BroadcasterHost) 240 245 for _, message := range messages { 246 + err := atproto.AddModBadgeIfApplicable(ctx, message, repoDID, issuerDID, a.Model) 247 + if err != nil { 248 + log.Error(ctx, "failed to add mod badge to message", "error", err) 249 + } 241 250 initialBurst <- message 242 251 } 243 252 }()
+61
pkg/atproto/badges.go
··· 1 + package atproto 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + 7 + "stream.place/streamplace/pkg/constants" 8 + "stream.place/streamplace/pkg/log" 9 + "stream.place/streamplace/pkg/model" 10 + "stream.place/streamplace/pkg/streamplace" 11 + ) 12 + 13 + // AddModBadgeIfApplicable checks if a message author has mod permissions for the streamer 14 + // and adds a mod or streamer badge as the first badge (server-controlled). 15 + // - If the author is the streamer, adds a "streamer" badge 16 + // - If the author has moderation permissions, adds a "mod" badge 17 + func AddModBadgeIfApplicable(ctx context.Context, message *streamplace.ChatDefs_MessageView, streamerDID string, issuerDID string, m model.Model) error { 18 + if message == nil { 19 + return fmt.Errorf("message is nil") 20 + } 21 + 22 + authorDID := message.Author.Did 23 + 24 + var badge *streamplace.BadgeDefs_BadgeView 25 + 26 + // Check if author is the streamer 27 + if authorDID == streamerDID { 28 + badge = &streamplace.BadgeDefs_BadgeView{ 29 + BadgeType: constants.BadgeTypeStreamer, 30 + Issuer: issuerDID, 31 + Recipient: authorDID, 32 + } 33 + } else { 34 + // Check if author has any moderation permissions for the streamer 35 + delegations, err := m.GetModerationDelegations(ctx, streamerDID, authorDID) 36 + if err != nil { 37 + log.Error(ctx, "failed to get moderation delegations", "err", err, "authorDID", authorDID, "streamerDID", streamerDID) 38 + return err 39 + } 40 + 41 + // If the author has any delegations (meaning they're a moderator), add a mod badge 42 + if len(delegations) > 0 { 43 + badge = &streamplace.BadgeDefs_BadgeView{ 44 + BadgeType: constants.BadgeTypeMod, 45 + Issuer: issuerDID, 46 + Recipient: authorDID, 47 + } 48 + } 49 + } 50 + 51 + // Prepend the badge if one was created (server-controlled badge is first) 52 + if badge != nil { 53 + if message.Badges == nil { 54 + message.Badges = []*streamplace.BadgeDefs_BadgeView{badge} 55 + } else { 56 + message.Badges = append([]*streamplace.BadgeDefs_BadgeView{badge}, message.Badges...) 57 + } 58 + } 59 + 60 + return nil 61 + }
+100
pkg/atproto/badges_test.go
··· 1 + package atproto 2 + 3 + import ( 4 + "context" 5 + "testing" 6 + "time" 7 + 8 + bsky "github.com/bluesky-social/indigo/api/bsky" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "github.com/bluesky-social/indigo/util" 11 + "github.com/stretchr/testify/require" 12 + "stream.place/streamplace/pkg/model" 13 + "stream.place/streamplace/pkg/streamplace" 14 + ) 15 + 16 + func TestAddModBadge(t *testing.T) { 17 + ctx := context.Background() 18 + 19 + mod, err := model.MakeDB(":memory:") 20 + require.NoError(t, err) 21 + 22 + streamerDID := "did:plc:streamer" 23 + moderatorDID := "did:plc:moderator" 24 + issuerDID := "did:web:example.com" 25 + 26 + // Create a chat message 27 + message := &streamplace.ChatDefs_MessageView{ 28 + LexiconTypeID: "place.stream.chat.defs#messageView", 29 + Uri: "at://test/place.stream.chat.message/123", 30 + Cid: "test-cid", 31 + Author: &bsky.ActorDefs_ProfileViewBasic{ 32 + Did: moderatorDID, 33 + Handle: "moderator.test", 34 + }, 35 + IndexedAt: "2024-01-01T00:00:00Z", 36 + } 37 + 38 + t.Run("no badge when user is not a moderator", func(t *testing.T) { 39 + msg := *message // copy 40 + err := AddModBadgeIfApplicable(ctx, &msg, streamerDID, issuerDID, mod) 41 + require.NoError(t, err) 42 + require.Nil(t, msg.Badges, "should not have badges when user is not a moderator") 43 + }) 44 + 45 + t.Run("adds streamer badge when user is the streamer", func(t *testing.T) { 46 + msg := *message // copy 47 + msg.Author = &bsky.ActorDefs_ProfileViewBasic{ 48 + Did: streamerDID, 49 + Handle: "streamer.test", 50 + } 51 + err := AddModBadgeIfApplicable(ctx, &msg, streamerDID, issuerDID, mod) 52 + require.NoError(t, err) 53 + require.Len(t, msg.Badges, 1, "should have 1 badge when user is the streamer") 54 + require.Equal(t, "place.stream.badge.defs#streamer", msg.Badges[0].BadgeType) 55 + require.Equal(t, issuerDID, msg.Badges[0].Issuer) 56 + require.Equal(t, streamerDID, msg.Badges[0].Recipient) 57 + }) 58 + 59 + t.Run("adds mod badge when user has moderation permissions", func(t *testing.T) { 60 + // Grant moderation permissions to the moderator 61 + perm := &streamplace.ModerationPermission{ 62 + LexiconTypeID: "place.stream.moderation.permission", 63 + Moderator: moderatorDID, 64 + Permissions: []string{"ban", "hide"}, 65 + CreatedAt: time.Now().Format(util.ISO8601), 66 + } 67 + aturi, err := syntax.ParseATURI("at://" + streamerDID + "/place.stream.moderation.permission/test123") 68 + require.NoError(t, err) 69 + 70 + // Sync the permission to the model 71 + err = mod.CreateModerationDelegation(ctx, perm, aturi) 72 + require.NoError(t, err) 73 + 74 + msg := *message // copy 75 + err = AddModBadgeIfApplicable(ctx, &msg, streamerDID, issuerDID, mod) 76 + require.NoError(t, err) 77 + require.Len(t, msg.Badges, 1, "should have 1 badge when user is a moderator") 78 + require.Equal(t, "place.stream.badges.badge#mod", msg.Badges[0].BadgeType) 79 + require.Equal(t, issuerDID, msg.Badges[0].Issuer) 80 + require.Equal(t, moderatorDID, msg.Badges[0].Recipient) 81 + }) 82 + 83 + t.Run("prepends mod badge to existing badges", func(t *testing.T) { 84 + // Create message with existing user-settable badge 85 + msg := *message // copy 86 + msg.Badges = []*streamplace.BadgeDefs_BadgeView{ 87 + { 88 + BadgeType: "place.stream.badges.badge#vip", 89 + Issuer: "did:web:other.com", 90 + Recipient: moderatorDID, 91 + }, 92 + } 93 + 94 + err = AddModBadgeIfApplicable(ctx, &msg, streamerDID, issuerDID, mod) 95 + require.NoError(t, err) 96 + require.Len(t, msg.Badges, 2, "should have 2 badges") 97 + require.Equal(t, "place.stream.badges.badge#mod", msg.Badges[0].BadgeType, "mod badge should be first") 98 + require.Equal(t, "place.stream.badges.badge#vip", msg.Badges[1].BadgeType, "vip badge should be second") 99 + }) 100 + }
+8
pkg/atproto/sync.go
··· 150 150 log.Error(ctx, "failed to convert chat message to streamplace message view", "err", err) 151 151 return nil 152 152 } 153 + 154 + // Add mod badge if the author is a moderator 155 + issuerDID := fmt.Sprintf("did:web:%s", atsync.CLI.BroadcasterHost) 156 + err = AddModBadgeIfApplicable(ctx, scm, rec.Streamer, issuerDID, atsync.Model) 157 + if err != nil { 158 + log.Error(ctx, "failed to add mod badge", "err", err) 159 + } 160 + 153 161 go atsync.Bus.Publish(rec.Streamer, scm) 154 162 155 163 if !isUpdate && !isFirstSync {
+6
pkg/constants/constants.go
··· 15 15 var PLACE_STREAM_DEFAULT_METADATA = "place.stream.metadata.configuration" //nolint:all 16 16 var PLACE_STREAM_LIVE_RECOMMENDATIONS = "place.stream.live.recommendations" //nolint:all 17 17 18 + // Streamplace badge types 19 + const ( 20 + BadgeTypeMod = "place.stream.badge.defs#mod" 21 + BadgeTypeStreamer = "place.stream.badge.defs#streamer" 22 + ) 23 + 18 24 const DID_KEY_PREFIX = "did:key" //nolint:all 19 25 const ADDRESS_KEY_PREFIX = "0x" //nolint:all 20 26
+3
pkg/gen/gen.go
··· 36 36 streamplace.ModerationPermission{}, 37 37 streamplace.LiveTeleport{}, 38 38 streamplace.LiveRecommendations{}, 39 + streamplace.BadgeIssuance{}, 40 + streamplace.BadgeDisplay{}, 41 + streamplace.BadgeDisplay_BadgeSelection{}, 39 42 ); err != nil { 40 43 panic(err) 41 44 }
+18
pkg/streamplace/badgedefs.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + // Lexicon schema: place.stream.badge.defs 4 + 5 + package streamplace 6 + 7 + // BadgeDefs_BadgeView is a "badgeView" in the place.stream.badge.defs schema. 8 + // 9 + // 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. 10 + type BadgeDefs_BadgeView struct { 11 + BadgeType string `json:"badgeType" cborgen:"badgeType"` 12 + // issuer: DID of the badge issuer. 13 + Issuer string `json:"issuer" cborgen:"issuer"` 14 + // recipient: DID of the badge recipient. 15 + Recipient string `json:"recipient" cborgen:"recipient"` 16 + // signature: TODO: Cryptographic signature of the badge (of a place.stream.key). 17 + Signature *string `json:"signature,omitempty" cborgen:"signature,omitempty"` 18 + }
+28
pkg/streamplace/badgedisplay.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + // Lexicon schema: place.stream.badge.display 4 + 5 + package streamplace 6 + 7 + import ( 8 + lexutil "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 11 + func init() { 12 + lexutil.RegisterType("place.stream.badge.display", &BadgeDisplay{}) 13 + } 14 + 15 + type BadgeDisplay struct { 16 + LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.badge.display"` 17 + // badges: Up to 3 badge tokens to display with the message. First badge is server-controlled, remaining badges are user-settable. 18 + Badges []*BadgeDisplay_BadgeSelection `json:"badges" cborgen:"badges"` 19 + } 20 + 21 + // BadgeDisplay_BadgeSelection is a "badgeSelection" in the place.stream.badge.display schema. 22 + // 23 + // 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. 24 + type BadgeDisplay_BadgeSelection struct { 25 + BadgeType string `json:"badgeType" cborgen:"badgeType"` 26 + // issuance: URI of the badge issuance record (place.stream.badge.issuance) that represents this badge. Required if badgeType is not recognized. 27 + Issuance *string `json:"issuance,omitempty" cborgen:"issuance,omitempty"` 28 + }
+22
pkg/streamplace/badgeissuance.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + // Lexicon schema: place.stream.badge.issuance 4 + 5 + package streamplace 6 + 7 + import ( 8 + lexutil "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 11 + func init() { 12 + lexutil.RegisterType("place.stream.badge.issuance", &BadgeIssuance{}) 13 + } 14 + 15 + type BadgeIssuance struct { 16 + LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.badge.issuance"` 17 + BadgeType string `json:"badgeType" cborgen:"badgeType"` 18 + // recipient: DID of the badge recipient. 19 + Recipient string `json:"recipient" cborgen:"recipient"` 20 + // signature: TODO: Cryptographic signature of the badge (of a place.stream.key). 21 + Signature string `json:"signature" cborgen:"signature"` 22 + }
+527
pkg/streamplace/cbor_gen.go
··· 5966 5966 5967 5967 return nil 5968 5968 } 5969 + func (t *BadgeIssuance) MarshalCBOR(w io.Writer) error { 5970 + if t == nil { 5971 + _, err := w.Write(cbg.CborNull) 5972 + return err 5973 + } 5974 + 5975 + cw := cbg.NewCborWriter(w) 5976 + 5977 + if _, err := cw.Write([]byte{164}); err != nil { 5978 + return err 5979 + } 5980 + 5981 + // t.LexiconTypeID (string) (string) 5982 + if len("$type") > 1000000 { 5983 + return xerrors.Errorf("Value in field \"$type\" was too long") 5984 + } 5985 + 5986 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 5987 + return err 5988 + } 5989 + if _, err := cw.WriteString(string("$type")); err != nil { 5990 + return err 5991 + } 5992 + 5993 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.badge.issuance"))); err != nil { 5994 + return err 5995 + } 5996 + if _, err := cw.WriteString(string("place.stream.badge.issuance")); err != nil { 5997 + return err 5998 + } 5999 + 6000 + // t.BadgeType (string) (string) 6001 + if len("badgeType") > 1000000 { 6002 + return xerrors.Errorf("Value in field \"badgeType\" was too long") 6003 + } 6004 + 6005 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("badgeType"))); err != nil { 6006 + return err 6007 + } 6008 + if _, err := cw.WriteString(string("badgeType")); err != nil { 6009 + return err 6010 + } 6011 + 6012 + if len(t.BadgeType) > 1000000 { 6013 + return xerrors.Errorf("Value in field t.BadgeType was too long") 6014 + } 6015 + 6016 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.BadgeType))); err != nil { 6017 + return err 6018 + } 6019 + if _, err := cw.WriteString(string(t.BadgeType)); err != nil { 6020 + return err 6021 + } 6022 + 6023 + // t.Recipient (string) (string) 6024 + if len("recipient") > 1000000 { 6025 + return xerrors.Errorf("Value in field \"recipient\" was too long") 6026 + } 6027 + 6028 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("recipient"))); err != nil { 6029 + return err 6030 + } 6031 + if _, err := cw.WriteString(string("recipient")); err != nil { 6032 + return err 6033 + } 6034 + 6035 + if len(t.Recipient) > 1000000 { 6036 + return xerrors.Errorf("Value in field t.Recipient was too long") 6037 + } 6038 + 6039 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Recipient))); err != nil { 6040 + return err 6041 + } 6042 + if _, err := cw.WriteString(string(t.Recipient)); err != nil { 6043 + return err 6044 + } 6045 + 6046 + // t.Signature (string) (string) 6047 + if len("signature") > 1000000 { 6048 + return xerrors.Errorf("Value in field \"signature\" was too long") 6049 + } 6050 + 6051 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("signature"))); err != nil { 6052 + return err 6053 + } 6054 + if _, err := cw.WriteString(string("signature")); err != nil { 6055 + return err 6056 + } 6057 + 6058 + if len(t.Signature) > 1000000 { 6059 + return xerrors.Errorf("Value in field t.Signature was too long") 6060 + } 6061 + 6062 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Signature))); err != nil { 6063 + return err 6064 + } 6065 + if _, err := cw.WriteString(string(t.Signature)); err != nil { 6066 + return err 6067 + } 6068 + return nil 6069 + } 6070 + 6071 + func (t *BadgeIssuance) UnmarshalCBOR(r io.Reader) (err error) { 6072 + *t = BadgeIssuance{} 6073 + 6074 + cr := cbg.NewCborReader(r) 6075 + 6076 + maj, extra, err := cr.ReadHeader() 6077 + if err != nil { 6078 + return err 6079 + } 6080 + defer func() { 6081 + if err == io.EOF { 6082 + err = io.ErrUnexpectedEOF 6083 + } 6084 + }() 6085 + 6086 + if maj != cbg.MajMap { 6087 + return fmt.Errorf("cbor input should be of type map") 6088 + } 6089 + 6090 + if extra > cbg.MaxLength { 6091 + return fmt.Errorf("BadgeIssuance: map struct too large (%d)", extra) 6092 + } 6093 + 6094 + n := extra 6095 + 6096 + nameBuf := make([]byte, 9) 6097 + for i := uint64(0); i < n; i++ { 6098 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 6099 + if err != nil { 6100 + return err 6101 + } 6102 + 6103 + if !ok { 6104 + // Field doesn't exist on this type, so ignore it 6105 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 6106 + return err 6107 + } 6108 + continue 6109 + } 6110 + 6111 + switch string(nameBuf[:nameLen]) { 6112 + // t.LexiconTypeID (string) (string) 6113 + case "$type": 6114 + 6115 + { 6116 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 6117 + if err != nil { 6118 + return err 6119 + } 6120 + 6121 + t.LexiconTypeID = string(sval) 6122 + } 6123 + // t.BadgeType (string) (string) 6124 + case "badgeType": 6125 + 6126 + { 6127 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 6128 + if err != nil { 6129 + return err 6130 + } 6131 + 6132 + t.BadgeType = string(sval) 6133 + } 6134 + // t.Recipient (string) (string) 6135 + case "recipient": 6136 + 6137 + { 6138 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 6139 + if err != nil { 6140 + return err 6141 + } 6142 + 6143 + t.Recipient = string(sval) 6144 + } 6145 + // t.Signature (string) (string) 6146 + case "signature": 6147 + 6148 + { 6149 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 6150 + if err != nil { 6151 + return err 6152 + } 6153 + 6154 + t.Signature = string(sval) 6155 + } 6156 + 6157 + default: 6158 + // Field doesn't exist on this type, so ignore it 6159 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 6160 + return err 6161 + } 6162 + } 6163 + } 6164 + 6165 + return nil 6166 + } 6167 + func (t *BadgeDisplay) MarshalCBOR(w io.Writer) error { 6168 + if t == nil { 6169 + _, err := w.Write(cbg.CborNull) 6170 + return err 6171 + } 6172 + 6173 + cw := cbg.NewCborWriter(w) 6174 + 6175 + if _, err := cw.Write([]byte{162}); err != nil { 6176 + return err 6177 + } 6178 + 6179 + // t.LexiconTypeID (string) (string) 6180 + if len("$type") > 1000000 { 6181 + return xerrors.Errorf("Value in field \"$type\" was too long") 6182 + } 6183 + 6184 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 6185 + return err 6186 + } 6187 + if _, err := cw.WriteString(string("$type")); err != nil { 6188 + return err 6189 + } 6190 + 6191 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.badge.display"))); err != nil { 6192 + return err 6193 + } 6194 + if _, err := cw.WriteString(string("place.stream.badge.display")); err != nil { 6195 + return err 6196 + } 6197 + 6198 + // t.Badges ([]*streamplace.BadgeDisplay_BadgeSelection) (slice) 6199 + if len("badges") > 1000000 { 6200 + return xerrors.Errorf("Value in field \"badges\" was too long") 6201 + } 6202 + 6203 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("badges"))); err != nil { 6204 + return err 6205 + } 6206 + if _, err := cw.WriteString(string("badges")); err != nil { 6207 + return err 6208 + } 6209 + 6210 + if len(t.Badges) > 8192 { 6211 + return xerrors.Errorf("Slice value in field t.Badges was too long") 6212 + } 6213 + 6214 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Badges))); err != nil { 6215 + return err 6216 + } 6217 + for _, v := range t.Badges { 6218 + if err := v.MarshalCBOR(cw); err != nil { 6219 + return err 6220 + } 6221 + 6222 + } 6223 + return nil 6224 + } 6225 + 6226 + func (t *BadgeDisplay) UnmarshalCBOR(r io.Reader) (err error) { 6227 + *t = BadgeDisplay{} 6228 + 6229 + cr := cbg.NewCborReader(r) 6230 + 6231 + maj, extra, err := cr.ReadHeader() 6232 + if err != nil { 6233 + return err 6234 + } 6235 + defer func() { 6236 + if err == io.EOF { 6237 + err = io.ErrUnexpectedEOF 6238 + } 6239 + }() 6240 + 6241 + if maj != cbg.MajMap { 6242 + return fmt.Errorf("cbor input should be of type map") 6243 + } 6244 + 6245 + if extra > cbg.MaxLength { 6246 + return fmt.Errorf("BadgeDisplay: map struct too large (%d)", extra) 6247 + } 6248 + 6249 + n := extra 6250 + 6251 + nameBuf := make([]byte, 6) 6252 + for i := uint64(0); i < n; i++ { 6253 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 6254 + if err != nil { 6255 + return err 6256 + } 6257 + 6258 + if !ok { 6259 + // Field doesn't exist on this type, so ignore it 6260 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 6261 + return err 6262 + } 6263 + continue 6264 + } 6265 + 6266 + switch string(nameBuf[:nameLen]) { 6267 + // t.LexiconTypeID (string) (string) 6268 + case "$type": 6269 + 6270 + { 6271 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 6272 + if err != nil { 6273 + return err 6274 + } 6275 + 6276 + t.LexiconTypeID = string(sval) 6277 + } 6278 + // t.Badges ([]*streamplace.BadgeDisplay_BadgeSelection) (slice) 6279 + case "badges": 6280 + 6281 + maj, extra, err = cr.ReadHeader() 6282 + if err != nil { 6283 + return err 6284 + } 6285 + 6286 + if extra > 8192 { 6287 + return fmt.Errorf("t.Badges: array too large (%d)", extra) 6288 + } 6289 + 6290 + if maj != cbg.MajArray { 6291 + return fmt.Errorf("expected cbor array") 6292 + } 6293 + 6294 + if extra > 0 { 6295 + t.Badges = make([]*BadgeDisplay_BadgeSelection, extra) 6296 + } 6297 + 6298 + for i := 0; i < int(extra); i++ { 6299 + { 6300 + var maj byte 6301 + var extra uint64 6302 + var err error 6303 + _ = maj 6304 + _ = extra 6305 + _ = err 6306 + 6307 + { 6308 + 6309 + b, err := cr.ReadByte() 6310 + if err != nil { 6311 + return err 6312 + } 6313 + if b != cbg.CborNull[0] { 6314 + if err := cr.UnreadByte(); err != nil { 6315 + return err 6316 + } 6317 + t.Badges[i] = new(BadgeDisplay_BadgeSelection) 6318 + if err := t.Badges[i].UnmarshalCBOR(cr); err != nil { 6319 + return xerrors.Errorf("unmarshaling t.Badges[i] pointer: %w", err) 6320 + } 6321 + } 6322 + 6323 + } 6324 + 6325 + } 6326 + } 6327 + 6328 + default: 6329 + // Field doesn't exist on this type, so ignore it 6330 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 6331 + return err 6332 + } 6333 + } 6334 + } 6335 + 6336 + return nil 6337 + } 6338 + func (t *BadgeDisplay_BadgeSelection) MarshalCBOR(w io.Writer) error { 6339 + if t == nil { 6340 + _, err := w.Write(cbg.CborNull) 6341 + return err 6342 + } 6343 + 6344 + cw := cbg.NewCborWriter(w) 6345 + fieldCount := 2 6346 + 6347 + if t.Issuance == nil { 6348 + fieldCount-- 6349 + } 6350 + 6351 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 6352 + return err 6353 + } 6354 + 6355 + // t.Issuance (string) (string) 6356 + if t.Issuance != nil { 6357 + 6358 + if len("issuance") > 1000000 { 6359 + return xerrors.Errorf("Value in field \"issuance\" was too long") 6360 + } 6361 + 6362 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("issuance"))); err != nil { 6363 + return err 6364 + } 6365 + if _, err := cw.WriteString(string("issuance")); err != nil { 6366 + return err 6367 + } 6368 + 6369 + if t.Issuance == nil { 6370 + if _, err := cw.Write(cbg.CborNull); err != nil { 6371 + return err 6372 + } 6373 + } else { 6374 + if len(*t.Issuance) > 1000000 { 6375 + return xerrors.Errorf("Value in field t.Issuance was too long") 6376 + } 6377 + 6378 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Issuance))); err != nil { 6379 + return err 6380 + } 6381 + if _, err := cw.WriteString(string(*t.Issuance)); err != nil { 6382 + return err 6383 + } 6384 + } 6385 + } 6386 + 6387 + // t.BadgeType (string) (string) 6388 + if len("badgeType") > 1000000 { 6389 + return xerrors.Errorf("Value in field \"badgeType\" was too long") 6390 + } 6391 + 6392 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("badgeType"))); err != nil { 6393 + return err 6394 + } 6395 + if _, err := cw.WriteString(string("badgeType")); err != nil { 6396 + return err 6397 + } 6398 + 6399 + if len(t.BadgeType) > 1000000 { 6400 + return xerrors.Errorf("Value in field t.BadgeType was too long") 6401 + } 6402 + 6403 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.BadgeType))); err != nil { 6404 + return err 6405 + } 6406 + if _, err := cw.WriteString(string(t.BadgeType)); err != nil { 6407 + return err 6408 + } 6409 + return nil 6410 + } 6411 + 6412 + func (t *BadgeDisplay_BadgeSelection) UnmarshalCBOR(r io.Reader) (err error) { 6413 + *t = BadgeDisplay_BadgeSelection{} 6414 + 6415 + cr := cbg.NewCborReader(r) 6416 + 6417 + maj, extra, err := cr.ReadHeader() 6418 + if err != nil { 6419 + return err 6420 + } 6421 + defer func() { 6422 + if err == io.EOF { 6423 + err = io.ErrUnexpectedEOF 6424 + } 6425 + }() 6426 + 6427 + if maj != cbg.MajMap { 6428 + return fmt.Errorf("cbor input should be of type map") 6429 + } 6430 + 6431 + if extra > cbg.MaxLength { 6432 + return fmt.Errorf("BadgeDisplay_BadgeSelection: map struct too large (%d)", extra) 6433 + } 6434 + 6435 + n := extra 6436 + 6437 + nameBuf := make([]byte, 9) 6438 + for i := uint64(0); i < n; i++ { 6439 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 6440 + if err != nil { 6441 + return err 6442 + } 6443 + 6444 + if !ok { 6445 + // Field doesn't exist on this type, so ignore it 6446 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 6447 + return err 6448 + } 6449 + continue 6450 + } 6451 + 6452 + switch string(nameBuf[:nameLen]) { 6453 + // t.Issuance (string) (string) 6454 + case "issuance": 6455 + 6456 + { 6457 + b, err := cr.ReadByte() 6458 + if err != nil { 6459 + return err 6460 + } 6461 + if b != cbg.CborNull[0] { 6462 + if err := cr.UnreadByte(); err != nil { 6463 + return err 6464 + } 6465 + 6466 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 6467 + if err != nil { 6468 + return err 6469 + } 6470 + 6471 + t.Issuance = (*string)(&sval) 6472 + } 6473 + } 6474 + // t.BadgeType (string) (string) 6475 + case "badgeType": 6476 + 6477 + { 6478 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 6479 + if err != nil { 6480 + return err 6481 + } 6482 + 6483 + t.BadgeType = string(sval) 6484 + } 6485 + 6486 + default: 6487 + // Field doesn't exist on this type, so ignore it 6488 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 6489 + return err 6490 + } 6491 + } 6492 + } 6493 + 6494 + return nil 6495 + }
+4 -2
pkg/streamplace/chatdefs.go
··· 16 16 type ChatDefs_MessageView struct { 17 17 LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.chat.defs#messageView"` 18 18 Author *appbsky.ActorDefs_ProfileViewBasic `json:"author" cborgen:"author"` 19 - ChatProfile *ChatProfile `json:"chatProfile,omitempty" cborgen:"chatProfile,omitempty"` 20 - Cid string `json:"cid" cborgen:"cid"` 19 + // 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. 20 + Badges []*BadgeDefs_BadgeView `json:"badges,omitempty" cborgen:"badges,omitempty"` 21 + ChatProfile *ChatProfile `json:"chatProfile,omitempty" cborgen:"chatProfile,omitempty"` 22 + Cid string `json:"cid" cborgen:"cid"` 21 23 // deleted: If true, this message has been deleted or labeled and should be cleared from the cache 22 24 Deleted *bool `json:"deleted,omitempty" cborgen:"deleted,omitempty"` 23 25 IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`