···11+import { Platform } from "react-native";
22+33+export default function clearQueryParams(par = ["iss", "state", "code"]) {
44+ if (Platform.OS !== "web") {
55+ return;
66+ }
77+ const u = new URL(document.location.href);
88+ const params = new URLSearchParams(u.search);
99+ if (u.search === "") {
1010+ return;
1111+ }
1212+ par.forEach((p) => params.delete(p));
1313+ u.search = params.toString();
1414+ window.history.replaceState(null, "", u.toString());
1515+}
+3-6
js/atproto-oauth-client-react-native/README.md
···8787forwarded the port with `adb reverse`. For testing on iOS hardware, you'll
8888instead need to set up TLS.
89899090-[react-native-quick-crypto]:
9191- https://github.com/margelo/react-native-quick-crypto
9090+[react-native-quick-crypto]: https://github.com/margelo/react-native-quick-crypto
9291[expo-sqlite]: https://docs.expo.dev/versions/latest/sdk/sqlite/
9393-[README]:
9494- https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser
9595-[example]:
9696- https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example
9292+[README]: https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser
9393+[example]: https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example
···22export * from "./countdown";
33export * from "./input";
44export * from "./metrics";
55-export * from "./sensitive-content-overlay";
65export * from "./streamer-context-menu";
76export * from "./streamer-loading-overlay";
87export * from "./viewer-context-menu";
···11// barrel file :)
22+export * from "./useAQState";
23export * from "./useAvatars";
34export * from "./useCameraToggle";
45export * from "./useDocumentTitle";
···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-5
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···191194 setReportSubject: (
192195 subject: ComAtprotoModerationCreateReport.InputSchema["subject"] | null,
193196 ) => void;
194194-195195- /** Has the user acknowledged the sensitive content warning? */
196196- sensitiveContentAcknowledged: boolean;
197197- /** Function to set the sensitive content acknowledgement state */
198198- setSensitiveContentAcknowledged: (acknowledged: boolean) => void;
199197}
200198201199export type PlayerEvent = {
···11+---
22+import { Card, CardGrid } from "@astrojs/starlight/components";
33+44+interface Props {
55+ searchPlaceholder?: string;
66+}
77+---
88+99+<div class="helpdesk">
1010+1111+ <h2>How can we help?</h2>
1212+ <p>Search the knowledge base, or check out topics below.</p>
1313+1414+ <CardGrid>
1515+ <Card title="Getting Started" icon="rocket">
1616+ <p>New to Streamplace? Start here to set up your first stream.</p>
1717+ <ul>
1818+ <li><a href="/docs/guides/start-streaming/quick-start">Quick start guide</a></li>
1919+ <li><a href="/docs/guides/start-streaming/obs">Stream with OBS</a></li>
2020+ </ul>
2121+ </Card>
2222+2323+ <Card title="Developers & Self-Hosters" icon="laptop">
2424+ <p>Building with Streamplace or running your own node?</p>
2525+ <ul>
2626+ <li><a href="/docs/developers">Developer documentation</a></li>
2727+ </ul>
2828+ </Card>
2929+ </CardGrid>
3030+</div>
3131+3232+<style>
3333+ .helpdesk {
3434+ margin: 0 auto;
3535+ }
3636+3737+ .helpdesk-search {
3838+ margin-bottom: 2rem;
3939+ }
4040+4141+ .search-input {
4242+ width: 100%;
4343+ padding: 1rem 1.5rem;
4444+ font-size: 1.125rem;
4545+ border: 2px solid var(--sl-color-gray-5);
4646+ border-radius: 0.5rem;
4747+ background: var(--sl-color-bg);
4848+ color: var(--sl-color-text);
4949+ transition: border-color 0.2s;
5050+ }
5151+5252+ .search-input:focus {
5353+ outline: none;
5454+ border-color: var(--sl-color-accent);
5555+ }
5656+5757+ .helpdesk h2 {
5858+ margin-bottom: 1.5rem;
5959+ }
6060+</style>
+1-2
js/docs/src/content/docs/components/custom_ui.md
···11---
22title: Creating your own player UI
33-description:
44- How to set up your player UI with components from @streamplace/components.
33+description: How to set up your player UI with components from @streamplace/components.
54---
6576# Building a Custom Player UI
+40
js/docs/src/content/docs/developers.mdx
···11+---
22+title: Developers & Self-Hosters
33+description: Build with Streamplace or run your own infrastructure.
44+template: doc
55+---
66+77+import { Card, CardGrid } from "@astrojs/starlight/components";
88+99+## Learn how to deploy, or contribute to Streamplace.
1010+1111+<br />
1212+1313+<CardGrid stagger>
1414+ <Card title="Building an Application" icon="laptop">
1515+ Integrate live video into your project. - [API
1616+ reference](/docs/lex-reference/place-stream-defs) - [Our component
1717+ library](/docs/components/custom_ui/)
1818+ </Card>
1919+2020+{" "}
2121+2222+<Card title="Self-Hosting" icon="seti:config">
2323+ Run your own Streamplace infrastructure. - [Installation
2424+ guide](/docs/guides/installing/installing-streamplace)
2525+</Card>
2626+2727+{" "}
2828+2929+<Card title="Contributing" icon="github">
3030+ Help improve Streamplace. - [Development
3131+ setup](/docs/guides/streamplace-dev-setup) - [Video
3232+ signing](/docs/video-metadata/intro/)
3333+</Card>
3434+3535+ <Card title="Support & Community" icon="information">
3636+ Get help and connect with other developers. - [GitHub
3737+ issues](https://github.com/streamplace/streamplace/issues) - [Discord
3838+ community](https://discord.stream.place)
3939+ </Card>
4040+</CardGrid>
+3-1
js/docs/src/content/docs/features/danmu.md
···33description: Add flying bullet-style chat comments to the player, or your stream
44---
5566-:::note This feature is experimental and may change in future releases. :::
66+:::note
77+This feature is experimental and may change in future releases.
88+:::
79810[Danmu (or Danmaku)](https://en.wikipedia.org/wiki/Danmaku_subtitling) (ๅผนๅน,
911"bullet curtain") is a comment style where messages fly across the video
+27
js/docs/src/content/docs/features/embed.md
···11+---
22+title: Embedding your livestream
33+description: How to embed your livestream on your website, blog, etc.
44+---
55+66+Streamplace provides an easy way to embed your livestream on any website or
77+blog.
88+99+You can access the embedded livestream page by putting `/embed` in the URL of
1010+your livestream. For example, if your livestream URL is
1111+`https://stream.place/iame.li`, the embed URL will be
1212+`https://stream.place/embed/iame.li`.
1313+1414+You can use the following HTML snippet to embed your livestream:
1515+1616+```html
1717+<iframe
1818+ src="https://stream.place/embed/your-handle"
1919+ width="560"
2020+ height="315"
2121+ frameborder="0"
2222+ allowfullscreen
2323+></iframe>
2424+```
2525+2626+Alternatively, you can use the share sheet located on your livestream page.
2727+Click the "Share" button, and you'll find the embed code ready to copy.
···11+---
22+title: Multistreaming
33+description: Forward your Streamplace stream to other providers.
44+---
55+66+:::note
77+This guide isn't about setting up Streamplace as an OBS destination. See [OBS Multistreaming to Streamplace](/docs/guides/start-streaming/obs-multistreaming/) for information on that.
88+:::
99+1010+Multistreaming lets you forward your Streamplace stream to multiple platforms at the same time. Instead of streaming only to Streamplace, you can forward your stream to any platform that accepts RTMP input.
1111+1212+## Setting up multistream targets
1313+1414+1. Go to **Settings** > **Streaming** > **Multistream Targets**
1515+2. Click **Create Multistream Target**
1616+3. Enter the RTMP or RTMPS URL from your destination platform
1717+4. Optionally give it a name to identify it later
1818+5. Click **Create**
1919+2020+### Finding your multistream URL
2121+2222+Different platforms will provide their own RTMP URLs. Some common examples:
2323+2424+- **YouTube Live**: Format `rtmp://a.rtmp.youtube.com/live2/your-stream-key`
2525+ - Find your stream key at https://studio.youtube.com/channel/UC/livestreaming (click the copy icon in the top right corner of the 'connect your encoder to go live' box)
2626+- **Twitch**: Format `rtmp://usw20.contribute.live-video.net/app/your-stream-key`
2727+ - You can get a valid RTMPS url at https://help.twitch.tv/s/twitch-ingest-recommendation
2828+ - Find your stream key at https://dashboard.twitch.tv/settings/stream (your 'primary stream key')
2929+3030+:::note
3131+Your stream key should automatically be hidden once you confirm. Make sure you've entered it correctly!
3232+:::
3333+3434+## Managing targets during a stream
3535+3636+When you're live, you can see all your multistream targets on the Live Dashboard with their current status:
3737+3838+- **Green (Active)**: Successfully streaming to this target
3939+- **Yellow (Pending)**: Connecting to this target
4040+- **Red (Error)**: Connection failed; check your URL and credentials
4141+- **Gray (Inactive)**: This target is disabled
4242+4343+You can toggle any target on or off with the switch next to its name. Changes take effect immediately.
4444+4545+## Limits
4646+4747+- **Maximum targets**: 100 total per account
4848+- **Maximum active targets**: 5 simultaneous streams
4949+5050+### Credits
5151+5252+A portion of this documentation was taken from [ndroo.tv](https://bsky.app/profile/ndroo.tv)'s [guide on Streamplace](https://ndroo.tv/streamplace.html#2-configuring-your-account).
+83
js/docs/src/content/docs/features/webhooks.md
···11+---
22+title: Discord Webhooks
33+description: Configure Discord webhooks for livestream announcements and chat
44+sidebar:
55+ order: 30
66+---
77+88+Streamplace supports Discord webhooks for receiving livestream
99+notifications and chat messages. You can create, manage, and configure webhooks
1010+to customize how events are delivered to your Discord channels.
1111+1212+## Webhook Events
1313+1414+You can configure webhooks to listen for specific events. For right now, the
1515+following events are supported:
1616+1717+- `Chat`: Triggered when a chat message is sent.
1818+- `Livestream`: Triggered when a livestream starts.
1919+2020+## Creating a Webhook
2121+2222+To create a webhook, go to the "Settings" page of the Streamplace web app, then
2323+navigate to the "Webhooks" section. Click on "Create Webhook". The following
2424+fields are required:
2525+2626+- Name: Webhook URL. For example,
2727+ `https://discord.com/api/webhooks/{webhook.id}/{webhook.token}`
2828+- Events: Select the events you want to subscribe to (e.g., `Chat Messages`,
2929+ `Livestream Started`). `Livestream Started` is pre-checked by default.
3030+3131+We'd recommend also filling out these optional fields:
3232+3333+- Name: A name for the webhook (e.g., "Discord Livestream Notifications") that
3434+ you can remember.
3535+- Description: A description of what this webhook is for (e.g., "Sends
3636+ livestream start notifications to Discord channel").
3737+- Prefix: A prefix to add to each message sent by this webhook (e.g.,
3838+ "[Streamplace] "). Will apply to both Chat and Livestream events!
3939+- Suffix: A suffix to add to each message sent by this webhook (e.g., "is now
4040+ live!"). Will apply to both Chat and Livestream events!
4141+- Text replacements: A list of text replacements to apply to chat messages sent
4242+ by this webhook. Each replacement consists of a "from" string and a "to"
4343+ string. For example, you could replace all instances of "foo" with "bar".
4444+4545+After filling out the form, click "Create" to save your webhook. You should see
4646+it listed in the "Webhooks" section.
4747+4848+## Updating a Webhook
4949+5050+To update a webhook, go to the "Settings" page of the Streamplace web app, then
5151+navigate to the "Webhooks" section. Find the webhook you want to update and
5252+click on the "pen" icon next to it. This will open the webhook edit form, where
5353+you can modify the fields as needed. After making your changes, click "Update"
5454+to save your changes.
5555+5656+## Deleting a Webhook
5757+5858+To delete a webhook, go to the "Settings" page of the Streamplace web app, then
5959+navigate to the "Webhooks" section. Find the webhook you want to delete and
6060+click on the "trash" icon next to it. A confirmation dialog will appear; click
6161+"Delete" to confirm. The webhook will be removed from the list.
6262+6363+## Recommendations
6464+6565+We'd recommend:
6666+6767+- Creating separate Discord channels for livestream notifications and chat
6868+ messages to keep them organized.
6969+ - If you want to have one webhook for both chat and livestream events, you can
7070+ create multiple webhooks with the same URL but different event subscriptions
7171+ and prefixes/suffixes/replacements.
7272+- Testing your webhook by starting a livestream or sending a chat message to
7373+ ensure that notifications are being sent correctly.
7474+7575+## API Documentation
7676+7777+See these endpoint pages:
7878+7979+- [Create Webhook](/docs/api/operations/placestreamservercreatewebhook)
8080+- [Get Webhook](/docs/api/operations/placestreamservergetwebhook)
8181+- [List Webhooks](/docs/api/operations/placestreamserverlistwebhooks)
8282+- [Update Webhook](/docs/api/operations/placestreamserverupdatewebhook)
8383+- [Delete Webhook](/docs/api/operations/placestreamserverdeletewebhook)
+39
js/docs/src/content/docs/features-dev/badges.md
···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: Discord Webhooks
33-description: Configure Discord webhooks for livestream announcements and chat
44-sidebar:
55- order: 30
66----
77-88-Streamplace supports Discord webhook integration for receiving livestream
99-notifications and chat messages. You can create, manage, and configure webhooks
1010-to customize how events are delivered to your Discord channels.
1111-1212-## Webhook Events
1313-1414-You can configure webhooks to listen for specific events. For right now, the
1515-following events are supported:
1616-1717-- `Chat`: Triggered when a chat message is sent.
1818-- `Livestream`: Triggered when a livestream starts.
1919-2020-## Creating a Webhook
2121-2222-To create a webhook, go to the "Settings" page of the Streamplace web app, then
2323-navigate to the "Webhooks" section. Click on "Create Webhook". The following
2424-fields are required:
2525-2626-- Name: Webhook URL. For example,
2727- `https://discord.com/api/webhooks/{webhook.id}/{webhook.token}`
2828-- Events: Select the events you want to subscribe to (e.g., `Chat Messages`,
2929- `Livestream Started`). `Livestream Started` is pre-checked by default.
3030-3131-We'd recommend also filling out these optional fields:
3232-3333-- Name: A name for the webhook (e.g., "Discord Livestream Notifications") that
3434- you can remember.
3535-- Description: A description of what this webhook is for (e.g., "Sends
3636- livestream start notifications to Discord channel").
3737-- Prefix: A prefix to add to each message sent by this webhook (e.g.,
3838- "[Streamplace] "). Will apply to both Chat and Livestream events!
3939-- Suffix: A suffix to add to each message sent by this webhook (e.g., "is now
4040- live!"). Will apply to both Chat and Livestream events!
4141-- Text replacements: A list of text replacements to apply to chat messages sent
4242- by this webhook. Each replacement consists of a "from" string and a "to"
4343- string. For example, you could replace all instances of "foo" with "bar".
4444-4545-After filling out the form, click "Create" to save your webhook. You should see
4646-it listed in the "Webhooks" section.
4747-4848-## Updating a Webhook
4949-5050-To update a webhook, go to the "Settings" page of the Streamplace web app, then
5151-navigate to the "Webhooks" section. Find the webhook you want to update and
5252-click on the "pen" icon next to it. This will open the webhook edit form, where
5353-you can modify the fields as needed. After making your changes, click "Update"
5454-to save your changes.
5555-5656-## Deleting a Webhook
5757-5858-To delete a webhook, go to the "Settings" page of the Streamplace web app, then
5959-navigate to the "Webhooks" section. Find the webhook you want to delete and
6060-click on the "trash" icon next to it. A confirmation dialog will appear; click
6161-"Delete" to confirm. The webhook will be removed from the list.
6262-6363-## Recommendations
6464-6565-We'd recommend:
6666-6767-- Creating separate Discord channels for livestream notifications and chat
6868- messages to keep them organized.
6969- - If you want to have one webhook for both chat and livestream events, you can
7070- create multiple webhooks with the same URL but different event subscriptions
7171- and prefixes/suffixes/replacements.
7272-- Testing your webhook by starting a livestream or sending a chat message to
7373- ensure that notifications are being sent correctly.
7474-7575-## API Documentation
7676-7777-See these endpoint pages:
7878-7979-- [Create Webhook](/docs/api/operations/placestreamservercreatewebhook)
8080-- [Get Webhook](/docs/api/operations/placestreamservergetwebhook)
8181-- [List Webhooks](/docs/api/operations/placestreamserverlistwebhooks)
8282-- [Update Webhook](/docs/api/operations/placestreamserverupdatewebhook)
8383-- [Delete Webhook](/docs/api/operations/placestreamserverdeletewebhook)
···11----
22-title: Embedding your livestream
33-description: How to embed your livestream on your website, blog, etc.
44----
55-66-Streamplace provides an easy way to embed your livestream on any website or
77-blog.
88-99-You can access the embedded livestream page by putting `/embed` in the URL of
1010-your livestream. For example, if your livestream URL is
1111-`https://stream.place/iame.li`, the embed URL will be
1212-`https://stream.place/embed/iame.li`.
1313-1414-You can use the following HTML snippet to embed your livestream:
1515-1616-```html
1717-<iframe
1818- src="https://stream.place/embed/your-handle"
1919- width="560"
2020- height="315"
2121- frameborder="0"
2222- allowfullscreen
2323-></iframe>
2424-```
2525-2626-Alternatively, you can use the share sheet located on your livestream page.
2727-Click the "Share" button, and you'll find the embed code ready to copy.
···11---
22-title: OBS Multistreaming with Streamplace
22+title: OBS Multistreaming to Streamplace
33description:
44 Configure OBS for multistreaming to Streamplace and other platforms using the
55 obs-multi-rtmp plugin.
66sidebar:
77 order: 20
88---
99+1010+:::note
1111+This guide is not about the multistreaming feature. Check
1212+[the multistreaming guide](/docs/features/multistreaming) out for more
1313+information.
1414+:::
9151016This guide explains how to configure Open Broadcaster Software (OBS) for
1117simultaneous streaming to Streamplace and other platforms using the
···66666767- Video Encoder: x264/h264 (**must** be an x/h.264 encoder)
6868- Rate Control: `CBR`
6969-- Keyframe Interval: `1s`
6969+- Keyframe Interval: `1s` (or anything less than once every ~7s)
7070 - This is _one keyframe per second_
7171 - In some situations (e.g. 'keyframe interval (**frames**)'), this should be
7272 set to your FPS.
7373- x264 Options: `bframes=0`
7474 - If available, there also may be a 'bframes' checkbox which should **NOT** be
7575 checked
7676+7777+:::caution
7878+These last two options are very important! Your viewers' experience may be choppy or otherwise subpar if you don't have them correct.
7979+:::
76807781### 3. Announce your stream
7882···9094 - [OBS Multistreaming Guide](guides/obs-multistreaming)
919592962. [**Aitum Multistream Plugin**](https://aitum.tv/products/multi)
9797+9898+Alternatively, you can
9999+[multistream through Streamplace itself.](/docs/features/multistreaming)
9310094101## Best Practices
95102
···11+---
22+title: Quick Start
33+description: Get up and streaming on Streamplace quickly.
44+sidebar:
55+ order: 1
66+---
77+88+This guide gets you from zero to streaming. If you get stuck, check out the full [OBS setup guide](/docs/guides/start-streaming/obs).
99+1010+:::tip
1111+You will want to check out our [community guidelines](https://blog.stream.place/3mcqwibo4ks2w) first for guidance on what you can and cannot do on Streamplace.
1212+:::
1313+1414+## So, what is Streamplace?
1515+1616+Streamplace is a video streaming service built on top of the AT Protocol (Authenticated Transfer Protocol), the same protocol Bluesky is built on.
1717+1818+## Step 1: Create your account
1919+2020+1. Go to [stream.place](https://stream.place)
2121+2. Click "Sign in" in the top right.
2222+3. Use your Atmosphere credentials to log in (ex. your Bluesky handle)
2323+ - You'll need to use your actual password here - we're using OAuth so you enter your password on your PDS. We do not receive your password at all.
2424+4. You're done! Your stream profile is live at `stream.place/your-handle`
2525+2626+## Step 2: Get your stream key
2727+2828+1. Click **Live Dashboard** (or go to [stream.place/dashboard](https://stream.place/dashboard))
2929+2. Click **Stream from OBS**
3030+3. Click **Generate Stream Key**
3131+4. Your key is copied to clipboard automatically
3232+3333+Keep this key private. It's like a password, but for your stream.
3434+3535+## Step 3: Configure OBS
3636+3737+Open OBS and go to **Settings โ Stream**:
3838+3939+- **Service**: `Custom...`
4040+- **Server**: `rtmps://stream.place:1935/live`
4141+- **Stream Key**: Paste what you copied in Step 2
4242+4343+Then go to **Settings โ Output โ Streaming**:
4444+4545+- **Video Encoder**: `libx264` (or `NVIDIA NVENC H.264` if you have an NVIDIA GPU)
4646+- **Rate Control**: `CBR`
4747+- **Bitrate**: `6000` Kbps (adjust down if you drop frames)
4848+- **Keyframe Interval**: `1`
4949+- **x264 Options**: `bframes=0`. If there's a 'bframes' option, you'll want to have that at '0' or unchecked.
5050+5151+:::caution
5252+These last two options are very important! Your viewers' experience may be choppy or otherwise subpar if you don't have them correct.
5353+:::
5454+5555+## Step 4: Go live
5656+5757+1. In OBS, click **Start Streaming**
5858+2. Go back to the Live Dashboard at stream.place
5959+3. Fill in your stream title and optionally pick a thumbnail8
6060+4. If needed, turn on content warnings. ("Metadata" tab in Stream Settings)
6161+5. Click **Announce Livestream**
6262+6. Your stream is now live and visible to the world!
6363+6464+## Next steps
6565+6666+- **Customize your chat**: Change your name color in Settings > Account
6767+- **Stream to other platforms too**: Set your Twitch/YouTube URLs in Settings > Multistream Targets to push your stream there automatically. See the [Multistreaming guide](/docs/features/multistreaming) for more information
6868+- **Improve stream quality**: See the [OBS guide](/docs/guides/start-streaming/obs) for encoder settings and troubleshooting
6969+- **Join the Discord!**: If you need any help, or just want to chat, check out our discord at https://discord.stream.place.
7070+7171+### Credits
7272+7373+A portion of this documentation was taken from [ndroo.tv](https://bsky.app/profile/ndroo.tv)'s excellent [guide on Streamplace](https://ndroo.tv/streamplace.html#2-configuring-your-account).
+2-32
js/docs/src/content/docs/index.mdx
···22title: Welcome to Streamplace!
33description: Begin your development journey with the Streamplace documentation.
44template: doc
55-hero:
66- tagline: Solve live video for your project with Streamplace.
77- image:
88- file: ../../assets/cube.png
99- alt: Streamplace logo. A pink 3d box viewed from a top corner.
1010- actions:
1111- - text: Get Started
1212- link: /docs/guides/start-streaming/obs
1313- icon: right-arrow
1414- - text: Visit Streamplace
1515- link: /
1616- icon: external
1717- variant: minimal
185---
1962020-import { Card, CardGrid } from "@astrojs/starlight/components";
2121-2222-## Next Steps
77+import HelpDesk from "../../components/HelpDesk.astro";
2382424-<CardGrid>
2525- <Card title="Read the Docs" icon="open-book">
2626- Learn how to start streaming with
2727- [Streamplace](/docs/guides/start-streaming/obs).
2828- </Card>
2929- <Card title="Install Streamplace" icon="download">
3030- [Run your own Streamplace
3131- node](/docs/guides/installing/installing-streamplace).
3232- </Card>
3333- <Card title="API Reference" icon="document">
3434- Explore the [Lexicon API reference](/docs/lex-reference/place-stream-defs).
3535- </Card>
3636- <Card title="Developer Setup" icon="setting">
3737- Set up your [development environment](/docs/guides/streamplace-dev-setup).
3838- </Card>
3939-</CardGrid>
99+<HelpDesk />
···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+```
···2828- **Description:** Raw blob data with appropriate content-type
2929- **Schema:**
30303131-_Schema not defined._ **Possible Errors:**
3131+_Schema not defined._
3232+**Possible Errors:**
32333334- `BrandingNotFound`: The requested branding asset does not exist
3435
···13131414**Type:** `record`
15151616-Record indicating a livestream is published and available for replication at a
1717-given address. By convention, the record key is streamer::server
1616+Record indicating a livestream is published and available for replication at a given address. By convention, the record key is streamer::server
18171918**Record Key:** `any`
2019
···13131414**Type:** `record`
15151616-Record created by a Streamplace broadcaster to indicate that they will be
1717-replicating a livestream. NYI
1616+Record created by a Streamplace broadcaster to indicate that they will be replicating a livestream. NYI
18171918**Record Key:** `tid`
2019
···13131414**Type:** `query`
15151616-Find actor suggestions for a prefix search term. Expected use is for
1717-auto-completion during text field entry.
1616+Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry.
18171918**Parameters:**
2019
···13131414**Type:** `record`
15151616-Default metadata record for livestream including content warnings, rights, and
1717-distribution policy
1616+Default metadata record for livestream including content warnings, rights, and distribution policy
18171918**Record Key:** `literal:self`
2019
···33333434**Type:** `token`
35353636-All rights reserved to the creator โ others cannot use, modify, or share without
3737-explicit authorization.
3636+All rights reserved to the creator โ others cannot use, modify, or share without explicit authorization.
38373938---
4039···44434544**Type:** `token`
46454747-Public domain dedication. You waive all copyright and related rights where
4848-possible. Others may copy, modify, distribute, or perform your work for any
4949-purpose without attribution.
4646+Public domain dedication. You waive all copyright and related rights where possible. Others may copy, modify, distribute, or perform your work for any purpose without attribution.
50475148---
5249···56535754**Type:** `token`
58555959-Attribution required. Others may copy, distribute, remix, and build upon your
6060-work, even commercially, if they credit you.
5656+Attribution required. Others may copy, distribute, remix, and build upon your work, even commercially, if they credit you.
61576258---
6359···67636864**Type:** `token`
69657070-Attribution + share-alike. Others may adapt and build upon your work, even
7171-commercially, if they credit you and license their new creations under identical
7272-terms.
6666+Attribution + share-alike. Others may adapt and build upon your work, even commercially, if they credit you and license their new creations under identical terms.
73677468---
7569···79738074**Type:** `token`
81758282-Attribution + non-commercial. Others may adapt and build upon your work for
8383-non-commercial purposes only, and must credit you.
7676+Attribution + non-commercial. Others may adapt and build upon your work for non-commercial purposes only, and must credit you.
84778578---
8679···90839184**Type:** `token`
92859393-Attribution + non-commercial + share-alike. Others may adapt and build upon your
9494-work for non-commercial purposes only, must credit you, and must license their
9595-new creations under identical terms.
8686+Attribution + non-commercial + share-alike. Others may adapt and build upon your work for non-commercial purposes only, must credit you, and must license their new creations under identical terms.
96879788---
9889···1029310394**Type:** `token`
10495105105-Attribution + no derivatives. Others may reuse your work, even commercially, but
106106-it must remain unchanged and you must be credited.
9696+Attribution + no derivatives. Others may reuse your work, even commercially, but it must remain unchanged and you must be credited.
1079710898---
10999···113103114104**Type:** `token`
115105116116-Attribution + non-commercial + no derivatives. Others may download and share
117117-your work with credit, but cannot change it or use it commercially.
106106+Attribution + non-commercial + no derivatives. Others may download and share your work with credit, but cannot change it or use it commercially.
118107119108---
120109
···29293030**Type:** `token`
31313232-The content could be perceived as offensive due to the discussion or display of
3333-death.
3232+The content could be perceived as offensive due to the discussion or display of death.
34333534---
3635···40394140**Type:** `token`
42414343-The content contains a portrayal of the use or abuse of mind altering
4444-substances.
4242+The content contains a portrayal of the use or abuse of mind altering substances.
45434644---
4745···51495250**Type:** `token`
53515454-The content contains violent actions of a fantasy nature, involving human or
5555-non-human characters in situations easily distinguishable from real life.
5252+The content contains violent actions of a fantasy nature, involving human or non-human characters in situations easily distinguishable from real life.
56535754---
5855···62596360**Type:** `token`
64616565-The content contains flashing lights that could be harmful to viewers with
6666-seizure disorders such as photosensitive epilepsy.
6262+The content contains flashing lights that could be harmful to viewers with seizure disorders such as photosensitive epilepsy.
67636864---
6965···93899490**Type:** `token`
95919696-The content contains information that can be used to identify a particular
9797-individual, such as a name, phone number, email address, physical address, or IP
9898-address.
9292+The content contains information that can be used to identify a particular individual, such as a name, phone number, email address, physical address, or IP address.
999310094---
10195···10599106100**Type:** `token`
107101108108-The content could be perceived as offensive due to the discussion or display of
109109-sexuality.
102102+The content could be perceived as offensive due to the discussion or display of sexuality.
110103111104---
112105···116109117110**Type:** `token`
118111119119-The content could be perceived as distressing due to the discussion or display
120120-of suffering or triggering topics, including suicide, eating disorders or self
121121-harm.
112112+The content could be perceived as distressing due to the discussion or display of suffering or triggering topics, including suicide, eating disorders or self harm.
122113123114---
124115···128119129120**Type:** `token`
130121131131-The content could be perceived as offensive due to the discussion or display of
132132-violence.
122122+The content could be perceived as offensive due to the discussion or display of violence.
133123134124---
135125
···13131414**Type:** `procedure`
15151616-Create a block (ban) on behalf of a streamer. Requires 'ban' permission. Creates
1717-an app.bsky.graph.block record in the streamer's repository.
1616+Create a block (ban) on behalf of a streamer. Requires 'ban' permission. Creates an app.bsky.graph.block record in the streamer's repository.
18171918**Parameters:** _(None defined)_
2019···4645**Possible Errors:**
47464847- `Unauthorized`: The request lacks valid authentication credentials.
4949-- `Forbidden`: The caller does not have permission to create blocks for this
5050- streamer.
5151-- `SessionNotFound`: The streamer's OAuth session could not be found or is
5252- invalid.
4848+- `Forbidden`: The caller does not have permission to create blocks for this streamer.
4949+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
53505451---
5552
···13131414**Type:** `procedure`
15151616-Create a gate (hide message) on behalf of a streamer. Requires 'hide'
1717-permission. Creates a place.stream.chat.gate record in the streamer's
1818-repository.
1616+Create a gate (hide message) on behalf of a streamer. Requires 'hide' permission. Creates a place.stream.chat.gate record in the streamer's repository.
19172018**Parameters:** _(None defined)_
2119···4644**Possible Errors:**
47454846- `Unauthorized`: The request lacks valid authentication credentials.
4949-- `Forbidden`: The caller does not have permission to hide messages for this
5050- streamer.
5151-- `SessionNotFound`: The streamer's OAuth session could not be found or is
5252- invalid.
4747+- `Forbidden`: The caller does not have permission to hide messages for this streamer.
4848+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
53495450---
5551
···13131414**Type:** `procedure`
15151616-Delete a block (unban) on behalf of a streamer. Requires 'ban' permission.
1717-Deletes an app.bsky.graph.block record from the streamer's repository.
1616+Delete a block (unban) on behalf of a streamer. Requires 'ban' permission. Deletes an app.bsky.graph.block record from the streamer's repository.
18171918**Parameters:** _(None defined)_
2019···37363837**Schema Type:** `object`
39384040-_(No properties defined)_ **Possible Errors:**
3939+_(No properties defined)_
4040+**Possible Errors:**
41414242- `Unauthorized`: The request lacks valid authentication credentials.
4343-- `Forbidden`: The caller does not have permission to delete blocks for this
4444- streamer.
4545-- `SessionNotFound`: The streamer's OAuth session could not be found or is
4646- invalid.
4343+- `Forbidden`: The caller does not have permission to delete blocks for this streamer.
4444+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
47454846---
4947
···13131414**Type:** `procedure`
15151616-Delete a gate (unhide message) on behalf of a streamer. Requires 'hide'
1717-permission. Deletes a place.stream.chat.gate record from the streamer's
1818-repository.
1616+Delete a gate (unhide message) on behalf of a streamer. Requires 'hide' permission. Deletes a place.stream.chat.gate record from the streamer's repository.
19172018**Parameters:** _(None defined)_
2119···38363937**Schema Type:** `object`
40384141-_(No properties defined)_ **Possible Errors:**
3939+_(No properties defined)_
4040+**Possible Errors:**
42414342- `Unauthorized`: The request lacks valid authentication credentials.
4444-- `Forbidden`: The caller does not have permission to unhide messages for this
4545- streamer.
4646-- `SessionNotFound`: The streamer's OAuth session could not be found or is
4747- invalid.
4343+- `Forbidden`: The caller does not have permission to unhide messages for this streamer.
4444+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
48454946---
5047
···13131414**Type:** `procedure`
15151616-Update livestream metadata on behalf of a streamer. Requires 'livestream.manage'
1717-permission. Updates a place.stream.livestream record in the streamer's
1818-repository.
1616+Update livestream metadata on behalf of a streamer. Requires 'livestream.manage' permission. Updates a place.stream.livestream record in the streamer's repository.
19172018**Parameters:** _(None defined)_
2119···4745**Possible Errors:**
48464947- `Unauthorized`: The request lacks valid authentication credentials.
5050-- `Forbidden`: The caller does not have permission to update livestream metadata
5151- for this streamer.
5252-- `SessionNotFound`: The streamer's OAuth session could not be found or is
5353- invalid.
4848+- `Forbidden`: The caller does not have permission to update livestream metadata for this streamer.
4949+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
5450- `RecordNotFound`: The specified livestream record does not exist.
55515652---
···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 }
-8
lexicons/place/stream/livestream.json
···7878 "type": "ref",
7979 "description": "The number of viewers watching this livestream. Use when you can't reasonably use #viewerCount directly.",
8080 "ref": "#viewerCount"
8181- },
8282- "labels": {
8383- "type": "array",
8484- "items": { "type": "ref", "ref": "com.atproto.label.defs#label" }
8585- },
8686- "contentWarnings": {
8787- "type": "ref",
8888- "ref": "place.stream.metadata.contentWarnings"
8981 }
9082 }
9183 },
···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+}
+9-1
pkg/atproto/sync.go
···150150 log.Error(ctx, "failed to convert chat message to streamplace message view", "err", err)
151151 return nil
152152 }
153153+154154+ // Add mod badge if the author is a moderator
155155+ issuerDID := fmt.Sprintf("did:web:%s", atsync.CLI.BroadcasterHost)
156156+ err = AddModBadgeIfApplicable(ctx, scm, rec.Streamer, issuerDID, atsync.Model)
157157+ if err != nil {
158158+ log.Error(ctx, "failed to add mod badge", "err", err)
159159+ }
160160+153161 go atsync.Bus.Publish(rec.Streamer, scm)
154162155163 if !isUpdate && !isFirstSync {
···348356 if err != nil {
349357 return fmt.Errorf("failed to get latest livestream for repo: %w", err)
350358 }
351351- lsv, err := atsync.Model.GetLivestreamView(ctx, lsHydrated)
359359+ lsv, err := lsHydrated.ToLivestreamView()
352360 if err != nil {
353361 return fmt.Errorf("failed to convert livestream to bsky post view: %w", err)
354362 }
···143143 streams := make([]*placestreamtypes.Livestream_LivestreamView, len(ls))
144144145145 for i, l := range ls {
146146- stream, err := s.model.GetLivestreamView(ctx, &l)
146146+ stream, err := l.ToLivestreamView()
147147 if err != nil {
148148 return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to convert livestream to streamplace livestream: %s", err))
149149 }
+18
pkg/streamplace/badgedefs.go
···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"`
+6-8
pkg/streamplace/streamlivestream.go
···37373838// Livestream_LivestreamView is a "livestreamView" in the place.stream.livestream schema.
3939type Livestream_LivestreamView struct {
4040- LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.livestream#livestreamView"`
4141- Author *appbsky.ActorDefs_ProfileViewBasic `json:"author" cborgen:"author"`
4242- Cid string `json:"cid" cborgen:"cid"`
4343- ContentWarnings *MetadataContentWarnings `json:"contentWarnings,omitempty" cborgen:"contentWarnings,omitempty"`
4444- IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
4545- Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"`
4646- Record *lexutil.LexiconTypeDecoder `json:"record" cborgen:"record"`
4747- Uri string `json:"uri" cborgen:"uri"`
4040+ LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.livestream#livestreamView"`
4141+ Author *appbsky.ActorDefs_ProfileViewBasic `json:"author" cborgen:"author"`
4242+ Cid string `json:"cid" cborgen:"cid"`
4343+ IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
4444+ Record *lexutil.LexiconTypeDecoder `json:"record" cborgen:"record"`
4545+ Uri string `json:"uri" cborgen:"uri"`
4846 // viewerCount: The number of viewers watching this livestream. Use when you can't reasonably use #viewerCount directly.
4947 ViewerCount *Livestream_ViewerCount `json:"viewerCount,omitempty" cborgen:"viewerCount,omitempty"`
5048}