244 lines
6.3 KiB
TypeScript
244 lines
6.3 KiB
TypeScript
import { jsonError, jsonResponse } from "../../common.js";
|
|
import upload from "../../upload.js";
|
|
|
|
export async function onRequestPost(context: RequestContext) {
|
|
const {
|
|
actions,
|
|
bypass,
|
|
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);
|
|
}
|
|
|
|
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 ||
|
|
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) ? "" : "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.DATA.put(
|
|
`report_${reportId}`,
|
|
JSON.stringify({
|
|
attachments,
|
|
created_at: Date.now(),
|
|
fcm_token: typeof senderTokenId === "string" ? senderTokenId : undefined,
|
|
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 }),
|
|
);
|
|
}
|