Live video on the AT Protocol
1import {
2 getPDSServiceEndpoint,
3 resolveDIDDocument,
4} from "@streamplace/components";
5import { AppStore } from "store";
6import { StateCreator } from "zustand";
7import { BlueskySlice } from "./blueskySlice";
8
9export interface ContentMetadataSlice {
10 creating: boolean;
11 updating: boolean;
12 error: string | null;
13 lastCreatedRecord: any | null;
14 // actions
15 createContentMetadata: (params: {
16 contentWarnings?: string[];
17 distributionPolicy?: { deleteAfter?: number };
18 contentRights?: {
19 creator?: string;
20 copyrightNotice?: string;
21 copyrightYear?: number;
22 license?: string;
23 creditLine?: string;
24 };
25 }) => Promise<void>;
26 updateContentMetadata: (params: {
27 rkey?: string;
28 livestreamRef?: { uri: string; cid: string };
29 contentWarnings?: string[];
30 distributionPolicy?: { deleteAfter?: number };
31 contentRights?: {
32 creator?: string;
33 copyrightNotice?: string;
34 copyrightYear?: number;
35 license?: string;
36 creditLine?: string;
37 };
38 }) => Promise<void>;
39 getContentMetadata: (params?: {
40 userDid?: string;
41 rkey?: string;
42 }) => Promise<void>;
43 clearError: () => void;
44}
45
46export const createContentMetadataSlice: StateCreator<
47 AppStore,
48 [],
49 [],
50 ContentMetadataSlice
51> = (set, get) => ({
52 creating: false,
53 updating: false,
54 error: null,
55 lastCreatedRecord: null,
56
57 createContentMetadata: async ({
58 contentWarnings = [],
59 distributionPolicy = { deleteAfter: undefined },
60 contentRights = {},
61 }) => {
62 set({ creating: true, error: null });
63 try {
64 // need access to bluesky slice - will handle in combined store
65 const state = get() as any;
66 const bluesky: BlueskySlice = state;
67
68 if (!bluesky.pdsAgent) {
69 throw new Error("No agent");
70 }
71
72 const did = bluesky.oauthSession?.did;
73 if (!did) {
74 throw new Error("No DID");
75 }
76
77 const metadataRecord = {
78 $type: "place.stream.metadata.configuration",
79 createdAt: new Date().toISOString(),
80 ...(contentWarnings.length > 0 && {
81 contentWarnings: { warnings: contentWarnings },
82 }),
83 ...(distributionPolicy.deleteAfter && { distributionPolicy }),
84 ...(contentRights &&
85 Object.keys(contentRights).length > 0 && {
86 contentRights,
87 }),
88 };
89
90 const result = await bluesky.pdsAgent.com.atproto.repo.createRecord({
91 repo: did,
92 collection: "place.stream.metadata.configuration",
93 rkey: "self",
94 record: metadataRecord,
95 });
96
97 const rkey = result.data.uri.split("/").pop();
98
99 set({
100 creating: false,
101 error: null,
102 lastCreatedRecord: {
103 record: metadataRecord,
104 uri: result.data.uri,
105 cid: result.data.cid,
106 rkey,
107 },
108 });
109 } catch (error) {
110 set({
111 creating: false,
112 error: error?.message ?? "Failed to create content metadata",
113 });
114 }
115 },
116
117 updateContentMetadata: async ({
118 rkey,
119 livestreamRef,
120 contentWarnings = [],
121 distributionPolicy = { deleteAfter: undefined },
122 contentRights = {},
123 }) => {
124 set({ updating: true, error: null });
125 try {
126 const state = get() as any;
127 const bluesky: BlueskySlice = state;
128
129 if (!bluesky.pdsAgent) {
130 throw new Error("No agent");
131 }
132
133 const did = bluesky.oauthSession?.did;
134 if (!did) {
135 throw new Error("No DID");
136 }
137
138 const metadataRecord = {
139 $type: "place.stream.metadata.configuration",
140 ...(livestreamRef && { livestreamRef }),
141 createdAt: new Date().toISOString(),
142 ...(contentWarnings.length > 0 && {
143 contentWarnings: { warnings: contentWarnings },
144 }),
145 ...(distributionPolicy.deleteAfter && { distributionPolicy }),
146 ...(contentRights &&
147 Object.keys(contentRights).length > 0 && {
148 contentRights,
149 }),
150 };
151
152 const result = await bluesky.pdsAgent.com.atproto.repo.putRecord({
153 repo: did,
154 collection: "place.stream.metadata.configuration",
155 rkey: "self",
156 record: metadataRecord,
157 });
158
159 set({
160 updating: false,
161 error: null,
162 lastCreatedRecord: {
163 record: metadataRecord,
164 uri: `at://${did}/place.stream.metadata.configuration/self`,
165 cid: result.data.cid,
166 },
167 });
168 } catch (error) {
169 set({
170 updating: false,
171 error: error?.message ?? "Failed to update content metadata",
172 });
173 }
174 },
175
176 getContentMetadata: async ({ userDid, rkey = "self" } = {}) => {
177 set({ error: null });
178 try {
179 const state = get() as any;
180 const bluesky: BlueskySlice = state;
181
182 if (!bluesky.pdsAgent) {
183 throw new Error("No agent");
184 }
185
186 const targetDid = userDid || bluesky.oauthSession?.did;
187 if (!targetDid) {
188 throw new Error("No DID provided or user not authenticated");
189 }
190
191 console.log(`[getContentMetadata] Debug info:`, {
192 targetDid,
193 rkey,
194 pdsAgentType: bluesky.pdsAgent.constructor.name,
195 hasOAuthSession: !!bluesky.oauthSession,
196 currentUserDid: bluesky.oauthSession?.did,
197 pdsAgentHost:
198 (bluesky.pdsAgent as any)?.host ||
199 (bluesky.pdsAgent as any)?.service?.host ||
200 "unknown",
201 pdsAgentUrl:
202 (bluesky.pdsAgent as any)?.url ||
203 (bluesky.pdsAgent as any)?.service?.url ||
204 "unknown",
205 });
206
207 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 );
216 } catch (pdsResolveError) {
217 console.log(
218 `[getContentMetadata] Failed to resolve PDS for ${targetDid}:`,
219 pdsResolveError,
220 );
221 }
222
223 let agent = bluesky.pdsAgent;
224 if (targetPDS && targetPDS !== (bluesky.pdsAgent as any)?.host) {
225 const { StreamplaceAgent } = await import("streamplace");
226 agent = new StreamplaceAgent(targetPDS) as any;
227 console.log(
228 `[getContentMetadata] Created new agent for PDS:`,
229 targetPDS,
230 );
231 }
232
233 console.log(`[getContentMetadata] Attempting to fetch record from:`, {
234 repo: targetDid,
235 collection: "place.stream.metadata.configuration",
236 rkey,
237 usingPDS: targetPDS || "default",
238 });
239
240 const result = await agent.com.atproto.repo.getRecord({
241 repo: targetDid,
242 collection: "place.stream.metadata.configuration",
243 rkey,
244 });
245
246 console.log(`[getContentMetadata] API response:`, result);
247
248 if (!result.success) {
249 throw new Error("Failed to get content metadata record");
250 }
251
252 set({
253 error: null,
254 lastCreatedRecord: {
255 userDid: targetDid,
256 record: result.data.value,
257 uri: result.data.uri,
258 cid: result.data.cid,
259 },
260 });
261 } catch (error) {
262 console.log(`[getContentMetadata] Error details:`, {
263 error: error.message,
264 errorType: error.constructor.name,
265 errorStack: error.stack,
266 });
267
268 if (
269 error.message?.includes("not found") ||
270 error.message?.includes("RecordNotFound")
271 ) {
272 set({
273 error: null,
274 lastCreatedRecord: {
275 userDid: targetDid,
276 record: null,
277 uri: null,
278 cid: null,
279 },
280 });
281 return;
282 }
283 throw error;
284 }
285 } catch (error) {
286 set({
287 error: error?.message ?? "Failed to get content metadata",
288 });
289 }
290 },
291
292 clearError: () => {
293 set({ error: null });
294 },
295});