import { jsonError, jsonResponse } from "../../common.js"; import upload from "../../upload.js"; export async function onRequestPost(context: RequestContext) { const { description, files, senderTokenId, 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); } 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 || username.match(/\W/) ) 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>[] = []; const filesToProcess = []; for (const file of files) { const fileParts = file.name.split("."); let fileExten = fileParts.at(-1).toLowerCase(); if (fileExten === "mov") fileExten = "mp4"; if ( fileParts.length < 2 || !["mkv", "mp4", "wmv", "m4v", "gif", "webm"].includes(fileExten) ) 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( upload( context.env, ["mp4", "m4v", "webm"].includes(fileExten) ? fileUploadKey : `t/${fileUploadKey}`, file.size, fileExten, ), ); if (!["mp4", "m4v", "webm"].includes(fileExten)) { filesToProcess.push(fileUploadKey); } } 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; if (filesToProcess.length) await context.env.DATA.put( `coconutdata_${reportId}`, JSON.stringify({ attachments: filesToProcess, }), { expirationTtl: 1800, }, ); 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); attachments.push(new URL(urlResult.value).pathname.replace(/^\/?t?\//, "")); } await context.env.D1.prepare( "INSERT INTO reports (attachments, created_at, id, open, target_ids, target_usernames, user) VALUES (?, ?, ?, 1, ?, ?, ?);", ) .bind( JSON.stringify(attachments), Date.now(), reportId, JSON.stringify(metaIDs), JSON.stringify(metaNames), currentUser ? JSON.stringify(currentUser) : null, ) .run(); if (typeof senderTokenId === "string") await context.env.D1.prepare( "INSERT INTO push_notifications (created_at, event_id, event_type, token) VALUES (?, ?, ?, ?);", ) .bind(Date.now(), reportId, "report", senderTokenId) .run(); return jsonResponse( JSON.stringify({ id: reportId, upload_urls: uploadUrls }), ); }