import { ensureDir } from "@std/fs"; import { serveDir, serveFile } from "@std/http/file-server"; import * as z from "@zod/mini"; import vento from "ventojs"; import { db } from "./db.ts"; import { getPdsUrl, resolveHandle } from "../common/identity.ts"; import type { VideoRecord } from "../common/lexicons.ts"; import { VIDEO_PATTERN } from "../common/routes.ts"; import { getVideoRecord } from "../common/video.ts"; const vto = vento({ includes: "./web" }); async function getOrFetchVideo(did: string, blobCid: string): Promise { const cached = db.getVideo(did, blobCid); if (cached) return cached; const pdsUrl = await getPdsUrl(did); if (!pdsUrl) throw new Error("Could not resolve PDS"); let filename: string = crypto.randomUUID(); const getBlobURL = new URL("/xrpc/com.atproto.sync.getBlob", pdsUrl); getBlobURL.searchParams.set("did", did); getBlobURL.searchParams.set("cid", blobCid); const blobResponse = await fetch(getBlobURL, { headers: { "user-agent": "video.cerulea.app" }, }); const contentType = blobResponse.headers.get("content-type"); if (contentType === "video/mp4") filename += ".mp4"; if (contentType === "video/webm") filename += ".webm"; await ensureDir("./data/videos"); using file = await Deno.open("./data/videos/" + filename, { write: true, createNew: true, }); await blobResponse.body?.pipeTo(file.writable); db.addVideo(did, blobCid, filename); return filename; } async function fetchVideo(req: Request): Promise { if (req.method === "OPTIONS") { return new Response(null, { headers: { "access-control-allow-origin": "*", }, }); } if (req.method !== "POST") { return new Response("Method Not Allowed", { status: 405 }); } const BodySchema = z.object({ repo: z.string().check(z.startsWith("did:")), blob: z.string(), }); const body = BodySchema.parse(await req.json()); if (!db.inAllowlist(body.repo)) { return new Response( JSON.stringify({ error: "Denied", message: "repo is not allowlisted on AppView", }), { status: 400, headers: { "content-type": "application/json", "access-control-allow-origin": "*", }, }, ); } const filename = await getOrFetchVideo(body.repo, body.blob); return new Response(JSON.stringify({ filename }), { headers: { "content-type": "application/json", "access-control-allow-origin": "*", }, }); } export default { async fetch(req: Request, _info): Promise { const pathname = new URL(req.url).pathname; if (pathname === "/xrpc/blue.cerulea.video.fetchVideo") { return await fetchVideo(req); } if (pathname.startsWith("/user-content/")) { return await serveDir(req, { fsRoot: "./data/videos", quiet: true, enableCors: true, urlRoot: "user-content", }); } const videoRoute = VIDEO_PATTERN.exec(req.url); if (videoRoute) { const { repo, rkey } = videoRoute.pathname.groups as Record<"repo" | "rkey", string>; const did = repo.startsWith("did:") ? repo : await resolveHandle(repo); if (!db.inAllowlist(did)) { return await serveFile(req, "./web/viewer.html"); } let record = db.getRecord(did, rkey); if (!record) { record = await getVideoRecord(did, rkey); db.addRecord(did, rkey, record); } let videoUrl: string | undefined; let videoType: string | undefined; if ("$type" in record.video && record.video?.$type === "blob") { const blobCid = record.video.ref.$link; const filename = await getOrFetchVideo(did, blobCid); videoUrl = `${new URL(req.url).origin}/user-content/${filename}`; videoType = record.video.mimeType; } const result = await vto.run("viewer.vto", { title: record.title, description: record.description, pageUrl: req.url, videoUrl, videoType, }); return new Response(result.content, { headers: { "content-type": "text/html; charset=utf-8" }, }); } return await serveDir(req, { fsRoot: "./web", quiet: true }); }, } satisfies Deno.ServeDefaultExport;