import { ORPCError } from "@orpc/server"; import type { MediaCapabilities, MediaRuntime } from "@rakazo/adapter-kit"; import type { EncryptedSecretStore } from "@rakazo/adapters"; import { loadMediaSettings, MediaJobRejectedError, MediaSettingsInvalidError, mediaSettingsView, saveMediaSettings, } from "@rakazo/adapters"; import type { Actor, MediaJobStartInput, MediaSettingsUpdate } from "@rakazo/contracts"; import { MediaOperationSchema } from "@rakazo/contracts"; import type { PrismaClient } from "@rakazo/db"; import { IsolationError, requireMembership } from "@rakazo/db"; import { resolveThreadTarget } from "./thread-target.js"; export interface MediaRouteDeps { prisma: PrismaClient; secrets: EncryptedSecretStore; media?: MediaRuntime; } /** Media backends run on the deployment owner's own server, so only they configure them. */ function requireDeploymentOwner(actor: Actor) { if (!actor.isDeploymentOwner) throw new ORPCError("FORBIDDEN"); } function requireMedia(deps: MediaRouteDeps): MediaRuntime { if (!deps.media) throw new ORPCError("NOT_IMPLEMENTED"); return deps.media; } export async function getMediaSettings(deps: MediaRouteDeps, actor: Actor) { requireDeploymentOwner(actor); return mediaSettingsView(await loadMediaSettings(deps)); } export async function updateMediaSettings( deps: MediaRouteDeps, actor: Actor, input: MediaSettingsUpdate, ) { requireDeploymentOwner(actor); try { return await saveMediaSettings(deps, input); } catch (error) { if (error instanceof MediaSettingsInvalidError) { throw new ORPCError("BAD_REQUEST", { message: error.message }); } throw error; } } /** * The actor in the space a card lives in. A card is acted on where it is shown, which need not be * the space the client has selected (the main conversation lives in the home space), so the * space comes from the card's own record: it must belong to the actor, and the actor must still * be a member of that space. Anything else reads as not found. */ async function actorInSpace(deps: MediaRouteDeps, actor: Actor, spaceId: string): Promise { if (spaceId === actor.spaceId) return actor; try { return await requireMembership(deps.prisma, actor.userId, spaceId); } catch (error) { if (error instanceof IsolationError) throw new ORPCError("NOT_FOUND"); throw error; } } /** * Starts an operation a user picked on a card (export, transcribe, edit) in the card's * conversation. The conversation is authorized like every thread procedure, in the card's space; * the runtime checks that the operation's source artifacts belong to the actor there, and the * job can only ever read the actor's artifacts of that space. */ export async function startMediaJob(deps: MediaRouteDeps, actor: Actor, input: MediaJobStartInput) { const media = requireMedia(deps); const source = await deps.prisma.message.findFirst({ where: { id: input.messageId, thread: { userId: actor.userId, ...(input.botId ? { botId: input.botId } : { groupId: input.groupId }), }, }, select: { botId: true, thread: { select: { spaceId: true } } }, }); if (!source) throw new ORPCError("NOT_FOUND"); const scoped = await actorInSpace(deps, actor, source.thread.spaceId); const target = await resolveThreadTarget(deps.prisma, scoped, input).catch((error) => { if (error instanceof IsolationError) throw new ORPCError("NOT_FOUND"); throw error; }); const bots = target.kind === "bot" ? [target.botId] : [ ...(target.leadBotId && target.memberBotIds.includes(target.leadBotId) ? [target.leadBotId] : []), ...target.memberBotIds, ]; const botId = source.botId && bots.includes(source.botId) ? source.botId : bots[0]; if (!botId) throw new ORPCError("NOT_FOUND"); try { return await media.start({ operation: input.operation, input: input.input, spaceId: scoped.spaceId, userId: scoped.userId, botId, groupId: target.kind === "group" ? target.groupId : null, threadId: target.threadId, runId: null, }); } catch (error) { if (error instanceof MediaJobRejectedError) { throw new ORPCError("BAD_REQUEST", { message: error.message }); } throw error; } } /** * Booleans only: which operations a space can run, never how they are configured. With a job, * the space is the job's (a card's follow-ups); otherwise the actor's current space. */ export async function getMediaCapabilities( deps: MediaRouteDeps, actor: Actor, input: { jobId?: string } = {}, ): Promise { const spaceId = input.jobId ? (await ownedJob(deps, actor, input.jobId)).spaceId : actor.spaceId; if (deps.media) return deps.media.capabilities(spaceId); return Object.fromEntries( MediaOperationSchema.options.map((operation) => [operation, false]), ) as MediaCapabilities; } /** The actor's job, in whichever of the actor's spaces it runs. */ async function ownedJob(deps: MediaRouteDeps, actor: Actor, jobId: string) { const job = await deps.prisma.mediaJob.findFirst({ where: { id: jobId, userId: actor.userId }, select: { id: true, spaceId: true }, }); if (!job) throw new ORPCError("NOT_FOUND"); await actorInSpace(deps, actor, job.spaceId); return job; } export async function getMediaJob(deps: MediaRouteDeps, actor: Actor, jobId: string) { const media = requireMedia(deps); const snapshot = await media.get((await ownedJob(deps, actor, jobId)).id); if (!snapshot) throw new ORPCError("NOT_FOUND"); return snapshot; } export async function cancelMediaJob(deps: MediaRouteDeps, actor: Actor, jobId: string) { const media = requireMedia(deps); const { id } = await ownedJob(deps, actor, jobId); await media.cancel(id); const snapshot = await media.get(id); if (!snapshot) throw new ORPCError("NOT_FOUND"); return snapshot; }