116 lines
3.2 KiB
TypeScript
116 lines
3.2 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 { resolveDid } from "../common/identity.ts";
|
|
import { VIDEO_PATTERN } from "../common/routes.ts";
|
|
|
|
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 resolveDid(body.repo);
|
|
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;
|