diff --git a/js/components/src/components/chat/chat-box.tsx b/js/components/src/components/chat/chat-box.tsx index 3eb1adce..6047092e 100644 --- a/js/components/src/components/chat/chat-box.tsx +++ b/js/components/src/components/chat/chat-box.tsx @@ -182,7 +182,7 @@ export function ChatBox({ userDID, targetHandle, countdownSeconds, - linfo ? { uri: linfo.uri, cid: linfo.cid } : { uri: "", cid: "" }, + linfo ? { uri: linfo.uri, cid: linfo.cid } : undefined, setActiveTeleportUri, ); diff --git a/js/components/src/lib/slash-commands/teleport.ts b/js/components/src/lib/slash-commands/teleport.ts index 981d8ce0..1bccbc89 100644 --- a/js/components/src/lib/slash-commands/teleport.ts +++ b/js/components/src/lib/slash-commands/teleport.ts @@ -25,7 +25,7 @@ export async function createTeleport( userDID: string, targetHandle: string, countdownSeconds: number, - livestream: { uri: string; cid: string }, + livestream?: { uri: string; cid: string } | null, setActiveTeleportUri?: (uri: string | null) => void, ): Promise<{ success: boolean; error?: string }> { if (countdownSeconds < 5 || countdownSeconds > 300) { @@ -35,13 +35,6 @@ export async function createTeleport( }; } - if (!livestream?.uri || !livestream?.cid) { - return { - success: false, - error: "No active livestream to teleport from", - }; - } - let targetDID: string; try { const resolution = await pdsAgent.resolveHandle({ @@ -64,17 +57,22 @@ export async function createTeleport( const startsAt = new Date(Date.now() + countdownSeconds * 1000).toISOString(); + // The `livestream` strongRef is optional: when present it pins the source + // stream so the server can end exactly that record on arrival. When absent + // (e.g. no active livestream) the teleport still sends viewers over; the + // server just won't end a source stream. + const record: Record = { + streamer: targetDID, + startsAt: startsAt, + }; + if (livestream?.uri && livestream?.cid) { + record.livestream = { uri: livestream.uri, cid: livestream.cid }; + } + try { const result = await pdsAgent.client.create( place.stream.live.teleport, - { - streamer: targetDID as any, - startsAt: startsAt as any, - livestream: { - uri: livestream.uri, - cid: livestream.cid, - } as any, - }, + record as any, { repo: userDID as any }, ); @@ -94,7 +92,7 @@ export async function createTeleport( export function registerTeleportCommand( pdsAgent: StreamplaceAgent, userDID: string, - getLivestream: () => { uri: string; cid: string } | null, + getLivestream?: () => { uri: string; cid: string } | null, setActiveTeleportUri?: (uri: string | null) => void, onOpenModal?: () => void, ) { @@ -144,13 +142,11 @@ export function registerTeleportCommand( countdownSeconds = parsedDuration; } - const livestream = getLivestream(); - if (!livestream?.uri || !livestream?.cid) { - return { - handled: true, - error: "No active livestream to teleport from", - }; - } + // The `livestream` strongRef is optional: when present it pins the source + // stream so the server can end exactly that record on arrival. When absent + // the teleport still sends viewers over; the server just won't end a + // source stream. + const livestream = getLivestream?.() ?? null; let targetDID: string; try { @@ -176,17 +172,18 @@ export function registerTeleportCommand( Date.now() + countdownSeconds * 1000, ).toISOString(); + const record: Record = { + streamer: targetDID, + startsAt: startsAt, + }; + if (livestream?.uri && livestream?.cid) { + record.livestream = { uri: livestream.uri, cid: livestream.cid }; + } + try { const result = await pdsAgent.client.create( place.stream.live.teleport, - { - streamer: targetDID as any, - startsAt: startsAt as any, - livestream: { - uri: livestream.uri, - cid: livestream.cid, - } as any, - }, + record as any, { repo: userDID as any }, ); diff --git a/js/docs/src/content/docs/lex-reference/live/place-stream-live-teleport.md b/js/docs/src/content/docs/lex-reference/live/place-stream-live-teleport.md index e4389ebd..08f64343 100644 --- a/js/docs/src/content/docs/lex-reference/live/place-stream-live-teleport.md +++ b/js/docs/src/content/docs/lex-reference/live/place-stream-live-teleport.md @@ -23,7 +23,7 @@ Record defining a 'teleport', that is active during a certain time. | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | `streamer` | `string` | ✅ | The DID of the streamer to teleport to. | Format: `did` | | `startsAt` | `string` | ✅ | The time the teleport becomes active. | Format: `datetime` | -| `livestream` | [`com.atproto.repo.strongRef`](https://github.com/bluesky-social/atproto/tree/main/lexicons/com/atproto/repo/strongref.json#undefined) | ✅ | The source livestream this teleport is sending viewers away from. When the teleport fires, this is the livestream that gets ended (the same update place.stream.live.stopLivestream performs), so the source streamer returns to pre-live. Teleports without an origin livestream are treated as a no-op. | | +| `livestream` | [`com.atproto.repo.strongRef`](https://github.com/bluesky-social/atproto/tree/main/lexicons/com/atproto/repo/strongref.json#undefined) | ❌ | The source livestream this teleport is sending viewers away from. When the teleport fires, this is the livestream that gets ended (the same update place.stream.live.stopLivestream performs), so the source streamer returns to pre-live. Teleports without an origin livestream are treated as a no-op. | | | `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
Max: 32400 | --- @@ -41,7 +41,7 @@ Record defining a 'teleport', that is active during a certain time. "description": "Record defining a 'teleport', that is active during a certain time.", "record": { "type": "object", - "required": ["streamer", "startsAt", "livestream"], + "required": ["streamer", "startsAt"], "properties": { "streamer": { "type": "string", diff --git a/lexicons/place/stream/live/teleport.json b/lexicons/place/stream/live/teleport.json index a100db17..1ae0986b 100644 --- a/lexicons/place/stream/live/teleport.json +++ b/lexicons/place/stream/live/teleport.json @@ -8,7 +8,7 @@ "description": "Record defining a 'teleport', that is active during a certain time.", "record": { "type": "object", - "required": ["streamer", "startsAt", "livestream"], + "required": ["streamer", "startsAt"], "properties": { "streamer": { "type": "string", diff --git a/pkg/atproto/teleport_endstream.go b/pkg/atproto/teleport_endstream.go index 9e0ace19..840430c5 100644 --- a/pkg/atproto/teleport_endstream.go +++ b/pkg/atproto/teleport_endstream.go @@ -42,17 +42,16 @@ import ( // arrival notification (the caller publishes that before invoking this). It is a // no-op when there is no stored session, no origin livestream strongRef, the // referenced record is gone, or the livestream is already ended. -func (atsync *ATProtoSynchronizer) endLivestreamForTeleport(ctx context.Context, repoDID string, livestreamRef comatproto.RepoStrongRef) { - ctx = log.WithLogValues(ctx, "func", "endLivestreamForTeleport", "repoDID", repoDID, "livestreamUri", livestreamRef.Uri) - - // A teleport without an origin livestream strongRef (e.g. a record from - // before the field existed) cannot be tied to a specific stream. Rather - // than guess via "latest livestream" — which can terminate an unrelated, - // newer broadcast — treat it as a no-op. - if livestreamRef.Uri == "" { - log.Debug(ctx, "teleport has no origin livestream strongRef, skipping stream-end") +func (atsync *ATProtoSynchronizer) endLivestreamForTeleport(ctx context.Context, repoDID string, livestreamRef *comatproto.RepoStrongRef) { + // A teleport without an origin livestream strongRef (the field is + // optional, and records from before it existed won't have one) cannot be + // tied to a specific stream. Rather than guess via "latest livestream" — + // which can terminate an unrelated, newer broadcast — treat it as a no-op. + if livestreamRef == nil || livestreamRef.Uri == "" { + log.Debug(ctx, "teleport has no origin livestream strongRef, skipping stream-end", "repoDID", repoDID) return } + ctx = log.WithLogValues(ctx, "func", "endLivestreamForTeleport", "repoDID", repoDID, "livestreamUri", livestreamRef.Uri) // A teleport record can arrive before the streamer has ever logged in to // this node (e.g. multi-node setups). Without a stored session we have no @@ -84,7 +83,7 @@ func (atsync *ATProtoSynchronizer) endLivestreamForTeleport(ctx context.Context, return } - if err := atsync.endReferencedLivestream(ctx, repoDID, livestreamRef, client); err != nil { + if err := atsync.endReferencedLivestream(ctx, repoDID, *livestreamRef, client); err != nil { log.Error(ctx, "failed to end livestream for teleport", "err", err) return } diff --git a/pkg/placestream/liveteleport.go b/pkg/placestream/liveteleport.go index 2196758d..bfee327a 100644 --- a/pkg/placestream/liveteleport.go +++ b/pkg/placestream/liveteleport.go @@ -23,7 +23,7 @@ type LiveTeleport struct { // 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). DurationSeconds *int64 `json:"durationSeconds,omitempty"` // livestream: The source livestream this teleport is sending viewers away from. When the teleport fires, this is the livestream that gets ended (the same update place.stream.live.stopLivestream performs), so the source streamer returns to pre-live. Teleports without an origin livestream are treated as a no-op. - Livestream comatproto.RepoStrongRef `json:"livestream"` + Livestream *comatproto.RepoStrongRef `json:"livestream,omitempty"` // startsAt: The time the teleport becomes active. StartsAt string `json:"startsAt"` // streamer: The DID of the streamer to teleport to.