358 lines
9.3 KiB
TypeScript
358 lines
9.3 KiB
TypeScript
import {
|
|
Button,
|
|
CircularProgress,
|
|
CircularProgressLabel,
|
|
Container,
|
|
FormControl,
|
|
FormLabel,
|
|
Heading,
|
|
HStack,
|
|
Input,
|
|
Link,
|
|
Text,
|
|
Textarea,
|
|
useToast,
|
|
} from "@chakra-ui/react";
|
|
import { useEffect, useState } from "react";
|
|
import { useLoaderData } from "@remix-run/react";
|
|
import Success from "../../components/Success.js";
|
|
|
|
export async function loader({
|
|
context,
|
|
}: {
|
|
context: RequestContext;
|
|
}): Promise<{ logged_in: boolean; site_key: string }> {
|
|
return {
|
|
logged_in: Boolean(context.data.current_user),
|
|
site_key: context.env.TURNSTILE_SITEKEY,
|
|
};
|
|
}
|
|
|
|
export function meta() {
|
|
return [
|
|
{
|
|
title: "Report an Exploiter - Car Crushers",
|
|
},
|
|
{
|
|
name: "description",
|
|
content: "Use this page to report a cheater",
|
|
},
|
|
];
|
|
}
|
|
|
|
export default function () {
|
|
const [fileProgress, setFileProgress] = useState(0);
|
|
const [showSuccess, setShowSuccess] = useState(false);
|
|
const toast = useToast();
|
|
const [uploading, setUploading] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const fileTypes: { [k: string]: string } = {
|
|
gif: "image/gif",
|
|
m4v: "video/x-m4v",
|
|
mkv: "video/x-matroska",
|
|
mov: "video/mp4",
|
|
mp4: "video/mp4",
|
|
webm: "video/webm",
|
|
wmv: "video/x-ms-wmv",
|
|
};
|
|
|
|
const { logged_in, site_key } = useLoaderData<typeof loader>();
|
|
|
|
async function submit() {
|
|
setLoading(true);
|
|
const usernames = (
|
|
document.getElementById("usernames") as HTMLInputElement
|
|
).value
|
|
.replaceAll(" ", "")
|
|
.split(",");
|
|
const files = (document.getElementById("evidence") as HTMLInputElement)
|
|
.files;
|
|
|
|
if (!usernames.length) {
|
|
setLoading(false);
|
|
return toast({
|
|
description: "Must provide at least one username",
|
|
isClosable: true,
|
|
status: "error",
|
|
title: "Error",
|
|
});
|
|
}
|
|
|
|
if (!files?.length) {
|
|
setLoading(false);
|
|
return toast({
|
|
description: "Must attach at least one file",
|
|
isClosable: true,
|
|
status: "error",
|
|
title: "Error",
|
|
});
|
|
}
|
|
|
|
if (usernames.length > 20) {
|
|
setLoading(false);
|
|
return toast({
|
|
description: "Only up to twenty users can be reported at a time",
|
|
isClosable: true,
|
|
status: "error",
|
|
title: "Too Many Usernames",
|
|
});
|
|
}
|
|
|
|
let turnstileToken = "";
|
|
|
|
if (!logged_in) {
|
|
const tokenElem = document
|
|
.getElementsByName("cf-turnstile-response")
|
|
.item(0) as HTMLInputElement;
|
|
|
|
if (!tokenElem.value) {
|
|
setLoading(false);
|
|
return toast({
|
|
description: "Please complete the captcha and try again",
|
|
isClosable: true,
|
|
status: "error",
|
|
title: "Captcha not completed",
|
|
});
|
|
}
|
|
|
|
turnstileToken = tokenElem.value;
|
|
}
|
|
|
|
const description = (
|
|
document.getElementById("description") as HTMLTextAreaElement
|
|
).value;
|
|
|
|
const filelist = [];
|
|
|
|
for (const file of files) {
|
|
filelist.push({ name: file.name, size: file.size });
|
|
}
|
|
|
|
const submitReq = await fetch("/api/reports/submit", {
|
|
body: JSON.stringify({
|
|
description: description || undefined,
|
|
files: filelist,
|
|
turnstileResponse: logged_in ? undefined : turnstileToken,
|
|
usernames,
|
|
}),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
method: "POST",
|
|
});
|
|
|
|
if (!submitReq.ok) {
|
|
setLoading(false);
|
|
if (!logged_in) {
|
|
try {
|
|
// @ts-expect-error
|
|
turnstile.reset();
|
|
} catch {}
|
|
}
|
|
|
|
return toast({
|
|
description: ((await submitReq.json()) as { error: string }).error,
|
|
isClosable: true,
|
|
status: "error",
|
|
title: "Error",
|
|
});
|
|
}
|
|
|
|
const { id, upload_urls }: { id: string; upload_urls: string[] } =
|
|
await submitReq.json();
|
|
|
|
const totalSize = filelist.reduce((a, b) => a + b.size, 0);
|
|
let bytesRead = 0;
|
|
let shouldRecall = false;
|
|
|
|
setUploading(true);
|
|
|
|
for (let i = 0; i < upload_urls.length; i++) {
|
|
await new Promise((resolve) => {
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.open("PUT", upload_urls[i], true);
|
|
xhr.setRequestHeader(
|
|
"content-type",
|
|
(files[i].name.split(".").at(-1) as string).toLowerCase() === "mov"
|
|
? "video/mp4"
|
|
: files[i].type ||
|
|
fileTypes[files[i].name.split(".").at(-1) as string],
|
|
);
|
|
|
|
xhr.upload.onprogress = (e) => {
|
|
if (!e.lengthComputable) return;
|
|
|
|
setFileProgress(
|
|
Math.floor(((bytesRead + e.loaded) / totalSize) * 100),
|
|
);
|
|
};
|
|
xhr.upload.onabort = () => {
|
|
shouldRecall = true;
|
|
setUploading(false);
|
|
setFileProgress(0);
|
|
};
|
|
xhr.upload.onerror = () => {
|
|
shouldRecall = true;
|
|
setUploading(false);
|
|
setFileProgress(0);
|
|
};
|
|
xhr.upload.onloadend = (ev) => {
|
|
if (i === upload_urls.length - 1) setUploading(false);
|
|
|
|
bytesRead += ev.total;
|
|
setFileProgress(bytesRead);
|
|
resolve(null);
|
|
};
|
|
|
|
xhr.send(files[i]);
|
|
});
|
|
}
|
|
|
|
if (shouldRecall) {
|
|
setLoading(false);
|
|
await fetch("/api/reports/recall", {
|
|
body: JSON.stringify({ id }),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
method: "POST",
|
|
});
|
|
|
|
// @ts-expect-error
|
|
turnstile.reset();
|
|
|
|
return toast({
|
|
description: "Failed to upload file",
|
|
isClosable: true,
|
|
status: "error",
|
|
title: "Error",
|
|
});
|
|
}
|
|
|
|
await fetch("/api/reports/complete", {
|
|
body: JSON.stringify({ id }),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
method: "POST",
|
|
});
|
|
|
|
setShowSuccess(true);
|
|
setLoading(false);
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (logged_in) return;
|
|
|
|
const script = document.createElement("script");
|
|
script.async = true;
|
|
script.defer = true;
|
|
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
|
|
document.body.appendChild(script);
|
|
}, [logged_in]);
|
|
|
|
return showSuccess ? (
|
|
<Success
|
|
heading="Report Submitted"
|
|
message="We will review it as soon as possible."
|
|
/>
|
|
) : (
|
|
<>
|
|
<Container maxW="container.md" pt="4vh" textAlign="start">
|
|
<Heading mb="4vh">Report an Exploiter</Heading>
|
|
{logged_in ? null : (
|
|
<Text>
|
|
Tip: Log in before submitting this report to have it appear on your
|
|
data page.
|
|
</Text>
|
|
)}
|
|
<br />
|
|
<FormControl isRequired>
|
|
<FormLabel htmlFor="usernames">
|
|
Username(s) - To specify more than one, provide a comma-delimited
|
|
list (User1, User2, User3...)
|
|
</FormLabel>
|
|
<Input id="usernames" placeholder="builderman" />
|
|
</FormControl>
|
|
<br />
|
|
<FormControl isRequired>
|
|
<FormLabel>Your Evidence (Max size per file: 512MB)</FormLabel>
|
|
<Button
|
|
colorScheme="blue"
|
|
mr="8px"
|
|
onClick={() => document.getElementById("evidence")?.click()}
|
|
>
|
|
Select File
|
|
</Button>
|
|
<input id="evidence" multiple type="file" />
|
|
</FormControl>
|
|
<br />
|
|
<FormControl>
|
|
<FormLabel>Optional description</FormLabel>
|
|
<Textarea id="description" maxLength={512} />
|
|
</FormControl>
|
|
<br />
|
|
<div
|
|
className="cf-turnstile"
|
|
data-error-callback="onTurnstileError"
|
|
data-sitekey={site_key}
|
|
></div>
|
|
<br />
|
|
<Text>
|
|
By submitting this form, you agree to the{" "}
|
|
<Link color="#646cff" href="/terms">
|
|
Terms of Service
|
|
</Link>{" "}
|
|
and{" "}
|
|
<Link color="#646cff" href="/privacy">
|
|
Privacy Policy
|
|
</Link>
|
|
.
|
|
</Text>
|
|
<br />
|
|
<HStack>
|
|
<Button
|
|
colorScheme="blue"
|
|
disabled={uploading}
|
|
isLoading={loading}
|
|
loadingText="Submitting"
|
|
mr="8px"
|
|
onClick={async () => await submit()}
|
|
>
|
|
Submit
|
|
</Button>
|
|
<CircularProgress
|
|
display={uploading ? undefined : "none"}
|
|
value={fileProgress}
|
|
>
|
|
<CircularProgressLabel>{fileProgress}%</CircularProgressLabel>
|
|
</CircularProgress>
|
|
</HStack>
|
|
</Container>
|
|
<script
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
function onTurnstileError(code) {
|
|
const messages = {
|
|
110500: "Your browser is too old to complete the captcha, please update it.",
|
|
110510: "Something unexpected happened, please try disabling all extensions and refresh the page. If this does not solve the problem, use a different browser.",
|
|
110600: "Failed to solve the captcha, please refresh the page to try again.",
|
|
200010: "Invalid cache, please clear your cache and site data in your browser's settings.",
|
|
200100: "Your device's clock is wrong, please fix it.",
|
|
};
|
|
|
|
const message = messages[code];
|
|
|
|
alert(message ?? \`Unknown error when solving captcha. Error \${code}\`);
|
|
|
|
return true;
|
|
}
|
|
`,
|
|
}}
|
|
></script>
|
|
</>
|
|
);
|
|
}
|