video.cerulea.blue/appview/main.ts

132 lines
3.5 KiB
TypeScript

import { ensureDir } from "@std/fs";
import { serveDir, serveFile } from "@std/http/file-server";
import * as z from "@zod/mini";
import { db } from "./db.ts";
import {
CompositeDidDocumentResolver,
PlcDidDocumentResolver,
WebDidDocumentResolver,
} from "@atcute/identity-resolver";
import { VIDEO_PATTERN } from "../common/routes.ts";
const didResolver = new CompositeDidDocumentResolver({
methods: {
plc: new PlcDidDocumentResolver(),
web: new WebDidDocumentResolver(),
},
});
async function fetchVideo(req: Request): Promise<Response> {
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());
const existingVideo = db.getVideo(body.repo, body.blob);
if (existingVideo !== undefined) {
return new Response(JSON.stringify({ filename: existingVideo }), {
headers: {
"content-type": "application/json",
"access-control-allow-origin": "*",
},
});
}
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 doc = await didResolver.resolve(
body.repo as `did:${"plc" | "web"}:${string}`
);
const pdsBaseURL = doc.service?.find(
(it) => it.id === "#atproto_pds"
)?.serviceEndpoint;
if (!pdsBaseURL || typeof pdsBaseURL !== "string")
return new Response(null, { status: 400 });
let filename: string = crypto.randomUUID();
{
const getBlobURL = new URL("/xrpc/com.atproto.sync.getBlob", pdsBaseURL);
getBlobURL.searchParams.set("did", body.repo);
getBlobURL.searchParams.set("cid", body.blob);
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(body.repo, body.blob, filename);
}
return new Response(JSON.stringify({ filename }), {
headers: {
"content-type": "application/json",
"access-control-allow-origin": "*",
},
});
}
export default {
async fetch(req: Request, _info): Promise<Response> {
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) {
return await serveFile(req, "./web/viewer.html");
}
return await serveDir(req, { fsRoot: "./web", quiet: true });
},
} satisfies Deno.ServeDefaultExport;