···1+import {
2+ getPDSServiceEndpoint,
3+ resolveDIDDocument,
4+} from "@streamplace/components";
5import { AppStore } from "store";
6import { StateCreator } from "zustand";
7import { BlueskySlice } from "./blueskySlice";
···205 });
206207 try {
208+ let targetPDS: string | null = null;
209 try {
210+ const didDoc = await resolveDIDDocument(targetDid);
211+ targetPDS = getPDSServiceEndpoint(didDoc);
212+ console.log(
213+ `[getContentMetadata] Resolved PDS for ${targetDid}:`,
214+ targetPDS,
215+ );
00000000216 } catch (pdsResolveError) {
217 console.log(
218 `[getContentMetadata] Failed to resolve PDS for ${targetDid}:`,
+15
js/app/utils/clear-query-params.ts
···000000000000000
···1+import { Platform } from "react-native";
2+3+export default function clearQueryParams(par = ["iss", "state", "code"]) {
4+ if (Platform.OS !== "web") {
5+ return;
6+ }
7+ const u = new URL(document.location.href);
8+ const params = new URLSearchParams(u.search);
9+ if (u.search === "") {
10+ return;
11+ }
12+ par.forEach((p) => params.delete(p));
13+ u.search = params.toString();
14+ window.history.replaceState(null, "", u.toString());
15+}
+3-6
js/atproto-oauth-client-react-native/README.md
···87forwarded the port with `adb reverse`. For testing on iOS hardware, you'll
88instead need to set up TLS.
8990-[react-native-quick-crypto]:
91- https://github.com/margelo/react-native-quick-crypto
92[expo-sqlite]: https://docs.expo.dev/versions/latest/sdk/sqlite/
93-[README]:
94- https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser
95-[example]:
96- https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example
···87forwarded the port with `adb reverse`. For testing on iOS hardware, you'll
88instead need to set up TLS.
8990+[react-native-quick-crypto]: https://github.com/margelo/react-native-quick-crypto
091[expo-sqlite]: https://docs.expo.dev/versions/latest/sdk/sqlite/
92+[README]: https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser
93+[example]: https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example
00
···51 [streamer] Looks like <1>@{ $handle } is offline</1>, but they recommend checking out:
52 *[default] Looks like <1>@{ $handle } is offline</1>, but we recommend checking out:
53}
54-user-offline-no-recommendations =
55 Looks like <1>@{ $handle } is offline</1> right now.
56 Check back later.
57streaming-title = streaming { $title }
···60 [1] 1 viewer
61 *[other] { $count } viewers
62}
000000000000
···51 [streamer] Looks like <1>@{ $handle } is offline</1>, but they recommend checking out:
52 *[default] Looks like <1>@{ $handle } is offline</1>, but we recommend checking out:
53}
54+user-offline-no-recommendations =
55 Looks like <1>@{ $handle } is offline</1> right now.
56 Check back later.
57streaming-title = streaming { $title }
···60 [1] 1 viewer
61 *[other] { $count } viewers
62}
63+64+## PDS Host Selector
65+pds-selector-title = New to the Atmosphere?
66+pds-selector-description = You'll need to select a PDS (Personal Data Server) to access apps on the Atmosphere, such as Bluesky, Tangled, and Spark.
67+pds-selector-custom-label = Another PDS
68+pds-selector-custom-description = Enter your own PDS host URL
69+pds-selector-custom-url-label = Custom PDS URL
70+pds-selector-custom-url-placeholder = https://pds.example.com
71+pds-selector-learn-more = Learn more about self-hosting
72+pds-selector-info = Each host has their own policies and reliability standards. Your ATProto data lives on the host you choose and you can migrate later. Note: Streamplace has its own moderation rules - you can be banned from Streamplace regardless of which host you choose.
73+pds-selector-read-policies = Read { $label }'s <tosLink>Terms of Service</tosLink> and <privacyLink>Privacy Policy</privacyLink> before continuing.
74+pds-selector-handle-policy-checkbox = I have read and agree to the <policyLink>handle policy</policyLink>
+2-1
js/components/locales/en-US/settings.ftl
···121active = Active
122123## Multistreaming
124-multistreaming = Multistreaming
125multistream-targets = Multistream Targets
126multistream-description = Automatically push your Streamplace livestreams to other streaming services like Twitch or YouTube.
127create-multistream-target = Create Multistream Target
···168no-languages-found = No languages found
169170## Branding Administration
0171branding-admin = Branding Administration
172branding-admin-description = Customize your Streamplace instance. Note that settings may take a few hours to propagate.
173branding-login-required = Please log in to manage branding
···121active = Active
122123## Multistreaming
124+multistream = Multistreaming
125multistream-targets = Multistream Targets
126multistream-description = Automatically push your Streamplace livestreams to other streaming services like Twitch or YouTube.
127create-multistream-target = Create Multistream Target
···168no-languages-found = No languages found
169170## Branding Administration
171+branding = Branding
172branding-admin = Branding Administration
173branding-admin-description = Customize your Streamplace instance. Note that settings may take a few hours to propagate.
174branding-login-required = Please log in to manage branding
···5export * from "./primitives/text";
67// Export styled components
08export * from "./button";
9export * from "./checkbox";
10export * from "./dialog";
···5export * from "./primitives/text";
67// Export styled components
8+export * from "./admonition";
9export * from "./button";
10export * from "./checkbox";
11export * from "./dialog";
···5import * as React from "react";
6import { Platform, TextInput, type TextInputProps } from "react-native";
7import { bg, borders, flex, p, text } from "../../lib/theme/atoms";
089const Textarea = React.forwardRef<TextInput, TextInputProps>(
10 ({ style, multiline = true, numberOfLines = 4, ...props }, ref) => {
011 // Detect if we're inside a bottom sheet
12 let isInBottomSheet = false;
13 try {
···38 { borderRadius: 10 },
39 style,
40 ]}
0041 multiline={multiline}
42 numberOfLines={numberOfLines}
43 textAlignVertical="top"
044 {...props}
45 />
46 );
···5import * as React from "react";
6import { Platform, TextInput, type TextInputProps } from "react-native";
7import { bg, borders, flex, p, text } from "../../lib/theme/atoms";
8+import { useTheme } from "../../ui";
910const Textarea = React.forwardRef<TextInput, TextInputProps>(
11 ({ style, multiline = true, numberOfLines = 4, ...props }, ref) => {
12+ let th = useTheme();
13 // Detect if we're inside a bottom sheet
14 let isInBottomSheet = false;
15 try {
···40 { borderRadius: 10 },
41 style,
42 ]}
43+ autoComplete={props.autoComplete || "off"}
44+ textContentType={props.textContentType || "none"}
45 multiline={multiline}
46 numberOfLines={numberOfLines}
47 textAlignVertical="top"
48+ placeholderTextColor={th.theme.colors.textMuted}
49 {...props}
50 />
51 );
+1
js/components/src/hooks/index.ts
···1// barrel file :)
02export * from "./useAvatars";
3export * from "./useCameraToggle";
4export * from "./useDocumentTitle";
···1// barrel file :)
2+export * from "./useAQState";
3export * from "./useAvatars";
4export * from "./useCameraToggle";
5export * from "./useDocumentTitle";
···34export * from "./components/chat/chat";
35export * from "./components/chat/chat-box";
36export * from "./components/chat/system-message";
037export { default as VideoRetry } from "./components/mobile-player/video-retry";
38export * from "./lib/system-messages";
3940export * from "./components/stream-notification";
41export * from "./lib/stream-notifications";
42043export * from "./utils/format-handle";
4445export { DanmuOverlay } from "./components/danmu/danmu-overlay";
···34export * from "./components/chat/chat";
35export * from "./components/chat/chat-box";
36export * from "./components/chat/system-message";
37+export * from "./components/chat/update-stream-title-dialog";
38export { default as VideoRetry } from "./components/mobile-player/video-retry";
39export * from "./lib/system-messages";
4041export * from "./components/stream-notification";
42export * from "./lib/stream-notifications";
4344+export * from "./utils/did";
45export * from "./utils/format-handle";
4647export { DanmuOverlay } from "./components/danmu/danmu-overlay";
···80 chatProfile: (message as any).chatProfile,
81 replyTo: (message as any).replyTo,
82 deleted: message.deleted,
083 };
84 state = reduceChat(state, [hydrated], [], []);
85 } else if (PlaceStreamSegment.isRecord(message)) {
···80 chatProfile: (message as any).chatProfile,
81 replyTo: (message as any).replyTo,
82 deleted: message.deleted,
83+ badges: message.badges,
84 };
85 state = reduceChat(state, [hydrated], [], []);
86 } else if (PlaceStreamSegment.isRecord(message)) {
+3
js/components/src/player-store/player-state.tsx
···63 ingestAutoStart?: boolean;
64 setIngestAutoStart?: (autoStart: boolean) => void;
6500066 /** Timestamp (number) when ingest started, or null if not started */
67 ingestStarted: number | null;
68
···63 ingestAutoStart?: boolean;
64 setIngestAutoStart?: (autoStart: boolean) => void;
6566+ /** stop ingest process, again with a slight delay to allow UI to update */
67+ stopIngest: () => void;
68+69 /** Timestamp (number) when ingest started, or null if not started */
70 ingestStarted: number | null;
71
···25 });
26};
270000000000000000000000028// hook to fetch broadcaster DID (unauthenticated)
29export function useFetchBroadcasterDID() {
30 const streamplaceAgent = usePossiblyUnauthedPDSAgent();
31 const store = getStreamplaceStoreFromContext();
000000000000000000000000000000003233 return useCallback(async () => {
34 try {
···140141// hook to get a specific branding asset by key
142export function useBrandingAsset(key: string): BrandingAsset | undefined {
143- return useStreamplaceStore((state) => state.branding?.[key]);
0000144}
145146// convenience hook for main logo
···25 });
26};
2728+const PropsInHeader = [
29+ "siteTitle",
30+ "siteDescription",
31+ "primaryColor",
32+ "accentColor",
33+ "defaultStreamer",
34+ "mainLogo",
35+ "favicon",
36+ "sidebarBg",
37+ "legalLinks",
38+];
39+40+function getMetaContent(key: string): BrandingAsset | null {
41+ if (typeof window === "undefined" || !window.document) return null;
42+ const meta = document.querySelector(`meta[name="internal-brand:${key}`);
43+ if (meta && meta.getAttribute("content")) {
44+ let content = meta.getAttribute("content");
45+ if (content) return JSON.parse(content) as BrandingAsset;
46+ }
47+48+ return null;
49+}
50+51// hook to fetch broadcaster DID (unauthenticated)
52export function useFetchBroadcasterDID() {
53 const streamplaceAgent = usePossiblyUnauthedPDSAgent();
54 const store = getStreamplaceStoreFromContext();
55+56+ // prefetch from meta records, if on web
57+ useEffect(() => {
58+ if (typeof window !== "undefined" && window.document) {
59+ try {
60+ const metaRecords = PropsInHeader.reduce(
61+ (acc, key) => {
62+ const meta = document.querySelector(
63+ `meta[name="internal-brand:${key}`,
64+ );
65+ // hrmmmmmmmmmmmm
66+ if (meta && meta.getAttribute("content")) {
67+ let content = meta.getAttribute("content");
68+ if (content) acc[key] = JSON.parse(content) as BrandingAsset;
69+ }
70+ return acc;
71+ },
72+ {} as Record<string, BrandingAsset>,
73+ );
74+75+ console.log("Found meta records for broadcaster DID:", metaRecords);
76+ // filter out all non-text values, can get on second fetch?
77+ for (const key of Object.keys(metaRecords)) {
78+ if (metaRecords[key].mimeType != "text/plain") {
79+ delete metaRecords[key];
80+ }
81+ }
82+ } catch (e) {
83+ console.warn("Failed to parse broadcaster DID from meta tags", e);
84+ }
85+ }
86+ }, []);
8788 return useCallback(async () => {
89 try {
···195196// hook to get a specific branding asset by key
197export function useBrandingAsset(key: string): BrandingAsset | undefined {
198+ return (
199+ useStreamplaceStore((state) => state.branding?.[key]) ||
200+ getMetaContent(key) ||
201+ undefined
202+ );
203}
204205// convenience hook for main logo
···1+---
2+import { Card, CardGrid } from "@astrojs/starlight/components";
3+4+interface Props {
5+ searchPlaceholder?: string;
6+}
7+---
8+9+<div class="helpdesk">
10+11+ <h2>How can we help?</h2>
12+ <p>Search the knowledge base, or check out topics below.</p>
13+14+ <CardGrid>
15+ <Card title="Getting Started" icon="rocket">
16+ <p>New to Streamplace? Start here to set up your first stream.</p>
17+ <ul>
18+ <li><a href="/docs/guides/start-streaming/quick-start">Quick start guide</a></li>
19+ <li><a href="/docs/guides/start-streaming/obs">Stream with OBS</a></li>
20+ </ul>
21+ </Card>
22+23+ <Card title="Developers & Self-Hosters" icon="laptop">
24+ <p>Building with Streamplace or running your own node?</p>
25+ <ul>
26+ <li><a href="/docs/developers">Developer documentation</a></li>
27+ </ul>
28+ </Card>
29+ </CardGrid>
30+</div>
31+32+<style>
33+ .helpdesk {
34+ margin: 0 auto;
35+ }
36+37+ .helpdesk-search {
38+ margin-bottom: 2rem;
39+ }
40+41+ .search-input {
42+ width: 100%;
43+ padding: 1rem 1.5rem;
44+ font-size: 1.125rem;
45+ border: 2px solid var(--sl-color-gray-5);
46+ border-radius: 0.5rem;
47+ background: var(--sl-color-bg);
48+ color: var(--sl-color-text);
49+ transition: border-color 0.2s;
50+ }
51+52+ .search-input:focus {
53+ outline: none;
54+ border-color: var(--sl-color-accent);
55+ }
56+57+ .helpdesk h2 {
58+ margin-bottom: 1.5rem;
59+ }
60+</style>
+1-2
js/docs/src/content/docs/components/custom_ui.md
···1---
2title: Creating your own player UI
3-description:
4- How to set up your player UI with components from @streamplace/components.
5---
67# Building a Custom Player UI
···1---
2title: Creating your own player UI
3+description: How to set up your player UI with components from @streamplace/components.
04---
56# Building a Custom Player UI
+40
js/docs/src/content/docs/developers.mdx
···0000000000000000000000000000000000000000
···1+---
2+title: Developers & Self-Hosters
3+description: Build with Streamplace or run your own infrastructure.
4+template: doc
5+---
6+7+import { Card, CardGrid } from "@astrojs/starlight/components";
8+9+## Learn how to deploy, or contribute to Streamplace.
10+11+<br />
12+13+<CardGrid stagger>
14+ <Card title="Building an Application" icon="laptop">
15+ Integrate live video into your project. - [API
16+ reference](/docs/lex-reference/place-stream-defs) - [Our component
17+ library](/docs/components/custom_ui/)
18+ </Card>
19+20+{" "}
21+22+<Card title="Self-Hosting" icon="seti:config">
23+ Run your own Streamplace infrastructure. - [Installation
24+ guide](/docs/guides/installing/installing-streamplace)
25+</Card>
26+27+{" "}
28+29+<Card title="Contributing" icon="github">
30+ Help improve Streamplace. - [Development
31+ setup](/docs/guides/streamplace-dev-setup) - [Video
32+ signing](/docs/video-metadata/intro/)
33+</Card>
34+35+ <Card title="Support & Community" icon="information">
36+ Get help and connect with other developers. - [GitHub
37+ issues](https://github.com/streamplace/streamplace/issues) - [Discord
38+ community](https://discord.stream.place)
39+ </Card>
40+</CardGrid>
+3-1
js/docs/src/content/docs/features/danmu.md
···3description: Add flying bullet-style chat comments to the player, or your stream
4---
56-:::note This feature is experimental and may change in future releases. :::
0078[Danmu (or Danmaku)](https://en.wikipedia.org/wiki/Danmaku_subtitling) (ๅผนๅน,
9"bullet curtain") is a comment style where messages fly across the video
···3description: Add flying bullet-style chat comments to the player, or your stream
4---
56+:::note
7+This feature is experimental and may change in future releases.
8+:::
910[Danmu (or Danmaku)](https://en.wikipedia.org/wiki/Danmaku_subtitling) (ๅผนๅน,
11"bullet curtain") is a comment style where messages fly across the video
+27
js/docs/src/content/docs/features/embed.md
···000000000000000000000000000
···1+---
2+title: Embedding your livestream
3+description: How to embed your livestream on your website, blog, etc.
4+---
5+6+Streamplace provides an easy way to embed your livestream on any website or
7+blog.
8+9+You can access the embedded livestream page by putting `/embed` in the URL of
10+your livestream. For example, if your livestream URL is
11+`https://stream.place/iame.li`, the embed URL will be
12+`https://stream.place/embed/iame.li`.
13+14+You can use the following HTML snippet to embed your livestream:
15+16+```html
17+<iframe
18+ src="https://stream.place/embed/your-handle"
19+ width="560"
20+ height="315"
21+ frameborder="0"
22+ allowfullscreen
23+></iframe>
24+```
25+26+Alternatively, you can use the share sheet located on your livestream page.
27+Click the "Share" button, and you'll find the embed code ready to copy.
···1+---
2+title: Multistreaming
3+description: Forward your Streamplace stream to other providers.
4+---
5+6+:::note
7+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.
8+:::
9+10+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.
11+12+## Setting up multistream targets
13+14+1. Go to **Settings** > **Streaming** > **Multistream Targets**
15+2. Click **Create Multistream Target**
16+3. Enter the RTMP or RTMPS URL from your destination platform
17+4. Optionally give it a name to identify it later
18+5. Click **Create**
19+20+### Finding your multistream URL
21+22+Different platforms will provide their own RTMP URLs. Some common examples:
23+24+- **YouTube Live**: Format `rtmp://a.rtmp.youtube.com/live2/your-stream-key`
25+ - 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)
26+- **Twitch**: Format `rtmp://usw20.contribute.live-video.net/app/your-stream-key`
27+ - You can get a valid RTMPS url at https://help.twitch.tv/s/twitch-ingest-recommendation
28+ - Find your stream key at https://dashboard.twitch.tv/settings/stream (your 'primary stream key')
29+30+:::note
31+Your stream key should automatically be hidden once you confirm. Make sure you've entered it correctly!
32+:::
33+34+## Managing targets during a stream
35+36+When you're live, you can see all your multistream targets on the Live Dashboard with their current status:
37+38+- **Green (Active)**: Successfully streaming to this target
39+- **Yellow (Pending)**: Connecting to this target
40+- **Red (Error)**: Connection failed; check your URL and credentials
41+- **Gray (Inactive)**: This target is disabled
42+43+You can toggle any target on or off with the switch next to its name. Changes take effect immediately.
44+45+## Limits
46+47+- **Maximum targets**: 100 total per account
48+- **Maximum active targets**: 5 simultaneous streams
49+50+### Credits
51+52+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).
···1+---
2+title: Discord Webhooks
3+description: Configure Discord webhooks for livestream announcements and chat
4+sidebar:
5+ order: 30
6+---
7+8+Streamplace supports Discord webhooks for receiving livestream
9+notifications and chat messages. You can create, manage, and configure webhooks
10+to customize how events are delivered to your Discord channels.
11+12+## Webhook Events
13+14+You can configure webhooks to listen for specific events. For right now, the
15+following events are supported:
16+17+- `Chat`: Triggered when a chat message is sent.
18+- `Livestream`: Triggered when a livestream starts.
19+20+## Creating a Webhook
21+22+To create a webhook, go to the "Settings" page of the Streamplace web app, then
23+navigate to the "Webhooks" section. Click on "Create Webhook". The following
24+fields are required:
25+26+- Name: Webhook URL. For example,
27+ `https://discord.com/api/webhooks/{webhook.id}/{webhook.token}`
28+- Events: Select the events you want to subscribe to (e.g., `Chat Messages`,
29+ `Livestream Started`). `Livestream Started` is pre-checked by default.
30+31+We'd recommend also filling out these optional fields:
32+33+- Name: A name for the webhook (e.g., "Discord Livestream Notifications") that
34+ you can remember.
35+- Description: A description of what this webhook is for (e.g., "Sends
36+ livestream start notifications to Discord channel").
37+- Prefix: A prefix to add to each message sent by this webhook (e.g.,
38+ "[Streamplace] "). Will apply to both Chat and Livestream events!
39+- Suffix: A suffix to add to each message sent by this webhook (e.g., "is now
40+ live!"). Will apply to both Chat and Livestream events!
41+- Text replacements: A list of text replacements to apply to chat messages sent
42+ by this webhook. Each replacement consists of a "from" string and a "to"
43+ string. For example, you could replace all instances of "foo" with "bar".
44+45+After filling out the form, click "Create" to save your webhook. You should see
46+it listed in the "Webhooks" section.
47+48+## Updating a Webhook
49+50+To update a webhook, go to the "Settings" page of the Streamplace web app, then
51+navigate to the "Webhooks" section. Find the webhook you want to update and
52+click on the "pen" icon next to it. This will open the webhook edit form, where
53+you can modify the fields as needed. After making your changes, click "Update"
54+to save your changes.
55+56+## Deleting a Webhook
57+58+To delete a webhook, go to the "Settings" page of the Streamplace web app, then
59+navigate to the "Webhooks" section. Find the webhook you want to delete and
60+click on the "trash" icon next to it. A confirmation dialog will appear; click
61+"Delete" to confirm. The webhook will be removed from the list.
62+63+## Recommendations
64+65+We'd recommend:
66+67+- Creating separate Discord channels for livestream notifications and chat
68+ messages to keep them organized.
69+ - If you want to have one webhook for both chat and livestream events, you can
70+ create multiple webhooks with the same URL but different event subscriptions
71+ and prefixes/suffixes/replacements.
72+- Testing your webhook by starting a livestream or sending a chat message to
73+ ensure that notifications are being sent correctly.
74+75+## API Documentation
76+77+See these endpoint pages:
78+79+- [Create Webhook](/docs/api/operations/placestreamservercreatewebhook)
80+- [Get Webhook](/docs/api/operations/placestreamservergetwebhook)
81+- [List Webhooks](/docs/api/operations/placestreamserverlistwebhooks)
82+- [Update Webhook](/docs/api/operations/placestreamserverupdatewebhook)
83+- [Delete Webhook](/docs/api/operations/placestreamserverdeletewebhook)
+39
js/docs/src/content/docs/features-dev/badges.md
···000000000000000000000000000000000000000
···1+---
2+title: badges system
3+description: user badges for chat messages
4+---
5+6+## Overview
7+8+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.
9+10+## Lexicon schemas
11+12+We have three relevant lexicons.
13+14+1. **`place.stream.badge.defs`** - badge definitions and view model
15+16+ - defines known badge types: `mod`, `streamer`, `vip`
17+ - `badgeView` object: `{badgeType, issuer, recipient, signature?}`
18+19+2. **`place.stream.badge.issuance`** - record of badge grant
20+21+ - stored as atproto record (key: tid)
22+ - issued by streamer or other authorized entity
23+ - example: streamer issues vip badge to a user
24+25+3. **`place.stream.badge.display`** - user's badge selection
26+ - user-controlled record defining which badges to show
27+ - array of up to 3 `badgeSelection` objects
28+ - first slot server-controlled (mod/streamer/staff), second slot is streamer-specific (vip, subscription), third slot is user-set (event, staff2, node subscription, etc.)
29+30+:::note
31+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?
32+:::
33+34+## TODO
35+36+- [ ] implement cryptographic signatures for badge issuance
37+- [ ] implement badge issuance ui (streamer grants vip badges)
38+- [ ] implement badge selection ui (users choose which badges to display)
39+- [ ] add more badge types (subscriber, founder, staff, etc)
···1----
2-title: Discord Webhooks
3-description: Configure Discord webhooks for livestream announcements and chat
4-sidebar:
5- order: 30
6----
7-8-Streamplace supports Discord webhook integration for receiving livestream
9-notifications and chat messages. You can create, manage, and configure webhooks
10-to customize how events are delivered to your Discord channels.
11-12-## Webhook Events
13-14-You can configure webhooks to listen for specific events. For right now, the
15-following events are supported:
16-17-- `Chat`: Triggered when a chat message is sent.
18-- `Livestream`: Triggered when a livestream starts.
19-20-## Creating a Webhook
21-22-To create a webhook, go to the "Settings" page of the Streamplace web app, then
23-navigate to the "Webhooks" section. Click on "Create Webhook". The following
24-fields are required:
25-26-- Name: Webhook URL. For example,
27- `https://discord.com/api/webhooks/{webhook.id}/{webhook.token}`
28-- Events: Select the events you want to subscribe to (e.g., `Chat Messages`,
29- `Livestream Started`). `Livestream Started` is pre-checked by default.
30-31-We'd recommend also filling out these optional fields:
32-33-- Name: A name for the webhook (e.g., "Discord Livestream Notifications") that
34- you can remember.
35-- Description: A description of what this webhook is for (e.g., "Sends
36- livestream start notifications to Discord channel").
37-- Prefix: A prefix to add to each message sent by this webhook (e.g.,
38- "[Streamplace] "). Will apply to both Chat and Livestream events!
39-- Suffix: A suffix to add to each message sent by this webhook (e.g., "is now
40- live!"). Will apply to both Chat and Livestream events!
41-- Text replacements: A list of text replacements to apply to chat messages sent
42- by this webhook. Each replacement consists of a "from" string and a "to"
43- string. For example, you could replace all instances of "foo" with "bar".
44-45-After filling out the form, click "Create" to save your webhook. You should see
46-it listed in the "Webhooks" section.
47-48-## Updating a Webhook
49-50-To update a webhook, go to the "Settings" page of the Streamplace web app, then
51-navigate to the "Webhooks" section. Find the webhook you want to update and
52-click on the "pen" icon next to it. This will open the webhook edit form, where
53-you can modify the fields as needed. After making your changes, click "Update"
54-to save your changes.
55-56-## Deleting a Webhook
57-58-To delete a webhook, go to the "Settings" page of the Streamplace web app, then
59-navigate to the "Webhooks" section. Find the webhook you want to delete and
60-click on the "trash" icon next to it. A confirmation dialog will appear; click
61-"Delete" to confirm. The webhook will be removed from the list.
62-63-## Recommendations
64-65-We'd recommend:
66-67-- Creating separate Discord channels for livestream notifications and chat
68- messages to keep them organized.
69- - If you want to have one webhook for both chat and livestream events, you can
70- create multiple webhooks with the same URL but different event subscriptions
71- and prefixes/suffixes/replacements.
72-- Testing your webhook by starting a livestream or sending a chat message to
73- ensure that notifications are being sent correctly.
74-75-## API Documentation
76-77-See these endpoint pages:
78-79-- [Create Webhook](/docs/api/operations/placestreamservercreatewebhook)
80-- [Get Webhook](/docs/api/operations/placestreamservergetwebhook)
81-- [List Webhooks](/docs/api/operations/placestreamserverlistwebhooks)
82-- [Update Webhook](/docs/api/operations/placestreamserverupdatewebhook)
83-- [Delete Webhook](/docs/api/operations/placestreamserverdeletewebhook)
···1----
2-title: Embedding your livestream
3-description: How to embed your livestream on your website, blog, etc.
4----
5-6-Streamplace provides an easy way to embed your livestream on any website or
7-blog.
8-9-You can access the embedded livestream page by putting `/embed` in the URL of
10-your livestream. For example, if your livestream URL is
11-`https://stream.place/iame.li`, the embed URL will be
12-`https://stream.place/embed/iame.li`.
13-14-You can use the following HTML snippet to embed your livestream:
15-16-```html
17-<iframe
18- src="https://stream.place/embed/your-handle"
19- width="560"
20- height="315"
21- frameborder="0"
22- allowfullscreen
23-></iframe>
24-```
25-26-Alternatively, you can use the share sheet located on your livestream page.
27-Click the "Share" button, and you'll find the embed code ready to copy.
···1---
2-title: OBS Multistreaming with Streamplace
3description:
4 Configure OBS for multistreaming to Streamplace and other platforms using the
5 obs-multi-rtmp plugin.
6sidebar:
7 order: 20
8---
000000910This guide explains how to configure Open Broadcaster Software (OBS) for
11simultaneous streaming to Streamplace and other platforms using the
···1---
2+title: OBS Multistreaming to Streamplace
3description:
4 Configure OBS for multistreaming to Streamplace and other platforms using the
5 obs-multi-rtmp plugin.
6sidebar:
7 order: 20
8---
9+10+:::note
11+This guide is not about the multistreaming feature. Check
12+[the multistreaming guide](/docs/features/multistreaming) out for more
13+information.
14+:::
1516This guide explains how to configure Open Broadcaster Software (OBS) for
17simultaneous streaming to Streamplace and other platforms using the
···58- Audio Encoder:
59 - For `RTMP`, choose an appropriate AAC encoder.
60 - For `WHIP`, use `ffmpeg_opus`.
0061- Video Encoder: _(Select appropriate encoder, e.g. libx264/nvenc_h264)_
6263#### 2e. Suggested Video Encoder Settings
64065- Rate Control: `CBR`
66-- Keyframe Interval: `1s`
00067- x264 Options: `bframes=0`
68- - If available, there also may be a 'no bframes' checkbox which should be
69 checked
70000071### 3. Announce your stream
72731. Once you're live, go back to the live dashboard.
···85862. [**Aitum Multistream Plugin**](https://aitum.tv/products/multi)
8700088## Best Practices
8990- Test your stream settings before going live
···96## Additional Resources
9798- [OBS Official Documentation](https://obsproject.com/docs/)
000000
···58- Audio Encoder:
59 - For `RTMP`, choose an appropriate AAC encoder.
60 - For `WHIP`, use `ffmpeg_opus`.
61+ - If you are using a server that supports the SRT protocol (e.g.
62+ multistreaming via NGINX) please check below for an example config.
63- Video Encoder: _(Select appropriate encoder, e.g. libx264/nvenc_h264)_
6465#### 2e. Suggested Video Encoder Settings
6667+- Video Encoder: x264/h264 (**must** be an x/h.264 encoder)
68- Rate Control: `CBR`
69+- Keyframe Interval: `1s` (or anything less than once every ~7s)
70+ - This is _one keyframe per second_
71+ - In some situations (e.g. 'keyframe interval (**frames**)'), this should be
72+ set to your FPS.
73- x264 Options: `bframes=0`
74+ - If available, there also may be a 'bframes' checkbox which should **NOT** be
75 checked
7677+:::caution
78+These last two options are very important! Your viewers' experience may be choppy or otherwise subpar if you don't have them correct.
79+:::
80+81### 3. Announce your stream
82831. Once you're live, go back to the live dashboard.
···95962. [**Aitum Multistream Plugin**](https://aitum.tv/products/multi)
9798+Alternatively, you can
99+[multistream through Streamplace itself.](/docs/features/multistreaming)
100+101## Best Practices
102103- Test your stream settings before going live
···109## Additional Resources
110111- [OBS Official Documentation](https://obsproject.com/docs/)
112+113+### Example Settings
114+115+
116+117+> Multistreaming via a server that supports the SRT protocol
···1+---
2+title: Quick Start
3+description: Get up and streaming on Streamplace quickly.
4+sidebar:
5+ order: 1
6+---
7+8+This guide gets you from zero to streaming. If you get stuck, check out the full [OBS setup guide](/docs/guides/start-streaming/obs).
9+10+:::tip
11+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.
12+:::
13+14+## So, what is Streamplace?
15+16+Streamplace is a video streaming service built on top of the AT Protocol (Authenticated Transfer Protocol), the same protocol Bluesky is built on.
17+18+## Step 1: Create your account
19+20+1. Go to [stream.place](https://stream.place)
21+2. Click "Sign in" in the top right.
22+3. Use your Atmosphere credentials to log in (ex. your Bluesky handle)
23+ - 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.
24+4. You're done! Your stream profile is live at `stream.place/your-handle`
25+26+## Step 2: Get your stream key
27+28+1. Click **Live Dashboard** (or go to [stream.place/dashboard](https://stream.place/dashboard))
29+2. Click **Stream from OBS**
30+3. Click **Generate Stream Key**
31+4. Your key is copied to clipboard automatically
32+33+Keep this key private. It's like a password, but for your stream.
34+35+## Step 3: Configure OBS
36+37+Open OBS and go to **Settings โ Stream**:
38+39+- **Service**: `Custom...`
40+- **Server**: `rtmps://stream.place:1935/live`
41+- **Stream Key**: Paste what you copied in Step 2
42+43+Then go to **Settings โ Output โ Streaming**:
44+45+- **Video Encoder**: `libx264` (or `NVIDIA NVENC H.264` if you have an NVIDIA GPU)
46+- **Rate Control**: `CBR`
47+- **Bitrate**: `6000` Kbps (adjust down if you drop frames)
48+- **Keyframe Interval**: `1`
49+- **x264 Options**: `bframes=0`. If there's a 'bframes' option, you'll want to have that at '0' or unchecked.
50+51+:::caution
52+These last two options are very important! Your viewers' experience may be choppy or otherwise subpar if you don't have them correct.
53+:::
54+55+## Step 4: Go live
56+57+1. In OBS, click **Start Streaming**
58+2. Go back to the Live Dashboard at stream.place
59+3. Fill in your stream title and optionally pick a thumbnail8
60+4. If needed, turn on content warnings. ("Metadata" tab in Stream Settings)
61+5. Click **Announce Livestream**
62+6. Your stream is now live and visible to the world!
63+64+## Next steps
65+66+- **Customize your chat**: Change your name color in Settings > Account
67+- **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
68+- **Improve stream quality**: See the [OBS guide](/docs/guides/start-streaming/obs) for encoder settings and troubleshooting
69+- **Join the Discord!**: If you need any help, or just want to chat, check out our discord at https://discord.stream.place.
70+71+### Credits
72+73+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).
···2title: Welcome to Streamplace!
3description: Begin your development journey with the Streamplace documentation.
4template: doc
5-hero:
6- tagline: Solve live video for your project with Streamplace.
7- image:
8- file: ../../assets/cube.png
9- alt: Streamplace logo. A pink 3d box viewed from a top corner.
10- actions:
11- - text: Get Started
12- link: /docs/guides/start-streaming/obs
13- icon: right-arrow
14- - text: Visit Streamplace
15- link: /
16- icon: external
17- variant: minimal
18---
1920-import { Card, CardGrid } from "@astrojs/starlight/components";
21-22-## Next Steps
2324-<CardGrid>
25- <Card title="Read the Docs" icon="open-book">
26- Learn how to start streaming with
27- [Streamplace](/docs/guides/start-streaming/obs).
28- </Card>
29- <Card title="Install Streamplace" icon="download">
30- [Run your own Streamplace
31- node](/docs/guides/installing/installing-streamplace).
32- </Card>
33- <Card title="API Reference" icon="document">
34- Explore the [Lexicon API reference](/docs/lex-reference/place-stream-defs).
35- </Card>
36- <Card title="Developer Setup" icon="setting">
37- Set up your [development environment](/docs/guides/streamplace-dev-setup).
38- </Card>
39-</CardGrid>
···2title: Welcome to Streamplace!
3description: Begin your development journey with the Streamplace documentation.
4template: doc
00000000000005---
67+import HelpDesk from "../../components/HelpDesk.astro";
0089+<HelpDesk />
000000000000000
···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+```
···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+```
···28- **Description:** Raw blob data with appropriate content-type
29- **Schema:**
3031-_Schema not defined._ **Possible Errors:**
03233- `BrandingNotFound`: The requested branding asset does not exist
34
···28- **Description:** Raw blob data with appropriate content-type
29- **Schema:**
3031+_Schema not defined._
32+**Possible Errors:**
3334- `BrandingNotFound`: The requested branding asset does not exist
35
···1314**Type:** `record`
1516-Record indicating a livestream is published and available for replication at a
17-given address. By convention, the record key is streamer::server
1819**Record Key:** `any`
20
···1314**Type:** `record`
1516+Record indicating a livestream is published and available for replication at a given address. By convention, the record key is streamer::server
01718**Record Key:** `any`
19
···1314**Type:** `record`
1516-Record created by a Streamplace broadcaster to indicate that they will be
17-replicating a livestream. NYI
1819**Record Key:** `tid`
20
···1314**Type:** `record`
1516+Record created by a Streamplace broadcaster to indicate that they will be replicating a livestream. NYI
01718**Record Key:** `tid`
19
···1314**Type:** `query`
1516-Find actor suggestions for a prefix search term. Expected use is for
17-auto-completion during text field entry.
1819**Parameters:**
20
···1314**Type:** `query`
1516+Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry.
01718**Parameters:**
19
···1314**Type:** `record`
1516-Default metadata record for livestream including content warnings, rights, and
17-distribution policy
1819**Record Key:** `literal:self`
20
···1314**Type:** `record`
1516+Default metadata record for livestream including content warnings, rights, and distribution policy
01718**Record Key:** `literal:self`
19
···3334**Type:** `token`
3536-All rights reserved to the creator โ others cannot use, modify, or share without
37-explicit authorization.
3839---
40···4445**Type:** `token`
4647-Public domain dedication. You waive all copyright and related rights where
48-possible. Others may copy, modify, distribute, or perform your work for any
49-purpose without attribution.
5051---
52···5657**Type:** `token`
5859-Attribution required. Others may copy, distribute, remix, and build upon your
60-work, even commercially, if they credit you.
6162---
63···6768**Type:** `token`
6970-Attribution + share-alike. Others may adapt and build upon your work, even
71-commercially, if they credit you and license their new creations under identical
72-terms.
7374---
75···7980**Type:** `token`
8182-Attribution + non-commercial. Others may adapt and build upon your work for
83-non-commercial purposes only, and must credit you.
8485---
86···9091**Type:** `token`
9293-Attribution + non-commercial + share-alike. Others may adapt and build upon your
94-work for non-commercial purposes only, must credit you, and must license their
95-new creations under identical terms.
9697---
98···102103**Type:** `token`
104105-Attribution + no derivatives. Others may reuse your work, even commercially, but
106-it must remain unchanged and you must be credited.
107108---
109···113114**Type:** `token`
115116-Attribution + non-commercial + no derivatives. Others may download and share
117-your work with credit, but cannot change it or use it commercially.
118119---
120
···3334**Type:** `token`
3536+All rights reserved to the creator โ others cannot use, modify, or share without explicit authorization.
03738---
39···4344**Type:** `token`
4546+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.
004748---
49···5354**Type:** `token`
5556+Attribution required. Others may copy, distribute, remix, and build upon your work, even commercially, if they credit you.
05758---
59···6364**Type:** `token`
6566+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.
006768---
69···7374**Type:** `token`
7576+Attribution + non-commercial. Others may adapt and build upon your work for non-commercial purposes only, and must credit you.
07778---
79···8384**Type:** `token`
8586+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.
008788---
89···9394**Type:** `token`
9596+Attribution + no derivatives. Others may reuse your work, even commercially, but it must remain unchanged and you must be credited.
09798---
99···103104**Type:** `token`
105106+Attribution + non-commercial + no derivatives. Others may download and share your work with credit, but cannot change it or use it commercially.
0107108---
109
···2930**Type:** `token`
3132-The content could be perceived as offensive due to the discussion or display of
33-death.
3435---
36···4041**Type:** `token`
4243-The content contains a portrayal of the use or abuse of mind altering
44-substances.
4546---
47···5152**Type:** `token`
5354-The content contains violent actions of a fantasy nature, involving human or
55-non-human characters in situations easily distinguishable from real life.
5657---
58···6263**Type:** `token`
6465-The content contains flashing lights that could be harmful to viewers with
66-seizure disorders such as photosensitive epilepsy.
6768---
69···9394**Type:** `token`
9596-The content contains information that can be used to identify a particular
97-individual, such as a name, phone number, email address, physical address, or IP
98-address.
99100---
101···105106**Type:** `token`
107108-The content could be perceived as offensive due to the discussion or display of
109-sexuality.
110111---
112···116117**Type:** `token`
118119-The content could be perceived as distressing due to the discussion or display
120-of suffering or triggering topics, including suicide, eating disorders or self
121-harm.
122123---
124···128129**Type:** `token`
130131-The content could be perceived as offensive due to the discussion or display of
132-violence.
133134---
135
···2930**Type:** `token`
3132+The content could be perceived as offensive due to the discussion or display of death.
03334---
35···3940**Type:** `token`
4142+The content contains a portrayal of the use or abuse of mind altering substances.
04344---
45···4950**Type:** `token`
5152+The content contains violent actions of a fantasy nature, involving human or non-human characters in situations easily distinguishable from real life.
05354---
55···5960**Type:** `token`
6162+The content contains flashing lights that could be harmful to viewers with seizure disorders such as photosensitive epilepsy.
06364---
65···8990**Type:** `token`
9192+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.
009394---
95···99100**Type:** `token`
101102+The content could be perceived as offensive due to the discussion or display of sexuality.
0103104---
105···109110**Type:** `token`
111112+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.
00113114---
115···119120**Type:** `token`
121122+The content could be perceived as offensive due to the discussion or display of violence.
0123124---
125
···1314**Type:** `procedure`
1516-Create a block (ban) on behalf of a streamer. Requires 'ban' permission. Creates
17-an app.bsky.graph.block record in the streamer's repository.
1819**Parameters:** _(None defined)_
20···46**Possible Errors:**
4748- `Unauthorized`: The request lacks valid authentication credentials.
49-- `Forbidden`: The caller does not have permission to create blocks for this
50- streamer.
51-- `SessionNotFound`: The streamer's OAuth session could not be found or is
52- invalid.
5354---
55
···1314**Type:** `procedure`
1516+Create a block (ban) on behalf of a streamer. Requires 'ban' permission. Creates an app.bsky.graph.block record in the streamer's repository.
01718**Parameters:** _(None defined)_
19···45**Possible Errors:**
4647- `Unauthorized`: The request lacks valid authentication credentials.
48+- `Forbidden`: The caller does not have permission to create blocks for this streamer.
49+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
005051---
52
···1314**Type:** `procedure`
1516-Create a gate (hide message) on behalf of a streamer. Requires 'hide'
17-permission. Creates a place.stream.chat.gate record in the streamer's
18-repository.
1920**Parameters:** _(None defined)_
21···46**Possible Errors:**
4748- `Unauthorized`: The request lacks valid authentication credentials.
49-- `Forbidden`: The caller does not have permission to hide messages for this
50- streamer.
51-- `SessionNotFound`: The streamer's OAuth session could not be found or is
52- invalid.
5354---
55
···1314**Type:** `procedure`
1516+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.
001718**Parameters:** _(None defined)_
19···44**Possible Errors:**
4546- `Unauthorized`: The request lacks valid authentication credentials.
47+- `Forbidden`: The caller does not have permission to hide messages for this streamer.
48+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
004950---
51
···1314**Type:** `procedure`
1516-Delete a block (unban) on behalf of a streamer. Requires 'ban' permission.
17-Deletes an app.bsky.graph.block record from the streamer's repository.
1819**Parameters:** _(None defined)_
20···3738**Schema Type:** `object`
3940-_(No properties defined)_ **Possible Errors:**
04142- `Unauthorized`: The request lacks valid authentication credentials.
43-- `Forbidden`: The caller does not have permission to delete blocks for this
44- streamer.
45-- `SessionNotFound`: The streamer's OAuth session could not be found or is
46- invalid.
4748---
49
···1314**Type:** `procedure`
1516+Delete a block (unban) on behalf of a streamer. Requires 'ban' permission. Deletes an app.bsky.graph.block record from the streamer's repository.
01718**Parameters:** _(None defined)_
19···3637**Schema Type:** `object`
3839+_(No properties defined)_
40+**Possible Errors:**
4142- `Unauthorized`: The request lacks valid authentication credentials.
43+- `Forbidden`: The caller does not have permission to delete blocks for this streamer.
44+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
004546---
47
···1314**Type:** `procedure`
1516-Delete a gate (unhide message) on behalf of a streamer. Requires 'hide'
17-permission. Deletes a place.stream.chat.gate record from the streamer's
18-repository.
1920**Parameters:** _(None defined)_
21···3839**Schema Type:** `object`
4041-_(No properties defined)_ **Possible Errors:**
04243- `Unauthorized`: The request lacks valid authentication credentials.
44-- `Forbidden`: The caller does not have permission to unhide messages for this
45- streamer.
46-- `SessionNotFound`: The streamer's OAuth session could not be found or is
47- invalid.
4849---
50
···1314**Type:** `procedure`
1516+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.
001718**Parameters:** _(None defined)_
19···3637**Schema Type:** `object`
3839+_(No properties defined)_
40+**Possible Errors:**
4142- `Unauthorized`: The request lacks valid authentication credentials.
43+- `Forbidden`: The caller does not have permission to unhide messages for this streamer.
44+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
004546---
47
···1314**Type:** `procedure`
1516-Update livestream metadata on behalf of a streamer. Requires 'livestream.manage'
17-permission. Updates a place.stream.livestream record in the streamer's
18-repository.
1920**Parameters:** _(None defined)_
21···47**Possible Errors:**
4849- `Unauthorized`: The request lacks valid authentication credentials.
50-- `Forbidden`: The caller does not have permission to update livestream metadata
51- for this streamer.
52-- `SessionNotFound`: The streamer's OAuth session could not be found or is
53- invalid.
54- `RecordNotFound`: The specified livestream record does not exist.
5556---
···1314**Type:** `procedure`
1516+Update livestream metadata on behalf of a streamer. Requires 'livestream.manage' permission. Updates a place.stream.livestream record in the streamer's repository.
001718**Parameters:** _(None defined)_
19···45**Possible Errors:**
4647- `Unauthorized`: The request lacks valid authentication credentials.
48+- `Forbidden`: The caller does not have permission to update livestream metadata for this streamer.
49+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
0050- `RecordNotFound`: The specified livestream record does not exist.
5152---
···1956 ]
1957 }
1958 },
000000000000000000000000000000000000000000000000000000000000000000000000001959 "/xrpc/com.atproto.sync.listRepos": {
1960 "get": {
1961 "summary": "Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.",
···1956 ]
1957 }
1958 },
1959+ "/xrpc/com.atproto.sync.getRepo": {
1960+ "get": {
1961+ "summary": "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.",
1962+ "operationId": "com.atproto.sync.getRepo",
1963+ "tags": ["com.atproto.sync"],
1964+ "responses": {
1965+ "200": {
1966+ "description": "Success",
1967+ "content": {
1968+ "application/vnd.ipld.car": {
1969+ "schema": {}
1970+ }
1971+ }
1972+ },
1973+ "400": {
1974+ "description": "Bad Request",
1975+ "content": {
1976+ "application/json": {
1977+ "schema": {
1978+ "type": "object",
1979+ "required": ["error", "message"],
1980+ "properties": {
1981+ "error": {
1982+ "type": "string",
1983+ "oneOf": [
1984+ {
1985+ "const": "RepoNotFound"
1986+ },
1987+ {
1988+ "const": "RepoTakendown"
1989+ },
1990+ {
1991+ "const": "RepoSuspended"
1992+ },
1993+ {
1994+ "const": "RepoDeactivated"
1995+ }
1996+ ]
1997+ },
1998+ "message": {
1999+ "type": "string"
2000+ }
2001+ }
2002+ }
2003+ }
2004+ }
2005+ }
2006+ },
2007+ "parameters": [
2008+ {
2009+ "name": "did",
2010+ "in": "query",
2011+ "required": true,
2012+ "description": "The DID of the repo.",
2013+ "schema": {
2014+ "type": "string",
2015+ "description": "The DID of the repo.",
2016+ "format": "did"
2017+ }
2018+ },
2019+ {
2020+ "name": "since",
2021+ "in": "query",
2022+ "required": false,
2023+ "description": "The revision ('rev') of the repo to create a diff from.",
2024+ "schema": {
2025+ "type": "string",
2026+ "description": "The revision ('rev') of the repo to create a diff from.",
2027+ "format": "tid"
2028+ }
2029+ }
2030+ ]
2031+ }
2032+ },
2033 "/xrpc/com.atproto.sync.listRepos": {
2034 "get": {
2035 "summary": "Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.",
+9
js/docs/src/content/docs/reference.mdx
···000000000
···1+---
2+title: API Reference
3+description: Our XRPC and OpenAPI Reference documentation
4+template: doc
5+---
6+7+import { Card, CardGrid } from "@astrojs/starlight/components";
8+9+Here contains our XRPC and OpenAPI Reference documentation.
···1+{
2+ "lexicon": 1,
3+ "id": "com.atproto.sync.getRepo",
4+ "defs": {
5+ "main": {
6+ "type": "query",
7+ "description": "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.",
8+ "parameters": {
9+ "type": "params",
10+ "required": ["did"],
11+ "properties": {
12+ "did": {
13+ "type": "string",
14+ "format": "did",
15+ "description": "The DID of the repo."
16+ },
17+ "since": {
18+ "type": "string",
19+ "format": "tid",
20+ "description": "The revision ('rev') of the repo to create a diff from."
21+ }
22+ }
23+ },
24+ "output": {
25+ "encoding": "application/vnd.ipld.car"
26+ },
27+ "errors": [
28+ { "name": "RepoNotFound" },
29+ { "name": "RepoTakendown" },
30+ { "name": "RepoSuspended" },
31+ { "name": "RepoDeactivated" }
32+ ]
33+ }
34+ }
35+}
+46
lexicons/place/stream/badge/defs.json
···0000000000000000000000000000000000000000000000
···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
···00000000000000000000000000000000000000000000
···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+}
···25 "deleted": {
26 "type": "boolean",
27 "description": "If true, this message has been deleted or labeled and should be cleared from the cache"
00000000028 }
29 }
30 }
···25 "deleted": {
26 "type": "boolean",
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+ }
37 }
38 }
39 }
···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+}
···56 Build *BuildFlags
57 DataDir string
58 DBURL string
059 EthAccountAddr string
60 EthKeystorePath string
61 EthPassword string
···140 SegmentDebugDir string
141 AdminDIDs []string
142 Syndicate []string
0143}
144145// ContentFilters represents the content filtering configuration
···240 fs.BoolVar(&cli.BehindHTTPSProxy, "behind-https-proxy", false, "set to true if this node is behind an https proxy and we should report https URLs even though the node isn't serving HTTPS")
241 cli.StringSliceFlag(fs, &cli.AdminDIDs, "admin-dids", []string{}, "comma-separated list of DIDs that are authorized to modify branding and other admin operations")
242 cli.StringSliceFlag(fs, &cli.Syndicate, "syndicate", []string{}, "list of DIDs that we should rebroadcast ('*' for everybody)")
000243244 fs.Bool("external-signing", true, "DEPRECATED, does nothing.")
245 fs.Bool("insecure", false, "DEPRECATED, does nothing.")
···56 Build *BuildFlags
57 DataDir string
58 DBURL string
59+ LocalDBURL string
60 EthAccountAddr string
61 EthKeystorePath string
62 EthPassword string
···141 SegmentDebugDir string
142 AdminDIDs []string
143 Syndicate []string
144+ PlayerTelemetry bool
145}
146147// ContentFilters represents the content filtering configuration
···242 fs.BoolVar(&cli.BehindHTTPSProxy, "behind-https-proxy", false, "set to true if this node is behind an https proxy and we should report https URLs even though the node isn't serving HTTPS")
243 cli.StringSliceFlag(fs, &cli.AdminDIDs, "admin-dids", []string{}, "comma-separated list of DIDs that are authorized to modify branding and other admin operations")
244 cli.StringSliceFlag(fs, &cli.Syndicate, "syndicate", []string{}, "list of DIDs that we should rebroadcast ('*' for everybody)")
245+ fs.BoolVar(&cli.PlayerTelemetry, "player-telemetry", true, "enable player telemetry")
246+ fs.StringVar(&cli.LocalDBURL, "local-db-url", "sqlite://$SP_DATA_DIR/localdb.sqlite", "URL of the local database to use for storing local data")
247+ cli.dataDirFlags = append(cli.dataDirFlags, &cli.LocalDBURL)
248249 fs.Bool("external-signing", true, "DEPRECATED, does nothing.")
250 fs.Bool("insecure", false, "DEPRECATED, does nothing.")
···1package model
2-3-import (
4- "context"
5- "database/sql/driver"
6- "encoding/json"
7- "errors"
8- "fmt"
9- "time"
10-11- "gorm.io/gorm"
12- "stream.place/streamplace/pkg/aqtime"
13- "stream.place/streamplace/pkg/log"
14- "stream.place/streamplace/pkg/streamplace"
15-)
16-17-type SegmentMediadataVideo struct {
18- Width int `json:"width"`
19- Height int `json:"height"`
20- FPSNum int `json:"fpsNum"`
21- FPSDen int `json:"fpsDen"`
22- BFrames bool `json:"bframes"`
23-}
24-25-type SegmentMediadataAudio struct {
26- Rate int `json:"rate"`
27- Channels int `json:"channels"`
28-}
29-30-type SegmentMediaData struct {
31- Video []*SegmentMediadataVideo `json:"video"`
32- Audio []*SegmentMediadataAudio `json:"audio"`
33- Duration int64 `json:"duration"`
34- Size int `json:"size"`
35-}
36-37-// Scan scan value into Jsonb, implements sql.Scanner interface
38-func (j *SegmentMediaData) Scan(value any) error {
39- bytes, ok := value.([]byte)
40- if !ok {
41- return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
42- }
43-44- result := SegmentMediaData{}
45- err := json.Unmarshal(bytes, &result)
46- *j = SegmentMediaData(result)
47- return err
48-}
49-50-// Value return json value, implement driver.Valuer interface
51-func (j SegmentMediaData) Value() (driver.Value, error) {
52- return json.Marshal(j)
53-}
54-55-// ContentRights represents content rights and attribution information
56-type ContentRights struct {
57- CopyrightNotice *string `json:"copyrightNotice,omitempty"`
58- CopyrightYear *int64 `json:"copyrightYear,omitempty"`
59- Creator *string `json:"creator,omitempty"`
60- CreditLine *string `json:"creditLine,omitempty"`
61- License *string `json:"license,omitempty"`
62-}
63-64-// Scan scan value into ContentRights, implements sql.Scanner interface
65-func (c *ContentRights) Scan(value any) error {
66- if value == nil {
67- *c = ContentRights{}
68- return nil
69- }
70- bytes, ok := value.([]byte)
71- if !ok {
72- return errors.New(fmt.Sprint("Failed to unmarshal ContentRights value:", value))
73- }
74-75- result := ContentRights{}
76- err := json.Unmarshal(bytes, &result)
77- *c = ContentRights(result)
78- return err
79-}
80-81-// Value return json value, implement driver.Valuer interface
82-func (c ContentRights) Value() (driver.Value, error) {
83- return json.Marshal(c)
84-}
85-86-// DistributionPolicy represents distribution policy information
87-type DistributionPolicy struct {
88- DeleteAfterSeconds *int64 `json:"deleteAfterSeconds,omitempty"`
89-}
90-91-// Scan scan value into DistributionPolicy, implements sql.Scanner interface
92-func (d *DistributionPolicy) Scan(value any) error {
93- if value == nil {
94- *d = DistributionPolicy{}
95- return nil
96- }
97- bytes, ok := value.([]byte)
98- if !ok {
99- return errors.New(fmt.Sprint("Failed to unmarshal DistributionPolicy value:", value))
100- }
101-102- result := DistributionPolicy{}
103- err := json.Unmarshal(bytes, &result)
104- *d = DistributionPolicy(result)
105- return err
106-}
107-108-// Value return json value, implement driver.Valuer interface
109-func (d DistributionPolicy) Value() (driver.Value, error) {
110- return json.Marshal(d)
111-}
112-113-// ContentWarningsSlice is a custom type for storing content warnings as JSON in the database
114-type ContentWarningsSlice []string
115-116-// Scan scan value into ContentWarningsSlice, implements sql.Scanner interface
117-func (c *ContentWarningsSlice) Scan(value any) error {
118- if value == nil {
119- *c = ContentWarningsSlice{}
120- return nil
121- }
122- bytes, ok := value.([]byte)
123- if !ok {
124- return errors.New(fmt.Sprint("Failed to unmarshal ContentWarningsSlice value:", value))
125- }
126-127- result := ContentWarningsSlice{}
128- err := json.Unmarshal(bytes, &result)
129- *c = ContentWarningsSlice(result)
130- return err
131-}
132-133-// Value return json value, implement driver.Valuer interface
134-func (c ContentWarningsSlice) Value() (driver.Value, error) {
135- return json.Marshal(c)
136-}
137-138-type Segment struct {
139- ID string `json:"id" gorm:"primaryKey"`
140- SigningKeyDID string `json:"signingKeyDID" gorm:"column:signing_key_did"`
141- SigningKey *SigningKey `json:"signingKey,omitempty" gorm:"foreignKey:DID;references:SigningKeyDID"`
142- StartTime time.Time `json:"startTime" gorm:"index:latest_segments,priority:2;index:start_time"`
143- RepoDID string `json:"repoDID" gorm:"index:latest_segments,priority:1;column:repo_did"`
144- Repo *Repo `json:"repo,omitempty" gorm:"foreignKey:DID;references:RepoDID"`
145- Title string `json:"title"`
146- Size int `json:"size" gorm:"column:size"`
147- MediaData *SegmentMediaData `json:"mediaData,omitempty"`
148- ContentWarnings ContentWarningsSlice `json:"contentWarnings,omitempty"`
149- ContentRights *ContentRights `json:"contentRights,omitempty"`
150- DistributionPolicy *DistributionPolicy `json:"distributionPolicy,omitempty"`
151- DeleteAfter *time.Time `json:"deleteAfter,omitempty" gorm:"column:delete_after;index:delete_after"`
152-}
153-154-func (s *Segment) ToStreamplaceSegment() (*streamplace.Segment, error) {
155- aqt := aqtime.FromTime(s.StartTime)
156- if s.MediaData == nil {
157- return nil, fmt.Errorf("media data is nil")
158- }
159- if len(s.MediaData.Video) == 0 || s.MediaData.Video[0] == nil {
160- return nil, fmt.Errorf("video data is nil")
161- }
162- if len(s.MediaData.Audio) == 0 || s.MediaData.Audio[0] == nil {
163- return nil, fmt.Errorf("audio data is nil")
164- }
165- duration := s.MediaData.Duration
166- sizei64 := int64(s.Size)
167-168- // Convert model metadata to streamplace metadata
169- var contentRights *streamplace.MetadataContentRights
170- if s.ContentRights != nil {
171- contentRights = &streamplace.MetadataContentRights{
172- CopyrightNotice: s.ContentRights.CopyrightNotice,
173- CopyrightYear: s.ContentRights.CopyrightYear,
174- Creator: s.ContentRights.Creator,
175- CreditLine: s.ContentRights.CreditLine,
176- License: s.ContentRights.License,
177- }
178- }
179-180- var contentWarnings *streamplace.MetadataContentWarnings
181- if len(s.ContentWarnings) > 0 {
182- contentWarnings = &streamplace.MetadataContentWarnings{
183- Warnings: []string(s.ContentWarnings),
184- }
185- }
186-187- var distributionPolicy *streamplace.MetadataDistributionPolicy
188- if s.DistributionPolicy != nil && s.DistributionPolicy.DeleteAfterSeconds != nil {
189- distributionPolicy = &streamplace.MetadataDistributionPolicy{
190- DeleteAfter: s.DistributionPolicy.DeleteAfterSeconds,
191- }
192- }
193-194- return &streamplace.Segment{
195- LexiconTypeID: "place.stream.segment",
196- Creator: s.RepoDID,
197- Id: s.ID,
198- SigningKey: s.SigningKeyDID,
199- StartTime: string(aqt),
200- Duration: &duration,
201- Size: &sizei64,
202- ContentRights: contentRights,
203- ContentWarnings: contentWarnings,
204- DistributionPolicy: distributionPolicy,
205- Video: []*streamplace.Segment_Video{
206- {
207- Codec: "h264",
208- Width: int64(s.MediaData.Video[0].Width),
209- Height: int64(s.MediaData.Video[0].Height),
210- Framerate: &streamplace.Segment_Framerate{
211- Num: int64(s.MediaData.Video[0].FPSNum),
212- Den: int64(s.MediaData.Video[0].FPSDen),
213- },
214- Bframes: &s.MediaData.Video[0].BFrames,
215- },
216- },
217- Audio: []*streamplace.Segment_Audio{
218- {
219- Codec: "opus",
220- Rate: int64(s.MediaData.Audio[0].Rate),
221- Channels: int64(s.MediaData.Audio[0].Channels),
222- },
223- },
224- }, nil
225-}
226-227-func (m *DBModel) CreateSegment(seg *Segment) error {
228- err := m.DB.Model(Segment{}).Create(seg).Error
229- if err != nil {
230- return err
231- }
232- return nil
233-}
234-235-// should return the most recent segment for each user, ordered by most recent first
236-// only includes segments from the last 30 seconds
237-func (m *DBModel) MostRecentSegments() ([]Segment, error) {
238- var segments []Segment
239- thirtySecondsAgo := time.Now().Add(-30 * time.Second)
240-241- err := m.DB.Table("segments").
242- Select("segments.*").
243- Where("start_time > ?", thirtySecondsAgo.UTC()).
244- Order("start_time DESC").
245- Find(&segments).Error
246- if err != nil {
247- return nil, err
248- }
249- if segments == nil {
250- return []Segment{}, nil
251- }
252-253- segmentMap := make(map[string]Segment)
254- for _, seg := range segments {
255- prev, ok := segmentMap[seg.RepoDID]
256- if !ok {
257- segmentMap[seg.RepoDID] = seg
258- } else {
259- if seg.StartTime.After(prev.StartTime) {
260- segmentMap[seg.RepoDID] = seg
261- }
262- }
263- }
264-265- filteredSegments := []Segment{}
266- for _, seg := range segmentMap {
267- filteredSegments = append(filteredSegments, seg)
268- }
269-270- return filteredSegments, nil
271-}
272-273-func (m *DBModel) LatestSegmentForUser(user string) (*Segment, error) {
274- var seg Segment
275- err := m.DB.Model(Segment{}).Where("repo_did = ?", user).Order("start_time DESC").First(&seg).Error
276- if err != nil {
277- return nil, err
278- }
279- return &seg, nil
280-}
281-282-func (m *DBModel) FilterLiveRepoDIDs(repoDIDs []string) ([]string, error) {
283- if len(repoDIDs) == 0 {
284- return []string{}, nil
285- }
286-287- thirtySecondsAgo := time.Now().Add(-30 * time.Second)
288-289- var liveDIDs []string
290-291- err := m.DB.Table("segments").
292- Select("DISTINCT repo_did").
293- Where("repo_did IN ? AND start_time > ?", repoDIDs, thirtySecondsAgo.UTC()).
294- Pluck("repo_did", &liveDIDs).Error
295-296- if err != nil {
297- return nil, err
298- }
299-300- return liveDIDs, nil
301-}
302-303-func (m *DBModel) LatestSegmentsForUser(user string, limit int, before *time.Time, after *time.Time) ([]Segment, error) {
304- var segs []Segment
305- if before == nil {
306- later := time.Now().Add(1000 * time.Hour)
307- before = &later
308- }
309- if after == nil {
310- earlier := time.Time{}
311- after = &earlier
312- }
313- err := m.DB.Model(Segment{}).Where("repo_did = ? AND start_time < ? AND start_time > ?", user, before.UTC(), after.UTC()).Order("start_time DESC").Limit(limit).Find(&segs).Error
314- if err != nil {
315- return nil, err
316- }
317- return segs, nil
318-}
319-320-func (m *DBModel) GetSegment(id string) (*Segment, error) {
321- var seg Segment
322-323- err := m.DB.Model(&Segment{}).
324- Preload("Repo").
325- Where("id = ?", id).
326- First(&seg).Error
327-328- if errors.Is(err, gorm.ErrRecordNotFound) {
329- return nil, nil
330- }
331- if err != nil {
332- return nil, err
333- }
334-335- return &seg, nil
336-}
337-338-func (m *DBModel) GetExpiredSegments(ctx context.Context) ([]Segment, error) {
339-340- var expiredSegments []Segment
341- now := time.Now()
342- err := m.DB.
343- Where("delete_after IS NOT NULL AND delete_after < ?", now.UTC()).
344- Find(&expiredSegments).Error
345- if err != nil {
346- return nil, err
347- }
348-349- return expiredSegments, nil
350-}
351-352-func (m *DBModel) DeleteSegment(ctx context.Context, id string) error {
353- return m.DB.Delete(&Segment{}, "id = ?", id).Error
354-}
355-356-func (m *DBModel) StartSegmentCleaner(ctx context.Context) error {
357- err := m.SegmentCleaner(ctx)
358- if err != nil {
359- return err
360- }
361- ticker := time.NewTicker(1 * time.Minute)
362- defer ticker.Stop()
363-364- for {
365- select {
366- case <-ctx.Done():
367- return nil
368- case <-ticker.C:
369- err := m.SegmentCleaner(ctx)
370- if err != nil {
371- log.Error(ctx, "Failed to clean segments", "error", err)
372- }
373- }
374- }
375-}
376-377-func (m *DBModel) SegmentCleaner(ctx context.Context) error {
378- // Calculate the cutoff time (10 minutes ago)
379- cutoffTime := aqtime.FromTime(time.Now().Add(-10 * time.Minute)).Time()
380-381- // Find all unique repo_did values
382- var repoDIDs []string
383- if err := m.DB.Model(&Segment{}).Distinct("repo_did").Pluck("repo_did", &repoDIDs).Error; err != nil {
384- log.Error(ctx, "Failed to get unique repo_dids for segment cleaning", "error", err)
385- return err
386- }
387-388- // For each user, keep their last 10 segments and delete older ones
389- for _, repoDID := range repoDIDs {
390- // Get IDs of the last 10 segments for this user
391- var keepSegmentIDs []string
392- if err := m.DB.Model(&Segment{}).
393- Where("repo_did = ?", repoDID).
394- Order("start_time DESC").
395- Limit(10).
396- Pluck("id", &keepSegmentIDs).Error; err != nil {
397- log.Error(ctx, "Failed to get segment IDs to keep", "repo_did", repoDID, "error", err)
398- return err
399- }
400-401- // Delete old segments except the ones we want to keep
402- result := m.DB.Where("repo_did = ? AND start_time < ? AND id NOT IN ?",
403- repoDID, cutoffTime, keepSegmentIDs).Delete(&Segment{})
404-405- if result.Error != nil {
406- log.Error(ctx, "Failed to clean old segments", "repo_did", repoDID, "error", result.Error)
407- } else if result.RowsAffected > 0 {
408- log.Log(ctx, "Cleaned old segments", "repo_did", repoDID, "count", result.RowsAffected)
409- }
410- }
411- return nil
412-}
···1package model
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
···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
···0000000000000000000000000000
···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
···0000000000000000000000
···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+}
···59665967 return nil
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
···16type ChatDefs_MessageView struct {
17 LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.chat.defs#messageView"`
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"`
0021 // deleted: If true, this message has been deleted or labeled and should be cleared from the cache
22 Deleted *bool `json:"deleted,omitempty" cborgen:"deleted,omitempty"`
23 IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
···16type ChatDefs_MessageView struct {
17 LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.chat.defs#messageView"`
18 Author *appbsky.ActorDefs_ProfileViewBasic `json:"author" cborgen:"author"`
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"`
23 // deleted: If true, this message has been deleted or labeled and should be cleared from the cache
24 Deleted *bool `json:"deleted,omitempty" cborgen:"deleted,omitempty"`
25 IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`