import { GenerateUploadURL } from "../../gcloud.js"; import { jsonError, jsonResponse } from "../../common.js"; export async function onRequestPost(context: RequestContext) { const { actions, bypass, description, files, turnstileResponse, usernames } = context.data.body; if (!context.data.current_user) { if (typeof turnstileResponse !== "string") return jsonError("You must complete the captcha", 401); const turnstileAPIResponse = await fetch( "https://challenges.cloudflare.com/turnstile/v0/siteverify", { body: JSON.stringify({ remoteip: context.request.headers.get("CF-Connecting-IP"), response: turnstileResponse, secret: context.env.TURNSTILE_SECRETKEY, }), headers: { "content-type": "application/json", }, method: "POST", }, ); const { success }: { success: boolean } = await turnstileAPIResponse.json(); if (!success) return jsonError("Captcha test failed", 403); } const origin = context.request.headers.get("Origin"); if (!origin) return jsonError("No origin header", 400); if (bypass && !(context.data.current_user?.permissions & (1 << 5))) return jsonError("Bypass directive cannot be used", 403); if (typeof bypass !== "boolean") return jsonError("Bypass must be a boolean", 400); if (!Array.isArray(usernames)) return jsonError("Usernames must be type of array", 400); if ( !["string", "undefined"].includes(typeof description) || description?.length > 512 ) return jsonError("Invalid description", 400); if ( !Array.isArray(files) || files.find((file) => { const keys = Object.keys(file); return !keys.includes("name") || !keys.includes("size"); }) ) return jsonError("File list missing name(s) and/or size(s)", 400); if ( files.find( (file) => typeof file.name !== "string" || typeof file.size !== "number" || file.size < 0 || file.size > 536870912, ) ) return jsonError("One or more files contain an invalid name or size", 400); if (!usernames.length || usernames.length > 20) return jsonError( "Number of usernames provided must be between 1 and 20", 400, ); for (const username of usernames) { if ( username.length < 3 || username.length > 20 || username.match(/_/g)?.length > 1 ) return jsonError(`Username "${username}" is invalid`, 400); } const rbxSearchReq = await fetch( "https://users.roblox.com/v1/usernames/users", { body: JSON.stringify({ usernames, excludeBannedUsers: true, }), headers: { "content-type": "application/json", }, method: "POST", }, ); if (!rbxSearchReq.ok) return jsonError( "Failed to locate Roblox users due to upstream error", 500, ); const rbxSearchData: { data: { [k: string]: any }[] } = await rbxSearchReq.json(); if (rbxSearchData.data.length < usernames.length) { const missingUsers = []; for (const userData of rbxSearchData.data) { if (!usernames.includes(userData.requestedUsername)) missingUsers.push(userData.requestedUsername); } return jsonError( `The following users do not exist or are banned from Roblox: ${missingUsers.toString()}`, 400, ); } const metaIDs = []; const metaNames = []; for (const data of rbxSearchData.data) { metaIDs.push(data.id); metaNames.push(data.name); } const uploadUrlPromises: Promise<string>[] = []; for (const file of files) { const fileParts = file.name.split("."); let fileExten = fileParts.at(-1); if (fileExten.toLowerCase() === "mov") fileExten = "mp4"; if ( fileParts.length < 2 || !["mkv", "mp4", "wmv", "m4v", "gif", "webm"].includes( fileExten.toLowerCase(), ) ) return jsonError( `File ${file.name} cannot be uploaded as it is unsupported`, 415, ); const fileUploadKey = `${crypto.randomUUID().replaceAll("-", "")}/${crypto .randomUUID() .replaceAll("-", "")}${context.request.headers.get( "cf-ray", )}${Date.now()}`; uploadUrlPromises.push( GenerateUploadURL( context.env, `t/${fileUploadKey}`, file.size, fileExten, origin, ), ); } const uploadUrlResults = await Promise.allSettled(uploadUrlPromises); const reportId = `${Date.now()}${context.request.headers.get( "cf-ray", )}${crypto.randomUUID().replaceAll("-", "")}`; const { current_user: currentUser } = context.data; await context.env.DATA.put( `reportprocessing_${reportId}`, currentUser?.id || context.request.headers.get("CF-Connecting-IP"), { expirationTtl: 3600 }, ); if (uploadUrlResults.find((uploadUrl) => uploadUrl.status === "rejected")) return jsonError("Failed to generate upload url", 500); const attachments: string[] = []; const uploadUrls: string[] = []; for (const urlResult of uploadUrlResults as PromiseFulfilledResult<string>[]) { uploadUrls.push(urlResult.value); let url = new URL(urlResult.value).searchParams.get("name") as string; const extension = (url.split(".").at(-1) as string).toLowerCase(); if (["mkv", "wmv"].includes(extension)) { url = url.replace(`.${extension}`, ".mp4"); await context.env.DATA.put(`videoprocessing_${url}`, "1", { expirationTtl: 600, }); } attachments.push(url); } await context.env.DATA.put( `report_${reportId}`, JSON.stringify({ attachments, created_at: Date.now(), id: reportId, open: !bypass, user: currentUser ? { email: currentUser.email, id: currentUser.id, username: currentUser.username, } : null, target_ids: metaIDs, target_usernames: metaNames, }), ); try { await context.env.D1.prepare( "INSERT INTO reports (created_at, id, open, user) VALUES (?, ?, ?, ?);", ) .bind(Date.now(), reportId, Number(!bypass), currentUser?.id || null) .run(); } catch {} return jsonResponse( JSON.stringify({ id: reportId, upload_urls: uploadUrls }), ); }