···11+import { Platform } from "react-native";
22+33+export default function clearQueryParams(par = ["iss", "state", "code"]) {
44+ if (Platform.OS !== "web") {
55+ return;
66+ }
77+ const u = new URL(document.location.href);
88+ const params = new URLSearchParams(u.search);
99+ if (u.search === "") {
1010+ return;
1111+ }
1212+ par.forEach((p) => params.delete(p));
1313+ u.search = params.toString();
1414+ window.history.replaceState(null, "", u.toString());
1515+}
+3-6
js/atproto-oauth-client-react-native/README.md
···8787forwarded the port with `adb reverse`. For testing on iOS hardware, you'll
8888instead need to set up TLS.
89899090-[react-native-quick-crypto]:
9191- https://github.com/margelo/react-native-quick-crypto
9090+[react-native-quick-crypto]: https://github.com/margelo/react-native-quick-crypto
9291[expo-sqlite]: https://docs.expo.dev/versions/latest/sdk/sqlite/
9393-[README]:
9494- https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser
9595-[example]:
9696- https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example
9292+[README]: https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser
9393+[example]: https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example
···11+censored-text-hide = Hide Text
22+censored-text-reveal = Reveal Text
33+censored-text-blocked-with-reasons = This text was blocked because of these reasons: { $reasons }
44+censored-text-blocked-unknown = This text was blocked for an unknown reason
55+66+category-discriminatory = Discriminatory content
77+category-sexually-explicit = Sexually explicit content
88+category-profanity = Profanity
+13-1
js/components/locales/en-US/common.ftl
···5151 [streamer] Looks like <1>@{ $handle } is offline</1>, but they recommend checking out:
5252 *[default] Looks like <1>@{ $handle } is offline</1>, but we recommend checking out:
5353}
5454-user-offline-no-recommendations =
5454+user-offline-no-recommendations =
5555 Looks like <1>@{ $handle } is offline</1> right now.
5656 Check back later.
5757streaming-title = streaming { $title }
···6060 [1] 1 viewer
6161 *[other] { $count } viewers
6262}
6363+6464+## PDS Host Selector
6565+pds-selector-title = New to the Atmosphere?
6666+pds-selector-description = You'll need to select a PDS (Personal Data Server) to access apps on the Atmosphere, such as Bluesky, Tangled, and Spark.
6767+pds-selector-custom-label = Another PDS
6868+pds-selector-custom-description = Enter your own PDS host URL
6969+pds-selector-custom-url-label = Custom PDS URL
7070+pds-selector-custom-url-placeholder = https://pds.example.com
7171+pds-selector-learn-more = Learn more about self-hosting
7272+pds-selector-info = Each host has their own policies and reliability standards. Your ATProto data lives on the host you choose and you can migrate later. Note: Streamplace has its own moderation rules - you can be banned from Streamplace regardless of which host you choose.
7373+pds-selector-read-policies = Read { $label }'s <tosLink>Terms of Service</tosLink> and <privacyLink>Privacy Policy</privacyLink> before continuing.
7474+pds-selector-handle-policy-checkbox = I have read and agree to the <policyLink>handle policy</policyLink>
+2-1
js/components/locales/en-US/settings.ftl
···121121active = Active
122122123123## Multistreaming
124124-multistreaming = Multistreaming
124124+multistream = Multistreaming
125125multistream-targets = Multistream Targets
126126multistream-description = Automatically push your Streamplace livestreams to other streaming services like Twitch or YouTube.
127127create-multistream-target = Create Multistream Target
···168168no-languages-found = No languages found
169169170170## Branding Administration
171171+branding = Branding
171172branding-admin = Branding Administration
172173branding-admin-description = Customize your Streamplace instance. Note that settings may take a few hours to propagate.
173174branding-login-required = Please log in to manage branding
+8
js/components/locales/es-ES/chat.ftl
···11+censored-text-hide = Ocultar texto
22+censored-text-reveal = Revelar texto
33+censored-text-blocked-with-reasons = Este texto fue bloqueado por estas razones: { $reasons }
44+censored-text-blocked-unknown = Este texto fue bloqueado por un motivo desconocido
55+66+category-discriminatory = Contenido discriminatorio
77+category-sexually-explicit = Contenido sexualmente explícito
88+category-profanity = Blasfemia
+8
js/components/locales/fr-FR/chat.ftl
···11+censored-text-hide = Masquer le texte
22+censored-text-reveal = Révéler le texte
33+censored-text-blocked-with-reasons = Ce texte a été bloqué pour ces raisons : { $reasons }
44+censored-text-blocked-unknown = Ce texte a été bloqué pour une raison inconnue
55+66+category-discriminatory = Contenu discriminatoire
77+category-sexually-explicit = Contenu sexuellement explicite
88+category-profanity = Blasphème
+8
js/components/locales/pt-BR/chat.ftl
···11+censored-text-hide = Ocultar texto
22+censored-text-reveal = Revelar texto
33+censored-text-blocked-with-reasons = Este texto foi bloqueado por estes motivos: { $reasons }
44+censored-text-blocked-unknown = Este texto foi bloqueado por um motivo desconhecido
55+66+category-discriminatory = Conteúdo discriminatório
77+category-sexually-explicit = Conteúdo sexualmente explícito
88+category-profanity = Profanidade
···55export * from "./primitives/text";
6677// Export styled components
88+export * from "./admonition";
89export * from "./button";
910export * from "./checkbox";
1011export * from "./dialog";
···1516export * from "./input";
1617export * from "./loader";
1718export * from "./menu";
1919+export * from "./portal";
1820export * from "./resizeable";
1921export * from "./slider";
2022export * from "./text";
···55import * as React from "react";
66import { Platform, TextInput, type TextInputProps } from "react-native";
77import { bg, borders, flex, p, text } from "../../lib/theme/atoms";
88+import { useTheme } from "../../ui";
89910const Textarea = React.forwardRef<TextInput, TextInputProps>(
1011 ({ style, multiline = true, numberOfLines = 4, ...props }, ref) => {
1212+ let th = useTheme();
1113 // Detect if we're inside a bottom sheet
1214 let isInBottomSheet = false;
1315 try {
···3840 { borderRadius: 10 },
3941 style,
4042 ]}
4343+ autoComplete={props.autoComplete || "off"}
4444+ textContentType={props.textContentType || "none"}
4145 multiline={multiline}
4246 numberOfLines={numberOfLines}
4347 textAlignVertical="top"
4848+ placeholderTextColor={th.theme.colors.textMuted}
4449 {...props}
4550 />
4651 );
+1
js/components/src/hooks/index.ts
···11// barrel file :)
22+export * from "./useAQState";
23export * from "./useAvatars";
34export * from "./useCameraToggle";
45export * from "./useDocumentTitle";
···22// Metro will use this file for React Native builds
3344// Import all translations directly so they're bundled into the app
55+import enUSChat from "../../public/locales/en-US/chat.json";
56import enUSCommon from "../../public/locales/en-US/common.json";
67import enUSSettings from "../../public/locales/en-US/settings.json";
88+import esESChat from "../../public/locales/es-ES/chat.json";
79import esESCommon from "../../public/locales/es-ES/common.json";
810import esESSettings from "../../public/locales/es-ES/settings.json";
1111+import frFRChat from "../../public/locales/fr-FR/chat.json";
912import frFRCommon from "../../public/locales/fr-FR/common.json";
1013import frFRSettings from "../../public/locales/fr-FR/settings.json";
1414+import ptBRChat from "../../public/locales/pt-BR/chat.json";
1115import ptBRCommon from "../../public/locales/pt-BR/common.json";
1216import ptBRSettings from "../../public/locales/pt-BR/settings.json";
1717+import zhHantChat from "../../public/locales/zh-Hant/chat.json";
1318import zhHantCommon from "../../public/locales/zh-Hant/common.json";
1419import zhHantSettings from "../../public/locales/zh-Hant/settings.json";
15201621const translationMap: Record<string, any> = {
2222+ "en-US/chat": enUSChat,
1723 "en-US/common": enUSCommon,
1824 "en-US/settings": enUSSettings,
2525+ "pt-BR/chat": ptBRChat,
1926 "pt-BR/common": ptBRCommon,
2027 "pt-BR/settings": ptBRSettings,
2828+ "es-ES/chat": esESChat,
2129 "es-ES/common": esESCommon,
2230 "es-ES/settings": esESSettings,
3131+ "zh-Hant/chat": zhHantChat,
2332 "zh-Hant/common": zhHantCommon,
2433 "zh-Hant/settings": zhHantSettings,
3434+ "fr-FR/chat": frFRChat,
2535 "fr-FR/common": frFRCommon,
2636 "fr-FR/settings": frFRSettings,
2737};
+1-1
js/components/src/i18n/i18next-config.ts
···116116117117export const I18NEXT_CONFIG = {
118118 lng: LOCALE,
119119- ns: ["common", "settings"], // Common should be first as it's most frequently used
119119+ ns: ["common", "settings", "chat"], // Common should be first as it's most frequently used
120120 defaultNS: "common",
121121 interpolation: {
122122 escapeValue: false, // React already safes from XSS
+6
js/components/src/index.tsx
···33333434export * from "./components/chat/chat";
3535export * from "./components/chat/chat-box";
3636+export * from "./components/chat/chat-settings";
3637export * from "./components/chat/system-message";
3838+export * from "./components/chat/update-stream-title-dialog";
3739export { default as VideoRetry } from "./components/mobile-player/video-retry";
3840export * from "./lib/system-messages";
39414242+export * from "./components/stream-notification";
4343+export * from "./lib/stream-notifications";
4444+4545+export * from "./utils/did";
4046export * from "./utils/format-handle";
41474248export { DanmuOverlay } from "./components/danmu/danmu-overlay";
···77 PlaceStreamChatMessage,
88 PlaceStreamDefs,
99 PlaceStreamLivestream,
1010- PlaceStreamModerationPermission,
1010+ PlaceStreamLiveTeleport,
1111 PlaceStreamSegment,
1212} from "streamplace";
1313import { SystemMessages } from "../lib/system-messages";
1414+import { formatHandleWithAt } from "../utils/format-handle";
1415import { reduceChat } from "./chat";
1516import { LivestreamState } from "./livestream-state";
1617import { findProblems } from "./problems";
···121122 pendingHides: newPendingHides,
122123 };
123124 state = reduceChat(state, [], [], [hiddenMessageUri]);
124124- } else if (
125125- PlaceStreamModerationPermission.isRecord(message) ||
126126- (message &&
127127- typeof message === "object" &&
128128- "$type" in message &&
129129- (message as { $type?: string }).$type ===
130130- "place.stream.moderation.permission")
131131- ) {
132132- // Handle moderation permission record updates
133133- // This can be a new permission or a deletion marker
134134- const permRecord = message as
135135- | PlaceStreamModerationPermission.Record
136136- | { deleted?: boolean; rkey?: string; streamer?: string };
125125+ } else if (PlaceStreamLiveTeleport.isRecord(message)) {
126126+ const teleportRecord = message as PlaceStreamLiveTeleport.Record;
127127+ state = {
128128+ ...state,
129129+ activeTeleport: teleportRecord,
130130+ };
131131+ } else if (PlaceStreamLivestream.isTeleportArrival(message)) {
132132+ // teleport has succeeded, we are now at the target stream
133133+ const arrival = message as PlaceStreamLivestream.TeleportArrival;
137134138138- if ((permRecord as any).deleted) {
139139- // Handle deletion: clear permissions to trigger refetch
140140- // The useCanModerate hook will refetch and repopulate
135135+ // add the teleporter's chat profile to the authors cache FIRST so mention rendering works
136136+ if (arrival.chatProfile && arrival.source.did) {
141137 state = {
142138 ...state,
143143- moderationPermissions: [],
139139+ authors: {
140140+ ...state.authors,
141141+ [arrival.source.did]: arrival.chatProfile,
142142+ },
144143 };
145145- } else {
146146- // Handle new/updated permission: add or update in the list
147147- // Use createdAt as a unique identifier since multiple records can exist for the same moderator
148148- // (e.g., one record with "ban" permission, another with "hide" permission)
149149- // Note: rkey would be ideal but isn't always present in the WebSocket message
150150- const newPerm =
151151- permRecord as PlaceStreamModerationPermission.Record & {
152152- rkey?: string;
153153- };
154154- const existingIndex = state.moderationPermissions.findIndex((p) => {
155155- const pWithRkey = p as PlaceStreamModerationPermission.Record & {
156156- rkey?: string;
157157- };
158158- // Prefer matching by rkey if available, fall back to createdAt
159159- if (newPerm.rkey && pWithRkey.rkey) {
160160- return pWithRkey.rkey === newPerm.rkey;
161161- }
162162- return (
163163- p.moderator === newPerm.moderator &&
164164- p.createdAt === newPerm.createdAt
165165- );
166166- });
144144+ }
167145168168- let newPermissions: PlaceStreamModerationPermission.Record[];
169169- if (existingIndex >= 0) {
170170- // Update existing record with same moderator AND createdAt
171171- newPermissions = [...state.moderationPermissions];
172172- newPermissions[existingIndex] = newPerm;
173173- } else {
174174- // Add new record (could be a new record for an existing moderator with different permissions)
175175- newPermissions = [...state.moderationPermissions, newPerm];
176176- }
146146+ const systemMessage = SystemMessages.teleportArrival(
147147+ formatHandleWithAt(arrival.source),
148148+ arrival.source.did,
149149+ arrival.viewerCount,
150150+ arrival.chatProfile,
151151+ );
152152+ // set proper times
153153+ systemMessage.indexedAt = arrival.startsAt;
154154+ systemMessage.record.createdAt = arrival.startsAt;
177155178178- state = {
179179- ...state,
180180- moderationPermissions: newPermissions,
181181- };
182182- }
156156+ state = reduceChat(state, [systemMessage], []);
157157+ } else if (PlaceStreamLivestream.isTeleportCanceled(message)) {
158158+ // teleport was canceled (deleted or denied)
159159+ state = {
160160+ ...state,
161161+ activeTeleport: null,
162162+ activeTeleportUri: null,
163163+ };
183164 }
184165 }
185166 }
+3
js/components/src/player-store/player-state.tsx
···6363 ingestAutoStart?: boolean;
6464 setIngestAutoStart?: (autoStart: boolean) => void;
65656666+ /** stop ingest process, again with a slight delay to allow UI to update */
6767+ stopIngest: () => void;
6868+6669 /** Timestamp (number) when ingest started, or null if not started */
6770 ingestStarted: number | null;
6871
···11+export interface DIDDocument {
22+ id: string;
33+ service?: Array<{
44+ id: string;
55+ type?: string;
66+ serviceEndpoint?: string;
77+ }>;
88+ [key: string]: any;
99+}
1010+1111+export async function resolveDIDDocument(did: string): Promise<DIDDocument> {
1212+ let didDocUrl: string;
1313+1414+ if (did.startsWith("did:web:")) {
1515+ // For did:web, construct the URL directly
1616+ const domain = did.replace("did:web:", "").replace(/:/g, "/");
1717+ didDocUrl = `https://${domain}/.well-known/did.json`;
1818+ } else if (did.startsWith("did:plc:")) {
1919+ // For did:plc, use plc.directory
2020+ didDocUrl = `https://plc.directory/${did}`;
2121+ } else {
2222+ throw new Error(`Unsupported DID method: ${did}`);
2323+ }
2424+2525+ const response = await fetch(didDocUrl);
2626+ if (!response.ok) {
2727+ throw new Error(
2828+ `Failed to resolve DID document for ${did}: ${response.status}`,
2929+ );
3030+ }
3131+3232+ return response.json();
3333+}
3434+3535+export function getPDSServiceEndpoint(didDoc: DIDDocument): string {
3636+ const pdsService = didDoc.service?.find((s) => s.id === "#atproto_pds");
3737+3838+ if (!pdsService?.serviceEndpoint) {
3939+ throw new Error("No PDS service endpoint found in DID document");
4040+ }
4141+4242+ return pdsService.serviceEndpoint;
4343+}
4444+4545+export async function getBlob(
4646+ did: string,
4747+ cid: string,
4848+ didDoc?: DIDDocument,
4949+): Promise<Blob> {
5050+ const doc = didDoc || (await resolveDIDDocument(did));
5151+ const pdsEndpoint = getPDSServiceEndpoint(doc);
5252+5353+ const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`;
5454+5555+ const response = await fetch(blobUrl);
5656+ if (!response.ok) {
5757+ throw new Error(`Failed to fetch blob: ${response.status}`);
5858+ }
5959+6060+ return response.blob();
6161+}
-59
js/docs/README.md
···11-# Starlight Starter Kit: Basics
22-33-[](https://starlight.astro.build)
44-55-```
66-pnpm create astro@latest -- --template starlight
77-```
88-99-[](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
1010-[](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
1111-[](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
1212-[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
1313-1414-> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
1515-1616-## 🚀 Project Structure
1717-1818-Inside of your Astro + Starlight project, you'll see the following folders and
1919-files:
2020-2121-```
2222-.
2323-├── public/
2424-├── src/
2525-│ ├── assets/
2626-│ ├── content/
2727-│ │ ├── docs/
2828-│ └── content.config.ts
2929-├── astro.config.mjs
3030-├── package.json
3131-└── tsconfig.json
3232-```
3333-3434-Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory.
3535-Each file is exposed as a route based on its file name.
3636-3737-Images can be added to `src/assets/` and embedded in Markdown with a relative
3838-link.
3939-4040-Static assets, like favicons, can be placed in the `public/` directory.
4141-4242-## 🧞 Commands
4343-4444-All commands are run from the root of the project, from a terminal:
4545-4646-| Command | Action |
4747-| :--------------------- | :----------------------------------------------- |
4848-| `pnpm install` | Installs dependencies |
4949-| `pnpm dev` | Starts local dev server at `localhost:4321` |
5050-| `pnpm build` | Build your production site to `./dist/` |
5151-| `pnpm preview` | Preview your build locally, before deploying |
5252-| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
5353-| `pnpm astro -- --help` | Get help using the Astro CLI |
5454-5555-## 👀 Want to learn more?
5656-5757-Check out [Starlight’s docs](https://starlight.astro.build/), read
5858-[the Astro documentation](https://docs.astro.build), or jump into the
5959-[Astro Discord server](https://astro.build/chat).
···11+---
22+import { Card, CardGrid } from "@astrojs/starlight/components";
33+44+interface Props {
55+ searchPlaceholder?: string;
66+}
77+---
88+99+<div class="helpdesk">
1010+1111+ <h2>How can we help?</h2>
1212+ <p>Search the knowledge base, or check out topics below.</p>
1313+1414+ <CardGrid>
1515+ <Card title="Getting Started" icon="rocket">
1616+ <p>New to Streamplace? Start here to set up your first stream.</p>
1717+ <ul>
1818+ <li><a href="/docs/guides/start-streaming/quick-start">Quick start guide</a></li>
1919+ <li><a href="/docs/guides/start-streaming/obs">Stream with OBS</a></li>
2020+ </ul>
2121+ </Card>
2222+2323+ <Card title="Developers & Self-Hosters" icon="laptop">
2424+ <p>Building with Streamplace or running your own node?</p>
2525+ <ul>
2626+ <li><a href="/docs/developers">Developer documentation</a></li>
2727+ </ul>
2828+ </Card>
2929+ </CardGrid>
3030+</div>
3131+3232+<style>
3333+ .helpdesk {
3434+ margin: 0 auto;
3535+ }
3636+3737+ .helpdesk-search {
3838+ margin-bottom: 2rem;
3939+ }
4040+4141+ .search-input {
4242+ width: 100%;
4343+ padding: 1rem 1.5rem;
4444+ font-size: 1.125rem;
4545+ border: 2px solid var(--sl-color-gray-5);
4646+ border-radius: 0.5rem;
4747+ background: var(--sl-color-bg);
4848+ color: var(--sl-color-text);
4949+ transition: border-color 0.2s;
5050+ }
5151+5252+ .search-input:focus {
5353+ outline: none;
5454+ border-color: var(--sl-color-accent);
5555+ }
5656+5757+ .helpdesk h2 {
5858+ margin-bottom: 1.5rem;
5959+ }
6060+</style>
+1-2
js/docs/src/content/docs/components/custom_ui.md
···11---
22title: Creating your own player UI
33-description:
44- How to set up your player UI with components from @streamplace/components.
33+description: How to set up your player UI with components from @streamplace/components.
54---
6576# Building a Custom Player UI
+40
js/docs/src/content/docs/developers.mdx
···11+---
22+title: Developers & Self-Hosters
33+description: Build with Streamplace or run your own infrastructure.
44+template: doc
55+---
66+77+import { Card, CardGrid } from "@astrojs/starlight/components";
88+99+## Learn how to deploy, or contribute to Streamplace.
1010+1111+<br />
1212+1313+<CardGrid stagger>
1414+ <Card title="Building an Application" icon="laptop">
1515+ Integrate live video into your project. - [API
1616+ reference](/docs/lex-reference/place-stream-defs) - [Our component
1717+ library](/docs/components/custom_ui/)
1818+ </Card>
1919+2020+{" "}
2121+2222+<Card title="Self-Hosting" icon="seti:config">
2323+ Run your own Streamplace infrastructure. - [Installation
2424+ guide](/docs/guides/installing/installing-streamplace)
2525+</Card>
2626+2727+{" "}
2828+2929+<Card title="Contributing" icon="github">
3030+ Help improve Streamplace. - [Development
3131+ setup](/docs/guides/streamplace-dev-setup) - [Video
3232+ signing](/docs/video-metadata/intro/)
3333+</Card>
3434+3535+ <Card title="Support & Community" icon="information">
3636+ Get help and connect with other developers. - [GitHub
3737+ issues](https://github.com/streamplace/streamplace/issues) - [Discord
3838+ community](https://discord.stream.place)
3939+ </Card>
4040+</CardGrid>
+3-1
js/docs/src/content/docs/features/danmu.md
···33description: Add flying bullet-style chat comments to the player, or your stream
44---
5566-:::note This feature is experimental and may change in future releases. :::
66+:::note
77+This feature is experimental and may change in future releases.
88+:::
79810[Danmu (or Danmaku)](https://en.wikipedia.org/wiki/Danmaku_subtitling) (弹幕,
911"bullet curtain") is a comment style where messages fly across the video
+27
js/docs/src/content/docs/features/embed.md
···11+---
22+title: Embedding your livestream
33+description: How to embed your livestream on your website, blog, etc.
44+---
55+66+Streamplace provides an easy way to embed your livestream on any website or
77+blog.
88+99+You can access the embedded livestream page by putting `/embed` in the URL of
1010+your livestream. For example, if your livestream URL is
1111+`https://stream.place/iame.li`, the embed URL will be
1212+`https://stream.place/embed/iame.li`.
1313+1414+You can use the following HTML snippet to embed your livestream:
1515+1616+```html
1717+<iframe
1818+ src="https://stream.place/embed/your-handle"
1919+ width="560"
2020+ height="315"
2121+ frameborder="0"
2222+ allowfullscreen
2323+></iframe>
2424+```
2525+2626+Alternatively, you can use the share sheet located on your livestream page.
2727+Click the "Share" button, and you'll find the embed code ready to copy.
···11+---
22+title: Multistreaming
33+description: Forward your Streamplace stream to other providers.
44+---
55+66+:::note
77+This guide isn't about setting up Streamplace as an OBS destination. See [OBS Multistreaming to Streamplace](/docs/guides/start-streaming/obs-multistreaming/) for information on that.
88+:::
99+1010+Multistreaming lets you forward your Streamplace stream to multiple platforms at the same time. Instead of streaming only to Streamplace, you can forward your stream to any platform that accepts RTMP input.
1111+1212+## Setting up multistream targets
1313+1414+1. Go to **Settings** > **Streaming** > **Multistream Targets**
1515+2. Click **Create Multistream Target**
1616+3. Enter the RTMP or RTMPS URL from your destination platform
1717+4. Optionally give it a name to identify it later
1818+5. Click **Create**
1919+2020+### Finding your multistream URL
2121+2222+Different platforms will provide their own RTMP URLs. Some common examples:
2323+2424+- **YouTube Live**: Format `rtmp://a.rtmp.youtube.com/live2/your-stream-key`
2525+ - Find your stream key at https://studio.youtube.com/channel/UC/livestreaming (click the copy icon in the top right corner of the 'connect your encoder to go live' box)
2626+- **Twitch**: Format `rtmp://usw20.contribute.live-video.net/app/your-stream-key`
2727+ - You can get a valid RTMPS url at https://help.twitch.tv/s/twitch-ingest-recommendation
2828+ - Find your stream key at https://dashboard.twitch.tv/settings/stream (your 'primary stream key')
2929+3030+:::note
3131+Your stream key should automatically be hidden once you confirm. Make sure you've entered it correctly!
3232+:::
3333+3434+## Managing targets during a stream
3535+3636+When you're live, you can see all your multistream targets on the Live Dashboard with their current status:
3737+3838+- **Green (Active)**: Successfully streaming to this target
3939+- **Yellow (Pending)**: Connecting to this target
4040+- **Red (Error)**: Connection failed; check your URL and credentials
4141+- **Gray (Inactive)**: This target is disabled
4242+4343+You can toggle any target on or off with the switch next to its name. Changes take effect immediately.
4444+4545+## Limits
4646+4747+- **Maximum targets**: 100 total per account
4848+- **Maximum active targets**: 5 simultaneous streams
4949+5050+### Credits
5151+5252+A portion of this documentation was taken from [ndroo.tv](https://bsky.app/profile/ndroo.tv)'s [guide on Streamplace](https://ndroo.tv/streamplace.html#2-configuring-your-account).
+83
js/docs/src/content/docs/features/webhooks.md
···11+---
22+title: Discord Webhooks
33+description: Configure Discord webhooks for livestream announcements and chat
44+sidebar:
55+ order: 30
66+---
77+88+Streamplace supports Discord webhooks for receiving livestream
99+notifications and chat messages. You can create, manage, and configure webhooks
1010+to customize how events are delivered to your Discord channels.
1111+1212+## Webhook Events
1313+1414+You can configure webhooks to listen for specific events. For right now, the
1515+following events are supported:
1616+1717+- `Chat`: Triggered when a chat message is sent.
1818+- `Livestream`: Triggered when a livestream starts.
1919+2020+## Creating a Webhook
2121+2222+To create a webhook, go to the "Settings" page of the Streamplace web app, then
2323+navigate to the "Webhooks" section. Click on "Create Webhook". The following
2424+fields are required:
2525+2626+- Name: Webhook URL. For example,
2727+ `https://discord.com/api/webhooks/{webhook.id}/{webhook.token}`
2828+- Events: Select the events you want to subscribe to (e.g., `Chat Messages`,
2929+ `Livestream Started`). `Livestream Started` is pre-checked by default.
3030+3131+We'd recommend also filling out these optional fields:
3232+3333+- Name: A name for the webhook (e.g., "Discord Livestream Notifications") that
3434+ you can remember.
3535+- Description: A description of what this webhook is for (e.g., "Sends
3636+ livestream start notifications to Discord channel").
3737+- Prefix: A prefix to add to each message sent by this webhook (e.g.,
3838+ "[Streamplace] "). Will apply to both Chat and Livestream events!
3939+- Suffix: A suffix to add to each message sent by this webhook (e.g., "is now
4040+ live!"). Will apply to both Chat and Livestream events!
4141+- Text replacements: A list of text replacements to apply to chat messages sent
4242+ by this webhook. Each replacement consists of a "from" string and a "to"
4343+ string. For example, you could replace all instances of "foo" with "bar".
4444+4545+After filling out the form, click "Create" to save your webhook. You should see
4646+it listed in the "Webhooks" section.
4747+4848+## Updating a Webhook
4949+5050+To update a webhook, go to the "Settings" page of the Streamplace web app, then
5151+navigate to the "Webhooks" section. Find the webhook you want to update and
5252+click on the "pen" icon next to it. This will open the webhook edit form, where
5353+you can modify the fields as needed. After making your changes, click "Update"
5454+to save your changes.
5555+5656+## Deleting a Webhook
5757+5858+To delete a webhook, go to the "Settings" page of the Streamplace web app, then
5959+navigate to the "Webhooks" section. Find the webhook you want to delete and
6060+click on the "trash" icon next to it. A confirmation dialog will appear; click
6161+"Delete" to confirm. The webhook will be removed from the list.
6262+6363+## Recommendations
6464+6565+We'd recommend:
6666+6767+- Creating separate Discord channels for livestream notifications and chat
6868+ messages to keep them organized.
6969+ - If you want to have one webhook for both chat and livestream events, you can
7070+ create multiple webhooks with the same URL but different event subscriptions
7171+ and prefixes/suffixes/replacements.
7272+- Testing your webhook by starting a livestream or sending a chat message to
7373+ ensure that notifications are being sent correctly.
7474+7575+## API Documentation
7676+7777+See these endpoint pages:
7878+7979+- [Create Webhook](/docs/api/operations/placestreamservercreatewebhook)
8080+- [Get Webhook](/docs/api/operations/placestreamservergetwebhook)
8181+- [List Webhooks](/docs/api/operations/placestreamserverlistwebhooks)
8282+- [Update Webhook](/docs/api/operations/placestreamserverupdatewebhook)
8383+- [Delete Webhook](/docs/api/operations/placestreamserverdeletewebhook)
···11----
22-title: Discord Webhooks
33-description: Configure Discord webhooks for livestream announcements and chat
44-sidebar:
55- order: 30
66----
77-88-Streamplace supports Discord webhook integration for receiving livestream
99-notifications and chat messages. You can create, manage, and configure webhooks
1010-to customize how events are delivered to your Discord channels.
1111-1212-## Webhook Events
1313-1414-You can configure webhooks to listen for specific events. For right now, the
1515-following events are supported:
1616-1717-- `Chat`: Triggered when a chat message is sent.
1818-- `Livestream`: Triggered when a livestream starts.
1919-2020-## Creating a Webhook
2121-2222-To create a webhook, go to the "Settings" page of the Streamplace web app, then
2323-navigate to the "Webhooks" section. Click on "Create Webhook". The following
2424-fields are required:
2525-2626-- Name: Webhook URL. For example,
2727- `https://discord.com/api/webhooks/{webhook.id}/{webhook.token}`
2828-- Events: Select the events you want to subscribe to (e.g., `Chat Messages`,
2929- `Livestream Started`). `Livestream Started` is pre-checked by default.
3030-3131-We'd recommend also filling out these optional fields:
3232-3333-- Name: A name for the webhook (e.g., "Discord Livestream Notifications") that
3434- you can remember.
3535-- Description: A description of what this webhook is for (e.g., "Sends
3636- livestream start notifications to Discord channel").
3737-- Prefix: A prefix to add to each message sent by this webhook (e.g.,
3838- "[Streamplace] "). Will apply to both Chat and Livestream events!
3939-- Suffix: A suffix to add to each message sent by this webhook (e.g., "is now
4040- live!"). Will apply to both Chat and Livestream events!
4141-- Text replacements: A list of text replacements to apply to chat messages sent
4242- by this webhook. Each replacement consists of a "from" string and a "to"
4343- string. For example, you could replace all instances of "foo" with "bar".
4444-4545-After filling out the form, click "Create" to save your webhook. You should see
4646-it listed in the "Webhooks" section.
4747-4848-## Updating a Webhook
4949-5050-To update a webhook, go to the "Settings" page of the Streamplace web app, then
5151-navigate to the "Webhooks" section. Find the webhook you want to update and
5252-click on the "pen" icon next to it. This will open the webhook edit form, where
5353-you can modify the fields as needed. After making your changes, click "Update"
5454-to save your changes.
5555-5656-## Deleting a Webhook
5757-5858-To delete a webhook, go to the "Settings" page of the Streamplace web app, then
5959-navigate to the "Webhooks" section. Find the webhook you want to delete and
6060-click on the "trash" icon next to it. A confirmation dialog will appear; click
6161-"Delete" to confirm. The webhook will be removed from the list.
6262-6363-## Recommendations
6464-6565-We'd recommend:
6666-6767-- Creating separate Discord channels for livestream notifications and chat
6868- messages to keep them organized.
6969- - If you want to have one webhook for both chat and livestream events, you can
7070- create multiple webhooks with the same URL but different event subscriptions
7171- and prefixes/suffixes/replacements.
7272-- Testing your webhook by starting a livestream or sending a chat message to
7373- ensure that notifications are being sent correctly.
7474-7575-## API Documentation
7676-7777-See these endpoint pages:
7878-7979-- [Create Webhook](/docs/api/operations/placestreamservercreatewebhook)
8080-- [Get Webhook](/docs/api/operations/placestreamservergetwebhook)
8181-- [List Webhooks](/docs/api/operations/placestreamserverlistwebhooks)
8282-- [Update Webhook](/docs/api/operations/placestreamserverupdatewebhook)
8383-- [Delete Webhook](/docs/api/operations/placestreamserverdeletewebhook)
···11----
22-title: Embedding your livestream
33-description: How to embed your livestream on your website, blog, etc.
44----
55-66-Streamplace provides an easy way to embed your livestream on any website or
77-blog.
88-99-You can access the embedded livestream page by putting `/embed` in the URL of
1010-your livestream. For example, if your livestream URL is
1111-`https://stream.place/iame.li`, the embed URL will be
1212-`https://stream.place/embed/iame.li`.
1313-1414-You can use the following HTML snippet to embed your livestream:
1515-1616-```html
1717-<iframe
1818- src="https://stream.place/embed/your-handle"
1919- width="560"
2020- height="315"
2121- frameborder="0"
2222- allowfullscreen
2323-></iframe>
2424-```
2525-2626-Alternatively, you can use the share sheet located on your livestream page.
2727-Click the "Share" button, and you'll find the embed code ready to copy.
···11---
22-title: OBS Multistreaming with Streamplace
22+title: OBS Multistreaming to Streamplace
33description:
44 Configure OBS for multistreaming to Streamplace and other platforms using the
55 obs-multi-rtmp plugin.
66sidebar:
77 order: 20
88---
99+1010+:::note
1111+This guide is not about the multistreaming feature. Check
1212+[the multistreaming guide](/docs/features/multistreaming) out for more
1313+information.
1414+:::
9151016This guide explains how to configure Open Broadcaster Software (OBS) for
1117simultaneous streaming to Streamplace and other platforms using the
···5858- Audio Encoder:
5959 - For `RTMP`, choose an appropriate AAC encoder.
6060 - For `WHIP`, use `ffmpeg_opus`.
6161+ - If you are using a server that supports the SRT protocol (e.g.
6262+ multistreaming via NGINX) please check below for an example config.
6163- Video Encoder: _(Select appropriate encoder, e.g. libx264/nvenc_h264)_
62646365#### 2e. Suggested Video Encoder Settings
64666767+- Video Encoder: x264/h264 (**must** be an x/h.264 encoder)
6568- Rate Control: `CBR`
6666-- Keyframe Interval: `1s`
6969+- Keyframe Interval: `1s` (or anything less than once every ~7s)
7070+ - This is _one keyframe per second_
7171+ - In some situations (e.g. 'keyframe interval (**frames**)'), this should be
7272+ set to your FPS.
6773- x264 Options: `bframes=0`
6868- - If available, there also may be a 'no bframes' checkbox which should be
7474+ - If available, there also may be a 'bframes' checkbox which should **NOT** be
6975 checked
70767777+:::caution
7878+These last two options are very important! Your viewers' experience may be choppy or otherwise subpar if you don't have them correct.
7979+:::
8080+7181### 3. Announce your stream
728273831. Once you're live, go back to the live dashboard.
···859586962. [**Aitum Multistream Plugin**](https://aitum.tv/products/multi)
87979898+Alternatively, you can
9999+[multistream through Streamplace itself.](/docs/features/multistreaming)
100100+88101## Best Practices
8910290103- Test your stream settings before going live
···96109## Additional Resources
9711098111- [OBS Official Documentation](https://obsproject.com/docs/)
112112+113113+### Example Settings
114114+115115+
116116+117117+> Multistreaming via a server that supports the SRT protocol
···11+---
22+title: Quick Start
33+description: Get up and streaming on Streamplace quickly.
44+sidebar:
55+ order: 1
66+---
77+88+This guide gets you from zero to streaming. If you get stuck, check out the full [OBS setup guide](/docs/guides/start-streaming/obs).
99+1010+:::tip
1111+You will want to check out our [community guidelines](https://blog.stream.place/3mcqwibo4ks2w) first for guidance on what you can and cannot do on Streamplace.
1212+:::
1313+1414+## So, what is Streamplace?
1515+1616+Streamplace is a video streaming service built on top of the AT Protocol (Authenticated Transfer Protocol), the same protocol Bluesky is built on.
1717+1818+## Step 1: Create your account
1919+2020+1. Go to [stream.place](https://stream.place)
2121+2. Click "Sign in" in the top right.
2222+3. Use your Atmosphere credentials to log in (ex. your Bluesky handle)
2323+ - You'll need to use your actual password here - we're using OAuth so you enter your password on your PDS. We do not receive your password at all.
2424+4. You're done! Your stream profile is live at `stream.place/your-handle`
2525+2626+## Step 2: Get your stream key
2727+2828+1. Click **Live Dashboard** (or go to [stream.place/dashboard](https://stream.place/dashboard))
2929+2. Click **Stream from OBS**
3030+3. Click **Generate Stream Key**
3131+4. Your key is copied to clipboard automatically
3232+3333+Keep this key private. It's like a password, but for your stream.
3434+3535+## Step 3: Configure OBS
3636+3737+Open OBS and go to **Settings → Stream**:
3838+3939+- **Service**: `Custom...`
4040+- **Server**: `rtmps://stream.place:1935/live`
4141+- **Stream Key**: Paste what you copied in Step 2
4242+4343+Then go to **Settings → Output → Streaming**:
4444+4545+- **Video Encoder**: `libx264` (or `NVIDIA NVENC H.264` if you have an NVIDIA GPU)
4646+- **Rate Control**: `CBR`
4747+- **Bitrate**: `6000` Kbps (adjust down if you drop frames)
4848+- **Keyframe Interval**: `1`
4949+- **x264 Options**: `bframes=0`. If there's a 'bframes' option, you'll want to have that at '0' or unchecked.
5050+5151+:::caution
5252+These last two options are very important! Your viewers' experience may be choppy or otherwise subpar if you don't have them correct.
5353+:::
5454+5555+## Step 4: Go live
5656+5757+1. In OBS, click **Start Streaming**
5858+2. Go back to the Live Dashboard at stream.place
5959+3. Fill in your stream title and optionally pick a thumbnail8
6060+4. If needed, turn on content warnings. ("Metadata" tab in Stream Settings)
6161+5. Click **Announce Livestream**
6262+6. Your stream is now live and visible to the world!
6363+6464+## Next steps
6565+6666+- **Customize your chat**: Change your name color in Settings > Account
6767+- **Stream to other platforms too**: Set your Twitch/YouTube URLs in Settings > Multistream Targets to push your stream there automatically. See the [Multistreaming guide](/docs/features/multistreaming) for more information
6868+- **Improve stream quality**: See the [OBS guide](/docs/guides/start-streaming/obs) for encoder settings and troubleshooting
6969+- **Join the Discord!**: If you need any help, or just want to chat, check out our discord at https://discord.stream.place.
7070+7171+### Credits
7272+7373+A portion of this documentation was taken from [ndroo.tv](https://bsky.app/profile/ndroo.tv)'s excellent [guide on Streamplace](https://ndroo.tv/streamplace.html#2-configuring-your-account).
···22title: Welcome to Streamplace!
33description: Begin your development journey with the Streamplace documentation.
44template: doc
55-hero:
66- tagline: Solve live video for your project with Streamplace.
77- image:
88- file: ../../assets/cube.png
99- alt: Streamplace logo. A pink 3d box viewed from a top corner.
1010- actions:
1111- - text: Get Started
1212- link: /docs/guides/start-streaming/obs
1313- icon: right-arrow
1414- - text: Visit Streamplace
1515- link: /
1616- icon: external
1717- variant: minimal
185---
1962020-import { Card, CardGrid } from "@astrojs/starlight/components";
2121-2222-## Next Steps
77+import HelpDesk from "../../components/HelpDesk.astro";
2382424-<CardGrid>
2525- <Card title="Read the Docs" icon="open-book">
2626- Learn how to start streaming with
2727- [Streamplace](/docs/guides/start-streaming/obs).
2828- </Card>
2929- <Card title="Install Streamplace" icon="download">
3030- [Run your own Streamplace
3131- node](/docs/guides/installing/installing-streamplace).
3232- </Card>
3333- <Card title="API Reference" icon="document">
3434- Explore the [Lexicon API reference](/docs/lex-reference/place-stream-defs).
3535- </Card>
3636- <Card title="Developer Setup" icon="setting">
3737- Set up your [development environment](/docs/guides/streamplace-dev-setup).
3838- </Card>
3939-</CardGrid>
99+<HelpDesk />
···2828- **Description:** Raw blob data with appropriate content-type
2929- **Schema:**
30303131-_Schema not defined._ **Possible Errors:**
3131+_Schema not defined._
3232+**Possible Errors:**
32333334- `BrandingNotFound`: The requested branding asset does not exist
3435
···13131414**Type:** `record`
15151616-Record indicating a livestream is published and available for replication at a
1717-given address. By convention, the record key is streamer::server
1616+Record indicating a livestream is published and available for replication at a given address. By convention, the record key is streamer::server
18171918**Record Key:** `any`
2019
···13131414**Type:** `record`
15151616-Record created by a Streamplace broadcaster to indicate that they will be
1717-replicating a livestream. NYI
1616+Record created by a Streamplace broadcaster to indicate that they will be replicating a livestream. NYI
18171918**Record Key:** `tid`
2019
···13131414**Type:** `query`
15151616-Find actor suggestions for a prefix search term. Expected use is for
1717-auto-completion during text field entry.
1616+Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry.
18171918**Parameters:**
2019
···11+---
22+title: place.stream.live.teleport
33+description: Reference for the place.stream.live.teleport lexicon
44+---
55+66+**Lexicon Version:** 1
77+88+## Definitions
99+1010+<a name="main"></a>
1111+1212+### `main`
1313+1414+**Type:** `record`
1515+1616+Record defining a 'teleport', that is active during a certain time.
1717+1818+**Record Key:** `tid`
1919+2020+**Record Properties:**
2121+2222+| Name | Type | Req'd | Description | Constraints |
2323+| ----------------- | --------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
2424+| `streamer` | `string` | ✅ | The DID of the streamer to teleport to. | Format: `did` |
2525+| `startsAt` | `string` | ✅ | The time the teleport becomes active. | Format: `datetime` |
2626+| `durationSeconds` | `integer` | ❌ | The time limit in seconds for the teleport. If not set, the teleport is permanent. Must be at least 60 seconds, and no more than 32,400 seconds (9 hours). | Min: 60<br/>Max: 32400 |
2727+2828+---
2929+3030+## Lexicon Source
3131+3232+```json
3333+{
3434+ "lexicon": 1,
3535+ "id": "place.stream.live.teleport",
3636+ "defs": {
3737+ "main": {
3838+ "type": "record",
3939+ "key": "tid",
4040+ "description": "Record defining a 'teleport', that is active during a certain time.",
4141+ "record": {
4242+ "type": "object",
4343+ "required": ["streamer", "startsAt"],
4444+ "properties": {
4545+ "streamer": {
4646+ "type": "string",
4747+ "format": "did",
4848+ "description": "The DID of the streamer to teleport to."
4949+ },
5050+ "startsAt": {
5151+ "type": "string",
5252+ "format": "datetime",
5353+ "description": "The time the teleport becomes active."
5454+ },
5555+ "durationSeconds": {
5656+ "type": "integer",
5757+ "description": "The time limit in seconds for the teleport. If not set, the teleport is permanent. Must be at least 60 seconds, and no more than 32,400 seconds (9 hours).",
5858+ "minimum": 60,
5959+ "maximum": 32400
6060+ }
6161+ }
6262+ }
6363+ }
6464+ }
6565+}
6666+```
···13131414**Type:** `record`
15151616-Default metadata record for livestream including content warnings, rights, and
1717-distribution policy
1616+Default metadata record for livestream including content warnings, rights, and distribution policy
18171918**Record Key:** `literal:self`
2019
···33333434**Type:** `token`
35353636-All rights reserved to the creator — others cannot use, modify, or share without
3737-explicit authorization.
3636+All rights reserved to the creator — others cannot use, modify, or share without explicit authorization.
38373938---
4039···44434544**Type:** `token`
46454747-Public domain dedication. You waive all copyright and related rights where
4848-possible. Others may copy, modify, distribute, or perform your work for any
4949-purpose without attribution.
4646+Public domain dedication. You waive all copyright and related rights where possible. Others may copy, modify, distribute, or perform your work for any purpose without attribution.
50475148---
5249···56535754**Type:** `token`
58555959-Attribution required. Others may copy, distribute, remix, and build upon your
6060-work, even commercially, if they credit you.
5656+Attribution required. Others may copy, distribute, remix, and build upon your work, even commercially, if they credit you.
61576258---
6359···67636864**Type:** `token`
69657070-Attribution + share-alike. Others may adapt and build upon your work, even
7171-commercially, if they credit you and license their new creations under identical
7272-terms.
6666+Attribution + share-alike. Others may adapt and build upon your work, even commercially, if they credit you and license their new creations under identical terms.
73677468---
7569···79738074**Type:** `token`
81758282-Attribution + non-commercial. Others may adapt and build upon your work for
8383-non-commercial purposes only, and must credit you.
7676+Attribution + non-commercial. Others may adapt and build upon your work for non-commercial purposes only, and must credit you.
84778578---
8679···90839184**Type:** `token`
92859393-Attribution + non-commercial + share-alike. Others may adapt and build upon your
9494-work for non-commercial purposes only, must credit you, and must license their
9595-new creations under identical terms.
8686+Attribution + non-commercial + share-alike. Others may adapt and build upon your work for non-commercial purposes only, must credit you, and must license their new creations under identical terms.
96879788---
9889···1029310394**Type:** `token`
10495105105-Attribution + no derivatives. Others may reuse your work, even commercially, but
106106-it must remain unchanged and you must be credited.
9696+Attribution + no derivatives. Others may reuse your work, even commercially, but it must remain unchanged and you must be credited.
1079710898---
10999···113103114104**Type:** `token`
115105116116-Attribution + non-commercial + no derivatives. Others may download and share
117117-your work with credit, but cannot change it or use it commercially.
106106+Attribution + non-commercial + no derivatives. Others may download and share your work with credit, but cannot change it or use it commercially.
118107119108---
120109
···29293030**Type:** `token`
31313232-The content could be perceived as offensive due to the discussion or display of
3333-death.
3232+The content could be perceived as offensive due to the discussion or display of death.
34333534---
3635···40394140**Type:** `token`
42414343-The content contains a portrayal of the use or abuse of mind altering
4444-substances.
4242+The content contains a portrayal of the use or abuse of mind altering substances.
45434644---
4745···51495250**Type:** `token`
53515454-The content contains violent actions of a fantasy nature, involving human or
5555-non-human characters in situations easily distinguishable from real life.
5252+The content contains violent actions of a fantasy nature, involving human or non-human characters in situations easily distinguishable from real life.
56535754---
5855···62596360**Type:** `token`
64616565-The content contains flashing lights that could be harmful to viewers with
6666-seizure disorders such as photosensitive epilepsy.
6262+The content contains flashing lights that could be harmful to viewers with seizure disorders such as photosensitive epilepsy.
67636864---
6965···93899490**Type:** `token`
95919696-The content contains information that can be used to identify a particular
9797-individual, such as a name, phone number, email address, physical address, or IP
9898-address.
9292+The content contains information that can be used to identify a particular individual, such as a name, phone number, email address, physical address, or IP address.
999310094---
10195···10599106100**Type:** `token`
107101108108-The content could be perceived as offensive due to the discussion or display of
109109-sexuality.
102102+The content could be perceived as offensive due to the discussion or display of sexuality.
110103111104---
112105···116109117110**Type:** `token`
118111119119-The content could be perceived as distressing due to the discussion or display
120120-of suffering or triggering topics, including suicide, eating disorders or self
121121-harm.
112112+The content could be perceived as distressing due to the discussion or display of suffering or triggering topics, including suicide, eating disorders or self harm.
122113123114---
124115···128119129120**Type:** `token`
130121131131-The content could be perceived as offensive due to the discussion or display of
132132-violence.
122122+The content could be perceived as offensive due to the discussion or display of violence.
133123134124---
135125
···13131414**Type:** `procedure`
15151616-Create a block (ban) on behalf of a streamer. Requires 'ban' permission. Creates
1717-an app.bsky.graph.block record in the streamer's repository.
1616+Create a block (ban) on behalf of a streamer. Requires 'ban' permission. Creates an app.bsky.graph.block record in the streamer's repository.
18171918**Parameters:** _(None defined)_
2019···4645**Possible Errors:**
47464847- `Unauthorized`: The request lacks valid authentication credentials.
4949-- `Forbidden`: The caller does not have permission to create blocks for this
5050- streamer.
5151-- `SessionNotFound`: The streamer's OAuth session could not be found or is
5252- invalid.
4848+- `Forbidden`: The caller does not have permission to create blocks for this streamer.
4949+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
53505451---
5552
···13131414**Type:** `procedure`
15151616-Create a gate (hide message) on behalf of a streamer. Requires 'hide'
1717-permission. Creates a place.stream.chat.gate record in the streamer's
1818-repository.
1616+Create a gate (hide message) on behalf of a streamer. Requires 'hide' permission. Creates a place.stream.chat.gate record in the streamer's repository.
19172018**Parameters:** _(None defined)_
2119···4644**Possible Errors:**
47454846- `Unauthorized`: The request lacks valid authentication credentials.
4949-- `Forbidden`: The caller does not have permission to hide messages for this
5050- streamer.
5151-- `SessionNotFound`: The streamer's OAuth session could not be found or is
5252- invalid.
4747+- `Forbidden`: The caller does not have permission to hide messages for this streamer.
4848+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
53495450---
5551
···13131414**Type:** `procedure`
15151616-Delete a block (unban) on behalf of a streamer. Requires 'ban' permission.
1717-Deletes an app.bsky.graph.block record from the streamer's repository.
1616+Delete a block (unban) on behalf of a streamer. Requires 'ban' permission. Deletes an app.bsky.graph.block record from the streamer's repository.
18171918**Parameters:** _(None defined)_
2019···37363837**Schema Type:** `object`
39384040-_(No properties defined)_ **Possible Errors:**
3939+_(No properties defined)_
4040+**Possible Errors:**
41414242- `Unauthorized`: The request lacks valid authentication credentials.
4343-- `Forbidden`: The caller does not have permission to delete blocks for this
4444- streamer.
4545-- `SessionNotFound`: The streamer's OAuth session could not be found or is
4646- invalid.
4343+- `Forbidden`: The caller does not have permission to delete blocks for this streamer.
4444+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
47454846---
4947
···13131414**Type:** `procedure`
15151616-Delete a gate (unhide message) on behalf of a streamer. Requires 'hide'
1717-permission. Deletes a place.stream.chat.gate record from the streamer's
1818-repository.
1616+Delete a gate (unhide message) on behalf of a streamer. Requires 'hide' permission. Deletes a place.stream.chat.gate record from the streamer's repository.
19172018**Parameters:** _(None defined)_
2119···38363937**Schema Type:** `object`
40384141-_(No properties defined)_ **Possible Errors:**
3939+_(No properties defined)_
4040+**Possible Errors:**
42414342- `Unauthorized`: The request lacks valid authentication credentials.
4444-- `Forbidden`: The caller does not have permission to unhide messages for this
4545- streamer.
4646-- `SessionNotFound`: The streamer's OAuth session could not be found or is
4747- invalid.
4343+- `Forbidden`: The caller does not have permission to unhide messages for this streamer.
4444+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
48454946---
5047
···13131414**Type:** `procedure`
15151616-Update livestream metadata on behalf of a streamer. Requires 'livestream.manage'
1717-permission. Updates a place.stream.livestream record in the streamer's
1818-repository.
1616+Update livestream metadata on behalf of a streamer. Requires 'livestream.manage' permission. Updates a place.stream.livestream record in the streamer's repository.
19172018**Parameters:** _(None defined)_
2119···4745**Possible Errors:**
48464947- `Unauthorized`: The request lacks valid authentication credentials.
5050-- `Forbidden`: The caller does not have permission to update livestream metadata
5151- for this streamer.
5252-- `SessionNotFound`: The streamer's OAuth session could not be found or is
5353- invalid.
4848+- `Forbidden`: The caller does not have permission to update livestream metadata for this streamer.
4949+- `SessionNotFound`: The streamer's OAuth session could not be found or is invalid.
5450- `RecordNotFound`: The specified livestream record does not exist.
55515652---
···11+{
22+ "lexicon": 1,
33+ "id": "com.atproto.sync.getRepo",
44+ "defs": {
55+ "main": {
66+ "type": "query",
77+ "description": "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.",
88+ "parameters": {
99+ "type": "params",
1010+ "required": ["did"],
1111+ "properties": {
1212+ "did": {
1313+ "type": "string",
1414+ "format": "did",
1515+ "description": "The DID of the repo."
1616+ },
1717+ "since": {
1818+ "type": "string",
1919+ "format": "tid",
2020+ "description": "The revision ('rev') of the repo to create a diff from."
2121+ }
2222+ }
2323+ },
2424+ "output": {
2525+ "encoding": "application/vnd.ipld.car"
2626+ },
2727+ "errors": [
2828+ { "name": "RepoNotFound" },
2929+ { "name": "RepoTakendown" },
3030+ { "name": "RepoSuspended" },
3131+ { "name": "RepoDeactivated" }
3232+ ]
3333+ }
3434+ }
3535+}
+39-1
lexicons/place/stream/chat/defs.json
···1212 "type": "ref",
1313 "ref": "app.bsky.actor.defs#profileViewBasic"
1414 },
1515- "record": { "type": "unknown" },
1515+ "record": {
1616+ "type": "union",
1717+ "refs": ["#messageRecordView"]
1818+ },
1619 "indexedAt": { "type": "string", "format": "datetime" },
1720 "chatProfile": {
1821 "type": "ref",
···2528 "deleted": {
2629 "type": "boolean",
2730 "description": "If true, this message has been deleted or labeled and should be cleared from the cache"
3131+ }
3232+ }
3333+ },
3434+ "messageRecordView": {
3535+ "type": "object",
3636+ "description": "The content of a chat message.",
3737+ "required": ["text", "createdAt", "streamer"],
3838+ "properties": {
3939+ "text": {
4040+ "type": "string",
4141+ "maxLength": 3000,
4242+ "maxGraphemes": 300,
4343+ "description": "The primary message content. May be an empty string, if there are embeds."
4444+ },
4545+ "createdAt": {
4646+ "type": "string",
4747+ "format": "datetime",
4848+ "description": "Client-declared timestamp when this message was originally created."
4949+ },
5050+ "facets": {
5151+ "type": "array",
5252+ "description": "Annotations of text (mentions, URLs, etc)",
5353+ "items": {
5454+ "type": "ref",
5555+ "ref": "place.stream.richtext.defs#facetView"
5656+ }
5757+ },
5858+ "streamer": {
5959+ "type": "string",
6060+ "format": "did",
6161+ "description": "The DID of the streamer whose chat this is."
6262+ },
6363+ "reply": {
6464+ "type": "ref",
6565+ "ref": "place.stream.chat.message#replyRef"
2866 }
2967 }
3068 }
+47
lexicons/place/stream/live/denyTeleport.json
···11+{
22+ "lexicon": 1,
33+ "id": "place.stream.live.denyTeleport",
44+ "defs": {
55+ "main": {
66+ "type": "procedure",
77+ "description": "Deny an incoming teleport request.",
88+ "input": {
99+ "encoding": "application/json",
1010+ "schema": {
1111+ "type": "object",
1212+ "required": ["uri"],
1313+ "properties": {
1414+ "uri": {
1515+ "type": "string",
1616+ "format": "at-uri",
1717+ "description": "The URI of the teleport record to deny."
1818+ }
1919+ }
2020+ }
2121+ },
2222+ "output": {
2323+ "encoding": "application/json",
2424+ "schema": {
2525+ "type": "object",
2626+ "required": ["success"],
2727+ "properties": {
2828+ "success": {
2929+ "type": "boolean",
3030+ "description": "Whether the teleport was successfully denied."
3131+ }
3232+ }
3333+ }
3434+ },
3535+ "errors": [
3636+ {
3737+ "name": "TeleportNotFound",
3838+ "description": "The specified teleport was not found."
3939+ },
4040+ {
4141+ "name": "Unauthorized",
4242+ "description": "The authenticated user is not the target of this teleport."
4343+ }
4444+ ]
4545+ }
4646+ }
4747+}
+33
lexicons/place/stream/live/teleport.json
···11+{
22+ "lexicon": 1,
33+ "id": "place.stream.live.teleport",
44+ "defs": {
55+ "main": {
66+ "type": "record",
77+ "key": "tid",
88+ "description": "Record defining a 'teleport', that is active during a certain time.",
99+ "record": {
1010+ "type": "object",
1111+ "required": ["streamer", "startsAt"],
1212+ "properties": {
1313+ "streamer": {
1414+ "type": "string",
1515+ "format": "did",
1616+ "description": "The DID of the streamer to teleport to."
1717+ },
1818+ "startsAt": {
1919+ "type": "string",
2020+ "format": "datetime",
2121+ "description": "The time the teleport becomes active."
2222+ },
2323+ "durationSeconds": {
2424+ "type": "integer",
2525+ "description": "The time limit in seconds for the teleport. If not set, the teleport is permanent. Must be at least 60 seconds, and no more than 32,400 seconds (9 hours).",
2626+ "minimum": 60,
2727+ "maximum": 32400
2828+ }
2929+ }
3030+ }
3131+ }
3232+ }
3333+}
+48
lexicons/place/stream/livestream.json
···8888 "count": { "type": "integer" }
8989 }
9090 },
9191+ "teleportArrival": {
9292+ "type": "object",
9393+ "required": ["teleportUri", "source", "viewerCount", "startsAt"],
9494+ "properties": {
9595+ "teleportUri": {
9696+ "type": "string",
9797+ "format": "at-uri",
9898+ "description": "The URI of the teleport record"
9999+ },
100100+ "source": {
101101+ "type": "ref",
102102+ "ref": "app.bsky.actor.defs#profileViewBasic",
103103+ "description": "The streamer who is teleporting their viewers here"
104104+ },
105105+ "chatProfile": {
106106+ "type": "ref",
107107+ "ref": "place.stream.chat.profile",
108108+ "description": "The chat profile of the source streamer"
109109+ },
110110+ "viewerCount": {
111111+ "type": "integer",
112112+ "description": "How many viewers are arriving from this teleport"
113113+ },
114114+ "startsAt": {
115115+ "type": "string",
116116+ "format": "datetime",
117117+ "description": "When this teleport started"
118118+ }
119119+ }
120120+ },
121121+ "teleportCanceled": {
122122+ "type": "object",
123123+ "required": ["teleportUri", "reason"],
124124+ "properties": {
125125+ "teleportUri": {
126126+ "type": "string",
127127+ "format": "at-uri",
128128+ "description": "The URI of the teleport record that was canceled"
129129+ },
130130+ "reason": {
131131+ "type": "string",
132132+ "enum": ["deleted", "denied", "expired"],
133133+ "description": "Why this teleport was canceled"
134134+ }
135135+ }
136136+ },
91137 "streamplaceAnything": {
92138 "type": "object",
93139 "required": ["livestream"],
···97143 "refs": [
98144 "#livestreamView",
99145 "#viewerCount",
146146+ "#teleportArrival",
147147+ "#teleportCanceled",
100148 "place.stream.defs#blockView",
101149 "place.stream.defs#renditions",
102150 "place.stream.defs#rendition",
+56
lexicons/place/stream/richtext/defs.json
···11+{
22+ "lexicon": 1,
33+ "id": "place.stream.richtext.defs",
44+ "defs": {
55+ "facetView": {
66+ "type": "object",
77+ "description": "Annotation of a sub-string within rich text.",
88+ "required": ["index", "features"],
99+ "properties": {
1010+ "index": { "type": "ref", "ref": "app.bsky.richtext.facet#byteSlice" },
1111+ "features": {
1212+ "type": "array",
1313+ "items": {
1414+ "type": "union",
1515+ "refs": [
1616+ "app.bsky.richtext.facet#mention",
1717+ "app.bsky.richtext.facet#link",
1818+ "#censor"
1919+ ]
2020+ }
2121+ }
2222+ }
2323+ },
2424+ "censor": {
2525+ "type": "object",
2626+ "description": "Indicates that the text in the given index has been censored.",
2727+ "properties": {
2828+ "reason": { "type": "string" },
2929+ "categories": {
3030+ "type": "array",
3131+ "items": {
3232+ "type": "string",
3333+ "knownValues": [
3434+ "place.stream.richtext.defs#discriminatory",
3535+ "place.stream.richtext.defs#sexually_explicit",
3636+ "place.stream.richtext.defs#profanity"
3737+ ]
3838+ },
3939+ "description": "Categories of censored content"
4040+ }
4141+ }
4242+ },
4343+ "discriminatory": {
4444+ "type": "token",
4545+ "description": "Indicates that the text has been censored due to discriminatory content."
4646+ },
4747+ "sexually_explicit": {
4848+ "type": "token",
4949+ "description": "Indicates that the text has been censored due to sexually explicit content."
5050+ },
5151+ "profanity": {
5252+ "type": "token",
5353+ "description": "Indicates that the text has been censored due to profanity."
5454+ }
5555+ }
5656+}
···5656 Build *BuildFlags
5757 DataDir string
5858 DBURL string
5959+ LocalDBURL string
5960 EthAccountAddr string
6061 EthKeystorePath string
6162 EthPassword string
···140141 SegmentDebugDir string
141142 AdminDIDs []string
142143 Syndicate []string
144144+ PlayerTelemetry bool
143145}
144146145147// ContentFilters represents the content filtering configuration
···240242 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")
241243 cli.StringSliceFlag(fs, &cli.AdminDIDs, "admin-dids", []string{}, "comma-separated list of DIDs that are authorized to modify branding and other admin operations")
242244 cli.StringSliceFlag(fs, &cli.Syndicate, "syndicate", []string{}, "list of DIDs that we should rebroadcast ('*' for everybody)")
245245+ fs.BoolVar(&cli.PlayerTelemetry, "player-telemetry", true, "enable player telemetry")
246246+ fs.StringVar(&cli.LocalDBURL, "local-db-url", "sqlite://$SP_DATA_DIR/localdb.sqlite", "URL of the local database to use for storing local data")
247247+ cli.dataDirFlags = append(cli.dataDirFlags, &cli.LocalDBURL)
243248244249 fs.Bool("external-signing", true, "DEPRECATED, does nothing.")
245250 fs.Bool("insecure", false, "DEPRECATED, does nothing.")
···5526552655275527 return nil
55285528}
55295529+func (t *LiveTeleport) MarshalCBOR(w io.Writer) error {
55305530+ if t == nil {
55315531+ _, err := w.Write(cbg.CborNull)
55325532+ return err
55335533+ }
55345534+55355535+ cw := cbg.NewCborWriter(w)
55365536+ fieldCount := 4
55375537+55385538+ if t.DurationSeconds == nil {
55395539+ fieldCount--
55405540+ }
55415541+55425542+ if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
55435543+ return err
55445544+ }
55455545+55465546+ // t.LexiconTypeID (string) (string)
55475547+ if len("$type") > 1000000 {
55485548+ return xerrors.Errorf("Value in field \"$type\" was too long")
55495549+ }
55505550+55515551+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
55525552+ return err
55535553+ }
55545554+ if _, err := cw.WriteString(string("$type")); err != nil {
55555555+ return err
55565556+ }
55575557+55585558+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.live.teleport"))); err != nil {
55595559+ return err
55605560+ }
55615561+ if _, err := cw.WriteString(string("place.stream.live.teleport")); err != nil {
55625562+ return err
55635563+ }
55645564+55655565+ // t.StartsAt (string) (string)
55665566+ if len("startsAt") > 1000000 {
55675567+ return xerrors.Errorf("Value in field \"startsAt\" was too long")
55685568+ }
55695569+55705570+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("startsAt"))); err != nil {
55715571+ return err
55725572+ }
55735573+ if _, err := cw.WriteString(string("startsAt")); err != nil {
55745574+ return err
55755575+ }
55765576+55775577+ if len(t.StartsAt) > 1000000 {
55785578+ return xerrors.Errorf("Value in field t.StartsAt was too long")
55795579+ }
55805580+55815581+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.StartsAt))); err != nil {
55825582+ return err
55835583+ }
55845584+ if _, err := cw.WriteString(string(t.StartsAt)); err != nil {
55855585+ return err
55865586+ }
55875587+55885588+ // t.Streamer (string) (string)
55895589+ if len("streamer") > 1000000 {
55905590+ return xerrors.Errorf("Value in field \"streamer\" was too long")
55915591+ }
55925592+55935593+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("streamer"))); err != nil {
55945594+ return err
55955595+ }
55965596+ if _, err := cw.WriteString(string("streamer")); err != nil {
55975597+ return err
55985598+ }
55995599+56005600+ if len(t.Streamer) > 1000000 {
56015601+ return xerrors.Errorf("Value in field t.Streamer was too long")
56025602+ }
56035603+56045604+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Streamer))); err != nil {
56055605+ return err
56065606+ }
56075607+ if _, err := cw.WriteString(string(t.Streamer)); err != nil {
56085608+ return err
56095609+ }
56105610+56115611+ // t.DurationSeconds (int64) (int64)
56125612+ if t.DurationSeconds != nil {
56135613+56145614+ if len("durationSeconds") > 1000000 {
56155615+ return xerrors.Errorf("Value in field \"durationSeconds\" was too long")
56165616+ }
56175617+56185618+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("durationSeconds"))); err != nil {
56195619+ return err
56205620+ }
56215621+ if _, err := cw.WriteString(string("durationSeconds")); err != nil {
56225622+ return err
56235623+ }
56245624+56255625+ if t.DurationSeconds == nil {
56265626+ if _, err := cw.Write(cbg.CborNull); err != nil {
56275627+ return err
56285628+ }
56295629+ } else {
56305630+ if *t.DurationSeconds >= 0 {
56315631+ if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.DurationSeconds)); err != nil {
56325632+ return err
56335633+ }
56345634+ } else {
56355635+ if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.DurationSeconds-1)); err != nil {
56365636+ return err
56375637+ }
56385638+ }
56395639+ }
56405640+56415641+ }
56425642+ return nil
56435643+}
56445644+56455645+func (t *LiveTeleport) UnmarshalCBOR(r io.Reader) (err error) {
56465646+ *t = LiveTeleport{}
56475647+56485648+ cr := cbg.NewCborReader(r)
56495649+56505650+ maj, extra, err := cr.ReadHeader()
56515651+ if err != nil {
56525652+ return err
56535653+ }
56545654+ defer func() {
56555655+ if err == io.EOF {
56565656+ err = io.ErrUnexpectedEOF
56575657+ }
56585658+ }()
56595659+56605660+ if maj != cbg.MajMap {
56615661+ return fmt.Errorf("cbor input should be of type map")
56625662+ }
56635663+56645664+ if extra > cbg.MaxLength {
56655665+ return fmt.Errorf("LiveTeleport: map struct too large (%d)", extra)
56665666+ }
56675667+56685668+ n := extra
56695669+56705670+ nameBuf := make([]byte, 15)
56715671+ for i := uint64(0); i < n; i++ {
56725672+ nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
56735673+ if err != nil {
56745674+ return err
56755675+ }
56765676+56775677+ if !ok {
56785678+ // Field doesn't exist on this type, so ignore it
56795679+ if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
56805680+ return err
56815681+ }
56825682+ continue
56835683+ }
56845684+56855685+ switch string(nameBuf[:nameLen]) {
56865686+ // t.LexiconTypeID (string) (string)
56875687+ case "$type":
56885688+56895689+ {
56905690+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
56915691+ if err != nil {
56925692+ return err
56935693+ }
56945694+56955695+ t.LexiconTypeID = string(sval)
56965696+ }
56975697+ // t.StartsAt (string) (string)
56985698+ case "startsAt":
56995699+57005700+ {
57015701+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
57025702+ if err != nil {
57035703+ return err
57045704+ }
57055705+57065706+ t.StartsAt = string(sval)
57075707+ }
57085708+ // t.Streamer (string) (string)
57095709+ case "streamer":
57105710+57115711+ {
57125712+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
57135713+ if err != nil {
57145714+ return err
57155715+ }
57165716+57175717+ t.Streamer = string(sval)
57185718+ }
57195719+ // t.DurationSeconds (int64) (int64)
57205720+ case "durationSeconds":
57215721+ {
57225722+57235723+ b, err := cr.ReadByte()
57245724+ if err != nil {
57255725+ return err
57265726+ }
57275727+ if b != cbg.CborNull[0] {
57285728+ if err := cr.UnreadByte(); err != nil {
57295729+ return err
57305730+ }
57315731+ maj, extra, err := cr.ReadHeader()
57325732+ if err != nil {
57335733+ return err
57345734+ }
57355735+ var extraI int64
57365736+ switch maj {
57375737+ case cbg.MajUnsignedInt:
57385738+ extraI = int64(extra)
57395739+ if extraI < 0 {
57405740+ return fmt.Errorf("int64 positive overflow")
57415741+ }
57425742+ case cbg.MajNegativeInt:
57435743+ extraI = int64(extra)
57445744+ if extraI < 0 {
57455745+ return fmt.Errorf("int64 negative overflow")
57465746+ }
57475747+ extraI = -1 - extraI
57485748+ default:
57495749+ return fmt.Errorf("wrong type for int64 field: %d", maj)
57505750+ }
57515751+57525752+ t.DurationSeconds = (*int64)(&extraI)
57535753+ }
57545754+ }
57555755+57565756+ default:
57575757+ // Field doesn't exist on this type, so ignore it
57585758+ if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
57595759+ return err
57605760+ }
57615761+ }
57625762+ }
57635763+57645764+ return nil
57655765+}
55295766func (t *LiveRecommendations) MarshalCBOR(w io.Writer) error {
55305767 if t == nil {
55315768 _, err := w.Write(cbg.CborNull)
+44-1
pkg/streamplace/chatdefs.go
···1212 lexutil "github.com/bluesky-social/indigo/lex/util"
1313)
14141515+// ChatDefs_MessageRecordView is a "messageRecordView" in the place.stream.chat.defs schema.
1616+//
1717+// The content of a chat message.
1818+type ChatDefs_MessageRecordView struct {
1919+ LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.chat.defs#messageRecordView"`
2020+ // createdAt: Client-declared timestamp when this message was originally created.
2121+ CreatedAt string `json:"createdAt" cborgen:"createdAt"`
2222+ // facets: Annotations of text (mentions, URLs, etc)
2323+ Facets []*RichtextDefs_FacetView `json:"facets,omitempty" cborgen:"facets,omitempty"`
2424+ Reply *ChatMessage_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"`
2525+ // streamer: The DID of the streamer whose chat this is.
2626+ Streamer string `json:"streamer" cborgen:"streamer"`
2727+ // text: The primary message content. May be an empty string, if there are embeds.
2828+ Text string `json:"text" cborgen:"text"`
2929+}
3030+1531// ChatDefs_MessageView is a "messageView" in the place.stream.chat.defs schema.
1632type ChatDefs_MessageView struct {
1733 LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.chat.defs#messageView"`
···2137 // deleted: If true, this message has been deleted or labeled and should be cleared from the cache
2238 Deleted *bool `json:"deleted,omitempty" cborgen:"deleted,omitempty"`
2339 IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
2424- Record *lexutil.LexiconTypeDecoder `json:"record" cborgen:"record"`
4040+ Record *ChatDefs_MessageView_Record `json:"record" cborgen:"record"`
2541 ReplyTo *ChatDefs_MessageView_ReplyTo `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"`
2642 Uri string `json:"uri" cborgen:"uri"`
4343+}
4444+4545+type ChatDefs_MessageView_Record struct {
4646+ ChatDefs_MessageRecordView *ChatDefs_MessageRecordView
4747+}
4848+4949+func (t *ChatDefs_MessageView_Record) MarshalJSON() ([]byte, error) {
5050+ if t.ChatDefs_MessageRecordView != nil {
5151+ t.ChatDefs_MessageRecordView.LexiconTypeID = "place.stream.chat.defs#messageRecordView"
5252+ return json.Marshal(t.ChatDefs_MessageRecordView)
5353+ }
5454+ return nil, fmt.Errorf("can not marshal empty union as JSON")
5555+}
5656+5757+func (t *ChatDefs_MessageView_Record) UnmarshalJSON(b []byte) error {
5858+ typ, err := lexutil.TypeExtract(b)
5959+ if err != nil {
6060+ return err
6161+ }
6262+6363+ switch typ {
6464+ case "place.stream.chat.defs#messageRecordView":
6565+ t.ChatDefs_MessageRecordView = new(ChatDefs_MessageRecordView)
6666+ return json.Unmarshal(b, t.ChatDefs_MessageRecordView)
6767+ default:
6868+ return nil
6969+ }
2770}
28712972type ChatDefs_MessageView_ReplyTo struct {
+33
pkg/streamplace/livedenyTeleport.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+// Lexicon schema: place.stream.live.denyTeleport
44+55+package streamplace
66+77+import (
88+ "context"
99+1010+ lexutil "github.com/bluesky-social/indigo/lex/util"
1111+)
1212+1313+// LiveDenyTeleport_Input is the input argument to a place.stream.live.denyTeleport call.
1414+type LiveDenyTeleport_Input struct {
1515+ // uri: The URI of the teleport record to deny.
1616+ Uri string `json:"uri" cborgen:"uri"`
1717+}
1818+1919+// LiveDenyTeleport_Output is the output of a place.stream.live.denyTeleport call.
2020+type LiveDenyTeleport_Output struct {
2121+ // success: Whether the teleport was successfully denied.
2222+ Success bool `json:"success" cborgen:"success"`
2323+}
2424+2525+// LiveDenyTeleport calls the XRPC method "place.stream.live.denyTeleport".
2626+func LiveDenyTeleport(ctx context.Context, c lexutil.LexClient, input *LiveDenyTeleport_Input) (*LiveDenyTeleport_Output, error) {
2727+ var out LiveDenyTeleport_Output
2828+ if err := c.LexDo(ctx, lexutil.Procedure, "application/json", "place.stream.live.denyTeleport", nil, input, &out); err != nil {
2929+ return nil, err
3030+ }
3131+3232+ return &out, nil
3333+}
+23
pkg/streamplace/liveteleport.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+// Lexicon schema: place.stream.live.teleport
44+55+package streamplace
66+77+import (
88+ lexutil "github.com/bluesky-social/indigo/lex/util"
99+)
1010+1111+func init() {
1212+ lexutil.RegisterType("place.stream.live.teleport", &LiveTeleport{})
1313+}
1414+1515+type LiveTeleport struct {
1616+ LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.live.teleport"`
1717+ // durationSeconds: The time limit in seconds for the teleport. If not set, the teleport is permanent. Must be at least 60 seconds, and no more than 32,400 seconds (9 hours).
1818+ DurationSeconds *int64 `json:"durationSeconds,omitempty" cborgen:"durationSeconds,omitempty"`
1919+ // startsAt: The time the teleport becomes active.
2020+ StartsAt string `json:"startsAt" cborgen:"startsAt"`
2121+ // streamer: The DID of the streamer to teleport to.
2222+ Streamer string `json:"streamer" cborgen:"streamer"`
2323+}
+74
pkg/streamplace/richtextdefs.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+// Lexicon schema: place.stream.richtext.defs
44+55+package streamplace
66+77+import (
88+ "encoding/json"
99+ "fmt"
1010+1111+ appbsky "github.com/bluesky-social/indigo/api/bsky"
1212+ lexutil "github.com/bluesky-social/indigo/lex/util"
1313+)
1414+1515+// RichtextDefs_Censor is a "censor" in the place.stream.richtext.defs schema.
1616+//
1717+// Indicates that the text in the given index has been censored.
1818+type RichtextDefs_Censor struct {
1919+ LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.richtext.defs#censor"`
2020+ // categories: Categories of censored content
2121+ Categories []string `json:"categories,omitempty" cborgen:"categories,omitempty"`
2222+ Reason *string `json:"reason,omitempty" cborgen:"reason,omitempty"`
2323+}
2424+2525+// RichtextDefs_FacetView is a "facetView" in the place.stream.richtext.defs schema.
2626+//
2727+// Annotation of a sub-string within rich text.
2828+type RichtextDefs_FacetView struct {
2929+ Features []*RichtextDefs_FacetView_Features_Elem `json:"features" cborgen:"features"`
3030+ Index *appbsky.RichtextFacet_ByteSlice `json:"index" cborgen:"index"`
3131+}
3232+3333+type RichtextDefs_FacetView_Features_Elem struct {
3434+ RichtextFacet_Mention *appbsky.RichtextFacet_Mention
3535+ RichtextFacet_Link *appbsky.RichtextFacet_Link
3636+ RichtextDefs_Censor *RichtextDefs_Censor
3737+}
3838+3939+func (t *RichtextDefs_FacetView_Features_Elem) MarshalJSON() ([]byte, error) {
4040+ if t.RichtextFacet_Mention != nil {
4141+ t.RichtextFacet_Mention.LexiconTypeID = "app.bsky.richtext.facet#mention"
4242+ return json.Marshal(t.RichtextFacet_Mention)
4343+ }
4444+ if t.RichtextFacet_Link != nil {
4545+ t.RichtextFacet_Link.LexiconTypeID = "app.bsky.richtext.facet#link"
4646+ return json.Marshal(t.RichtextFacet_Link)
4747+ }
4848+ if t.RichtextDefs_Censor != nil {
4949+ t.RichtextDefs_Censor.LexiconTypeID = "place.stream.richtext.defs#censor"
5050+ return json.Marshal(t.RichtextDefs_Censor)
5151+ }
5252+ return nil, fmt.Errorf("can not marshal empty union as JSON")
5353+}
5454+5555+func (t *RichtextDefs_FacetView_Features_Elem) UnmarshalJSON(b []byte) error {
5656+ typ, err := lexutil.TypeExtract(b)
5757+ if err != nil {
5858+ return err
5959+ }
6060+6161+ switch typ {
6262+ case "app.bsky.richtext.facet#mention":
6363+ t.RichtextFacet_Mention = new(appbsky.RichtextFacet_Mention)
6464+ return json.Unmarshal(b, t.RichtextFacet_Mention)
6565+ case "app.bsky.richtext.facet#link":
6666+ t.RichtextFacet_Link = new(appbsky.RichtextFacet_Link)
6767+ return json.Unmarshal(b, t.RichtextFacet_Link)
6868+ case "place.stream.richtext.defs#censor":
6969+ t.RichtextDefs_Censor = new(RichtextDefs_Censor)
7070+ return json.Unmarshal(b, t.RichtextDefs_Censor)
7171+ default:
7272+ return nil
7373+ }
7474+}
+46-6
pkg/streamplace/streamlivestream.go
···5959}
60606161type Livestream_StreamplaceAnything_Livestream struct {
6262- Livestream_LivestreamView *Livestream_LivestreamView
6363- Livestream_ViewerCount *Livestream_ViewerCount
6464- Defs_BlockView *Defs_BlockView
6565- Defs_Renditions *Defs_Renditions
6666- Defs_Rendition *Defs_Rendition
6767- ChatDefs_MessageView *ChatDefs_MessageView
6262+ Livestream_LivestreamView *Livestream_LivestreamView
6363+ Livestream_ViewerCount *Livestream_ViewerCount
6464+ Livestream_TeleportArrival *Livestream_TeleportArrival
6565+ Livestream_TeleportCanceled *Livestream_TeleportCanceled
6666+ Defs_BlockView *Defs_BlockView
6767+ Defs_Renditions *Defs_Renditions
6868+ Defs_Rendition *Defs_Rendition
6969+ ChatDefs_MessageView *ChatDefs_MessageView
6870}
69717072func (t *Livestream_StreamplaceAnything_Livestream) MarshalJSON() ([]byte, error) {
···7577 if t.Livestream_ViewerCount != nil {
7678 t.Livestream_ViewerCount.LexiconTypeID = "place.stream.livestream#viewerCount"
7779 return json.Marshal(t.Livestream_ViewerCount)
8080+ }
8181+ if t.Livestream_TeleportArrival != nil {
8282+ t.Livestream_TeleportArrival.LexiconTypeID = "place.stream.livestream#teleportArrival"
8383+ return json.Marshal(t.Livestream_TeleportArrival)
8484+ }
8585+ if t.Livestream_TeleportCanceled != nil {
8686+ t.Livestream_TeleportCanceled.LexiconTypeID = "place.stream.livestream#teleportCanceled"
8787+ return json.Marshal(t.Livestream_TeleportCanceled)
7888 }
7989 if t.Defs_BlockView != nil {
8090 t.Defs_BlockView.LexiconTypeID = "place.stream.defs#blockView"
···108118 case "place.stream.livestream#viewerCount":
109119 t.Livestream_ViewerCount = new(Livestream_ViewerCount)
110120 return json.Unmarshal(b, t.Livestream_ViewerCount)
121121+ case "place.stream.livestream#teleportArrival":
122122+ t.Livestream_TeleportArrival = new(Livestream_TeleportArrival)
123123+ return json.Unmarshal(b, t.Livestream_TeleportArrival)
124124+ case "place.stream.livestream#teleportCanceled":
125125+ t.Livestream_TeleportCanceled = new(Livestream_TeleportCanceled)
126126+ return json.Unmarshal(b, t.Livestream_TeleportCanceled)
111127 case "place.stream.defs#blockView":
112128 t.Defs_BlockView = new(Defs_BlockView)
113129 return json.Unmarshal(b, t.Defs_BlockView)
···123139 default:
124140 return nil
125141 }
142142+}
143143+144144+// Livestream_TeleportArrival is a "teleportArrival" in the place.stream.livestream schema.
145145+type Livestream_TeleportArrival struct {
146146+ LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.livestream#teleportArrival"`
147147+ // chatProfile: The chat profile of the source streamer
148148+ ChatProfile *ChatProfile `json:"chatProfile,omitempty" cborgen:"chatProfile,omitempty"`
149149+ // source: The streamer who is teleporting their viewers here
150150+ Source *appbsky.ActorDefs_ProfileViewBasic `json:"source" cborgen:"source"`
151151+ // startsAt: When this teleport started
152152+ StartsAt string `json:"startsAt" cborgen:"startsAt"`
153153+ // teleportUri: The URI of the teleport record
154154+ TeleportUri string `json:"teleportUri" cborgen:"teleportUri"`
155155+ // viewerCount: How many viewers are arriving from this teleport
156156+ ViewerCount int64 `json:"viewerCount" cborgen:"viewerCount"`
157157+}
158158+159159+// Livestream_TeleportCanceled is a "teleportCanceled" in the place.stream.livestream schema.
160160+type Livestream_TeleportCanceled struct {
161161+ LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.livestream#teleportCanceled"`
162162+ // reason: Why this teleport was canceled
163163+ Reason string `json:"reason" cborgen:"reason"`
164164+ // teleportUri: The URI of the teleport record that was canceled
165165+ TeleportUri string `json:"teleportUri" cborgen:"teleportUri"`
126166}
127167128168// Livestream_ViewerCount is a "viewerCount" in the place.stream.livestream schema.