356 lines
9.2 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({
bypass: false,
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;
bytesRead += e.loaded;
setFileProgress(Math.floor((bytesRead / totalSize) * 100));
};
xhr.upload.onabort = () => {
shouldRecall = true;
setUploading(false);
setFileProgress(0);
};
xhr.upload.onerror = () => {
shouldRecall = true;
setUploading(false);
setFileProgress(0);
};
xhr.upload.onloadend = () => {
if (i === upload_urls.length - 1) setUploading(false);
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>
</>
);
}