263 lines
6.6 KiB
TypeScript

import { GenerateUploadURL } from "../../gcloud.js";
function errorResponse(error: string, status: number): Response {
return new Response(JSON.stringify({ error }), {
headers: {
"content-type": "application/json",
},
status,
});
}
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 errorResponse("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 errorResponse("Captcha test failed", 403);
}
const origin = context.request.headers.get("Origin");
if (!origin) return errorResponse("No origin header", 400);
if (bypass && !(context.data.current_user?.permissions & (1 << 5)))
return errorResponse("Bypass directive cannot be used", 403);
if (typeof bypass !== "boolean")
return errorResponse("Bypass must be a boolean", 400);
if (!Array.isArray(usernames))
return errorResponse("Usernames must be type of array", 400);
if (
!["string", "undefined"].includes(typeof description) ||
description?.length > 512
)
return errorResponse("Invalid description", 400);
if (
!Array.isArray(files) ||
files.find((file) => {
const keys = Object.keys(file);
return !keys.includes("name") || !keys.includes("size");
})
)
return errorResponse("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 errorResponse(
"One or more files contain an invalid name or size",
400,
);
if (!usernames.length || usernames.length > 20)
return errorResponse(
"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 errorResponse(`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 errorResponse(
"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 errorResponse(
`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 filePartes = file.name.split(".");
let fileExten = filePartes.at(-1);
if (
filePartes.length < 2 ||
![
"mkv",
"mp4",
"wmv",
"jpg",
"png",
"m4v",
"jpeg",
"jfif",
"gif",
"webm",
"heif",
"heic",
"webp",
"mov",
].includes(fileExten.toLowerCase())
)
return errorResponse(
`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 errorResponse("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 = urlResult.toString().replace("t/", "");
const extension = (url.split(".").at(-1) as string).toLowerCase();
if (["mkv", "mov", "wmv"].includes(extension)) {
// These are merely glorified mp4 containers
if (extension !== "mov")
await context.env.DATA.put(`videoprocessing_${url}.${extension}`, "1", {
expirationTtl: 600,
});
url = url.replace(`.${extension}`, ".mp4");
}
attachments.push(url);
}
await context.env.DATA.put(
`report_${reportId}`,
JSON.stringify({
attachments,
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 new Response(
JSON.stringify({ id: reportId, upload_urls: uploadUrls }),
{
headers: {
"content-type": "application/json",
},
},
);
}