KV to D1 migration (this is totally gonna break something)

This commit is contained in:
Regalijan 2024-05-12 01:25:46 -04:00
parent a2b3391bda
commit e00b7e8c55
Signed by: regalijan
GPG Key ID: 5D4196DA269EF520
24 changed files with 1835 additions and 669 deletions

View File

@ -196,7 +196,7 @@ export default function () {
try { try {
queueReq = await fetch( queueReq = await fetch(
`/api/mod-queue/list?before=${before}&showClosed=${show_closed}&type=${queueType}`, `/api/mod-queue/${queueType}/list?before=${before}&showClosed=${show_closed}`,
); );
} catch { } catch {
alert("Failed to load mod queue"); alert("Failed to load mod queue");

View File

@ -20,11 +20,25 @@ export async function onRequestPost(context: RequestContext) {
context.data.targetId = id; context.data.targetId = id;
if (!pathname.endsWith("/ban")) { if (!pathname.endsWith("/ban")) {
const key = await context.env.DATA.get(`appeal_${id}`); const appeal: Record<string, any> | null = await context.env.D1.prepare(
"SELECT * FROM appeals WHERE id = ?;",
)
.bind(id)
.first();
if (!key) return jsonError("No appeal with that ID exists", 404); if (!appeal) return jsonError("No appeal with that ID exists", 404);
context.data.appeal = JSON.parse(key); appeal.user = JSON.parse(appeal.user);
context.data.appeal = appeal;
const pushNotificationData = await context.env.D1.prepare(
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'appeal';",
)
.bind(id)
.first();
if (pushNotificationData)
context.data.fcm_token = pushNotificationData.token;
} }
if ( if (

View File

@ -3,15 +3,21 @@ import sendEmail from "../../../email.js";
import { sendPushNotification } from "../../../gcloud.js"; import { sendPushNotification } from "../../../gcloud.js";
export async function onRequestPost(context: RequestContext) { export async function onRequestPost(context: RequestContext) {
const { appeal } = context.data; const { appeal, fcm_token } = context.data;
if (appeal.fcm_token) { if (fcm_token) {
await sendPushNotification( await sendPushNotification(
context.env, context.env,
"Appeal Accepted", "Appeal Accepted",
context.data.body.feedback || "No additional details to display", context.data.body.feedback || "No additional details to display",
appeal.fcm_token fcm_token,
); );
await context.env.D1.prepare(
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'appeal';",
)
.bind(appeal.id)
.run();
} else { } else {
const emailResponse = await sendEmail( const emailResponse = await sendEmail(
appeal.user.email, appeal.user.email,
@ -19,8 +25,8 @@ export async function onRequestPost(context: RequestContext) {
"Appeal Accepted", "Appeal Accepted",
"appeal_accepted", "appeal_accepted",
{ {
note: context.data.body.feedback || "No note provided." note: context.data.body.feedback || "No note provided.",
} },
); );
if (!emailResponse.ok) { if (!emailResponse.ok) {
@ -32,29 +38,21 @@ export async function onRequestPost(context: RequestContext) {
const { current_user: currentUser } = context.data; const { current_user: currentUser } = context.data;
await context.env.D1.prepare( await context.env.D1.prepare(
"UPDATE appeals SET approved = 1, open = 0 WHERE id = ?;" "UPDATE appeals SET approved = 1, user = json_remove(user, '$.email') WHERE id = ?;",
) )
.bind(context.params.id) .bind(context.params.id)
.run(); .run();
delete appeal.fcm_token;
delete appeal.user.email;
appeal.open = false;
await context.env.DATA.put(`appeal_${appeal.id}`, JSON.stringify(appeal), {
expirationTtl: 94608000
});
await fetch( await fetch(
`https://discord.com/api/v10/guilds/242263977986359297/bans/${appeal.user.id}`, `https://discord.com/api/v10/guilds/242263977986359297/bans/${appeal.user.id}`,
{ {
headers: { headers: {
authorization: `Bot ${context.env.BOT_TOKEN}`, authorization: `Bot ${context.env.BOT_TOKEN}`,
"x-audit-log-reason": `Appeal accepted by ${currentUser.username} (${currentUser.id})` "x-audit-log-reason": `Appeal accepted by ${currentUser.username} (${currentUser.id})`,
},
method: "DELETE",
}, },
method: "DELETE"
}
); );
await fetch(context.env.APPEALS_WEBHOOK, { await fetch(context.env.APPEALS_WEBHOOK, {
@ -67,19 +65,19 @@ export async function onRequestPost(context: RequestContext) {
fields: [ fields: [
{ {
name: "Moderator", name: "Moderator",
value: `${currentUser.username} (${currentUser.id})` value: `${currentUser.username} (${currentUser.id})`,
} },
] ],
} },
] ],
}), }),
headers: { headers: {
"content-type": "application/json" "content-type": "application/json",
}, },
method: "POST" method: "POST",
}); });
return new Response(null, { return new Response(null, {
status: 204 status: 204,
}); });
} }

View File

@ -3,16 +3,22 @@ import sendEmail from "../../../email.js";
import { sendPushNotification } from "../../../gcloud.js"; import { sendPushNotification } from "../../../gcloud.js";
export async function onRequestPost(context: RequestContext) { export async function onRequestPost(context: RequestContext) {
const { appeal } = context.data; const { appeal, fcm_token } = context.data;
if (appeal.fcm_token) { if (fcm_token) {
await sendPushNotification( await sendPushNotification(
context.env, context.env,
"Appeal Denied", "Appeal Denied",
`Unfortunately, we have decided to deny your appeal for the following reason: ${ `Unfortunately, we have decided to deny your appeal for the following reason: ${
context.data.body.feedback || "No additional details" context.data.body.feedback || "No additional details"
}`, }`,
appeal.fcm_token, fcm_token,
); );
await context.env.D1.prepare(
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'appeal';",
)
.bind(appeal.id)
.run();
} else { } else {
const emailResponse = await sendEmail( const emailResponse = await sendEmail(
appeal.user.email, appeal.user.email,
@ -31,22 +37,13 @@ export async function onRequestPost(context: RequestContext) {
} }
await context.env.D1.prepare( await context.env.D1.prepare(
"UPDATE appeals SET approved = 0, open = 0 WHERE id = ?;", "UPDATE appeals SET approved = 0, user = json_remove(user, '$.email') WHERE id = ?;",
) )
.bind(context.params.id) .bind(context.params.id)
.run(); .run();
const { current_user: currentUser } = context.data; const { current_user: currentUser } = context.data;
delete appeal.user.email;
delete appeal.fcm_token;
appeal.open = false;
await context.env.DATA.put(`appeal_${appeal.id}`, JSON.stringify(appeal), {
expirationTtl: 94608000,
});
await fetch(context.env.APPEALS_WEBHOOK, { await fetch(context.env.APPEALS_WEBHOOK, {
body: JSON.stringify({ body: JSON.stringify({
embeds: [ embeds: [

View File

@ -41,18 +41,17 @@ export async function onRequestPost(context: RequestContext) {
if (!currentUser.email) return jsonError("No email for this session", 403); if (!currentUser.email) return jsonError("No email for this session", 403);
const existingAppeals = await context.env.DATA.list({
prefix: `appeal_${currentUser.id}`,
});
const existingBlockedAppeal = await context.env.DATA.get( const existingBlockedAppeal = await context.env.DATA.get(
`blockedappeal_${currentUser.id}`, `blockedappeal_${currentUser.id}`,
); );
if ( if (
existingBlockedAppeal || existingBlockedAppeal ||
existingAppeals.keys.find( (await context.env.D1.prepare(
(appeal) => (appeal.metadata as { [k: string]: any })?.open, "SELECT approved FROM appeals WHERE approved IS NULL AND json_extract(user, '$.id') = ?;",
) )
.bind(currentUser.id)
.first())
) )
return jsonError("Appeal already submitted", 403); return jsonError("Appeal already submitted", 403);
@ -74,33 +73,31 @@ export async function onRequestPost(context: RequestContext) {
.randomUUID() .randomUUID()
.replaceAll("-", "")}`; .replaceAll("-", "")}`;
await context.env.DATA.put( await context.env.D1.prepare(
`appeal_${appealId}`, "INSERT INTO appeals (ban_reason, created_at, id, learned, reason_for_unban, user) VALUES (?, ?, ?, ?, ?, ?);",
JSON.stringify({ )
ban_reason: whyBanned, .bind(
created_at: Date.now(), whyBanned,
fcm_token: typeof senderTokenId === "string" ? senderTokenId : undefined, Date.now(),
appealId,
learned, learned,
id: appealId, whyUnban,
open: true, JSON.stringify({
reason_for_unban: whyUnban,
user: {
email: currentUser.email, email: currentUser.email,
id: currentUser.id, id: currentUser.id,
username: currentUser.username, username: currentUser.username,
},
}), }),
{
expirationTtl: 94608000,
},
);
await context.env.D1.prepare(
"INSERT INTO appeals (created_at, id, open, user) VALUES (?, ?, ?, ?)",
) )
.bind(Date.now(), appealId, 1, currentUser.id)
.run(); .run();
if (typeof senderTokenId === "string") {
await context.env.D1.prepare(
"INSERT INTO push_notifications (created_at, event_id, event_type, token) VALUES (?, ?, 'appeal', ?)",
)
.bind(Date.now(), appealId, senderTokenId)
.run();
}
await fetch(context.env.APPEALS_WEBHOOK, { await fetch(context.env.APPEALS_WEBHOOK, {
body: JSON.stringify({ body: JSON.stringify({
embeds: [ embeds: [

View File

@ -7,13 +7,14 @@ export async function onRequestPost(context: RequestContext) {
if (statsReduction && typeof statsReduction !== "number") if (statsReduction && typeof statsReduction !== "number")
return jsonError("Invalid stat reduction", 400); return jsonError("Invalid stat reduction", 400);
const appeal = await context.env.DATA.get( const appeal: Record<string, any> | null = await context.env.D1.prepare(
`gameappeal_${context.params.id as string}`, "SELECT * FROM game_appeals WHERE id = ?;",
); )
.bind(context.params.id)
.first();
if (!appeal) return jsonError("Appeal not found", 400); if (!appeal) return jsonError("Appeal not found", 400);
const data = JSON.parse(appeal);
const banList = (await getBanList(context)) as { const banList = (await getBanList(context)) as {
[k: string]: { BanType: number; Unbanned?: boolean; UnbanReduct?: number }; [k: string]: { BanType: number; Unbanned?: boolean; UnbanReduct?: number };
}; };
@ -23,12 +24,12 @@ export async function onRequestPost(context: RequestContext) {
.bind(context.params.id) .bind(context.params.id)
.run(); .run();
if (!banList[data.roblox_id]) if (!banList[appeal.roblox_id])
return new Response(null, { return new Response(null, {
status: 204, status: 204,
}); });
banList[data.roblox_id] = { banList[appeal.roblox_id] = {
BanType: 0, BanType: 0,
Unbanned: true, Unbanned: true,
UnbanReduct: statsReduction, UnbanReduct: statsReduction,
@ -43,7 +44,7 @@ export async function onRequestPost(context: RequestContext) {
Date.now(), Date.now(),
context.data.current_user.id, context.data.current_user.id,
crypto.randomUUID(), crypto.randomUUID(),
data.roblox_id, appeal.roblox_id,
) )
.run(); .run();

View File

@ -3,18 +3,20 @@ import { jsonError } from "../../../common.js";
export async function onRequestPost(context: RequestContext) { export async function onRequestPost(context: RequestContext) {
const appealId = context.params.id as string; const appealId = context.params.id as string;
const appeal = await context.env.DATA.get(`gameappeal_${appealId}`); const appeal = await context.env.D1.prepare(
"SELECT * FROM game_appeals WHERE id = ?;",
)
.bind(appealId)
.first();
if (!appeal) return jsonError("Appeal not found", 404); if (!appeal) return jsonError("Appeal not found", 404);
const appealData = JSON.parse(appeal);
await context.env.DATA.delete(`gameappeal_${appealId}`); await context.env.DATA.delete(`gameappeal_${appealId}`);
await context.env.D1.prepare("DELETE FROM game_appeals WHERE id = ?;") await context.env.D1.prepare("DELETE FROM game_appeals WHERE id = ?;")
.bind(appealId) .bind(appealId)
.run(); .run();
await context.env.DATA.put( await context.env.DATA.put(
`gameappealblock_${appealData.roblox_id}`, `gameappealblock_${appeal.roblox_id}`,
`${Date.now() + 2592000000}`, `${Date.now() + 2592000000}`,
{ expirationTtl: 2592000 }, { expirationTtl: 2592000 },
); );

View File

@ -5,9 +5,7 @@ export default async function (
user: number, user: number,
): Promise<{ can_appeal?: boolean; error?: string; reason?: string }> { ): Promise<{ can_appeal?: boolean; error?: string; reason?: string }> {
if ( if (
await context.env.D1.prepare( await context.env.D1.prepare("SELECT * FROM game_appeals WHERE user = ?;")
"SELECT * FROM game_appeals WHERE open = 1 AND user = ?;",
)
.bind(user) .bind(user)
.first() .first()
) )

View File

@ -43,25 +43,14 @@ export async function onRequestPost(context: RequestContext) {
if (!precheckData.can_appeal) if (!precheckData.can_appeal)
return jsonError(precheckData.reason as string, 400); return jsonError(precheckData.reason as string, 400);
const appealId = `${id}${context.request.headers const appealId = `${id}${
.get("cf-ray") context.request.headers.get("cf-ray")?.split("-")[0]
?.split("-")[0]}${Date.now()}`; }${Date.now()}`;
await context.env.DATA.put(
`gameappeal_${appealId}`,
JSON.stringify({
id: appealId,
reasonForUnban,
roblox_id: id,
roblox_username: username,
whatHappened,
}),
);
await context.env.D1.prepare( await context.env.D1.prepare(
"INSERT INTO game_appeals (created_at, id, open, user) VALUES (?, ?, ?, ?);", "INSERT INTO game_appeals (created_at, id, reason_for_unban, roblox_id, roblox_username) VALUES (?, ?, ?, ?, ?);",
) )
.bind(Date.now(), appealId, 1, id) .bind(Date.now(), appealId, reasonForUnban, id, username)
.run(); .run();
await fetch(context.env.REPORTS_WEBHOOK, { await fetch(context.env.REPORTS_WEBHOOK, {

View File

@ -1,17 +1,18 @@
import { jsonError } from "../../common.js"; import { jsonError } from "../../common.js";
import sendEmail from "../../email.js"; import sendEmail from "../../email.js";
import { sendPushNotification } from "../../gcloud.js"; import { sendPushNotification } from "../../gcloud.js";
import validateInactivityNotice from "./validate.js";
export async function onRequestDelete(context: RequestContext) { export async function onRequestDelete(context: RequestContext) {
const kvResult = await context.env.DATA.get( const result = await context.env.D1.prepare(
`inactivity_${context.params.id}`, "SELECT json_extract(user, '*.id') AS uid FROM inactivity_notices WHERE id = ?;",
); )
.bind(context.params.id)
.first();
if (!kvResult) return jsonError("No inactivity notice with that ID", 404); if (!result) return jsonError("No inactivity notice with that ID", 404);
if ( if (
JSON.parse(kvResult).user.id !== context.data.current_user.id && result.uid !== context.data.current_user.id &&
!(context.data.current_user.permissions & (1 << 0)) !(context.data.current_user.permissions & (1 << 0))
) )
return jsonError( return jsonError(
@ -19,7 +20,6 @@ export async function onRequestDelete(context: RequestContext) {
403, 403,
); );
await context.env.DATA.delete(`inactivity_${context.params.id}`);
await context.env.D1.prepare("DELETE FROM inactivity_notices WHERE id = ?;") await context.env.D1.prepare("DELETE FROM inactivity_notices WHERE id = ?;")
.bind(context.params.id) .bind(context.params.id)
.run(); .run();
@ -50,40 +50,61 @@ export async function onRequestPost(context: RequestContext) {
return jsonError("You are not a manager of any departments", 403); return jsonError("You are not a manager of any departments", 403);
const requestedNotice: { [k: string]: any } | null = const requestedNotice: { [k: string]: any } | null =
await context.env.DATA.get(`inactivity_${context.params.id as string}`, { await context.env.D1.prepare(
type: "json", "SELECT decisions, departments, user FROM inactivity_notices WHERE id = ?;",
}); )
.bind(context.params.id)
.first();
if (!requestedNotice) if (!requestedNotice)
return jsonError("Inactivity notices does not exist", 404); return jsonError("Inactivity notices does not exist", 404);
const decisions: { [dept: string]: boolean } = {}; const decisions: { [dept: string]: boolean } = JSON.parse(
requestedNotice.decisions,
);
for (const department of userAdminDepartments) for (const department of userAdminDepartments)
decisions[department] = accepted; decisions[department] = accepted;
requestedNotice.decisions = decisions; const applicableDepartments = JSON.parse(requestedNotice.departments).length;
if (Object.values(decisions).length === requestedNotice.departments.length) {
requestedNotice.open = false;
const approved = !Object.values(decisions).filter((d) => !d).length;
await context.env.D1.prepare( await context.env.D1.prepare(
"UPDATE inactivity_notices SET approved = ?, open = 0 WHERE id = ?;", "UPDATE inactivity_notices SET decisions = ?, user = json_remove(user, '*.email') WHERE id = ?;",
) )
.bind(Number(approved), context.params.id) .bind(JSON.stringify(decisions), context.params.id)
.run(); .run();
if (requestedNotice.fcm_token) { if (Object.values(decisions).length === applicableDepartments) {
const approved =
Object.values(decisions).filter((d) => d).length ===
applicableDepartments;
const denied =
Object.values(decisions).filter((d) => !d).length !==
applicableDepartments;
const fcmTokenResult: FCMTokenResult | null = await context.env.D1.prepare(
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'inactivity';",
)
.bind(context.params.id)
.first();
if (fcmTokenResult) {
let status = "Approved";
if (denied) status = "Denied";
else if (!approved) status = "Partially Approved";
await sendPushNotification( await sendPushNotification(
context.env, context.env,
`Inactivity Request ${approved ? "Approved" : "Denied"}`, `Inactivity Request ${status}`,
accepted accepted
? "Your inactivity request was approved." ? "Your inactivity request was approved."
: "Your inactivity request was denied, please reach out to management if you require more details.", : `Your inactivity request was ${denied ? "denied" : "partially approved"}, please reach out to management if you require more details.`,
requestedNotice.fcm_token, fcmTokenResult.token,
); );
await context.env.D1.prepare(
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'inactivity';",
).bind(context.params.id);
} else { } else {
await sendEmail( await sendEmail(
requestedNotice.user.email, requestedNotice.user.email,
@ -93,96 +114,8 @@ export async function onRequestPost(context: RequestContext) {
{ username: requestedNotice.user.username }, { username: requestedNotice.user.username },
); );
} }
delete requestedNotice.fcm_token;
delete requestedNotice.user.email;
} }
await context.env.DATA.put(
`inactivity_${context.params.id as string}`,
JSON.stringify(requestedNotice),
{ expirationTtl: 63072000 },
);
return new Response(null, {
status: 204,
});
}
export async function onRequestPut(context: RequestContext) {
const kvResult: InactivityNoticeProps | null = await context.env.DATA.get(
`inactivity_${context.params.id}`,
{ type: "json" },
);
if (!kvResult) return jsonError("No inactivity notice with that ID", 404);
if (kvResult.user.id !== context.data.current_user.id)
return jsonError(
"You do not have permission to modify this inactivity notice",
403,
);
const d1entry = await context.env.D1.prepare(
"SELECT open FROM inactivity_notices WHERE id = ?;",
)
.bind(context.params.id)
.first();
if (!Boolean(d1entry?.open))
return jsonError("Cannot modify a closed inactivity notice", 403);
const { departments, end, reason, start } = context.data.body;
const validationFailureResponse = validateInactivityNotice(
departments,
end,
reason,
start,
context.data.departments,
);
if (validationFailureResponse) return validationFailureResponse;
kvResult.departments = departments;
kvResult.end = end;
kvResult.reason = reason;
kvResult.start = start;
await context.env.DATA.put(
`inactivity_${context.params.id}`,
JSON.stringify(kvResult),
{
expirationTtl: 63072000,
},
);
const departmentWebhooks = [];
for (const department of departments)
departmentWebhooks.push(context.env[`${department}_INACTIVITY_WEBHOOK`]);
const webhookPromises = [];
for (const webhook of departmentWebhooks)
webhookPromises.push(
fetch(webhook, {
body: JSON.stringify({
embeds: [
{
title: "Inactivity Notice Modified",
color: 37562560,
description: `View the updated notice at https://carcrushers.cc/mod-queue?id=${context.params.id}&type=inactivity`,
},
],
}),
headers: {
"content-type": "application/json",
},
method: "POST",
}),
);
await Promise.allSettled(webhookPromises);
return new Response(null, { return new Response(null, {
status: 204, status: 204,
}); });

View File

@ -20,34 +20,33 @@ export async function onRequestPost(context: RequestContext) {
(context.request.headers.get("cf-ray") as string).split("-")[0] + (context.request.headers.get("cf-ray") as string).split("-")[0] +
Date.now().toString(); Date.now().toString();
await context.env.DATA.put( await context.env.D1.prepare(
`inactivity_${inactivityId}`, "INSERT INTO inactivity_notices (created_at, departments, end, hiatus, id, reason, start, user) VALUES (?, ?, ?, ?, ?, ?, ?, ?);",
JSON.stringify({ )
created_at: Date.now(), .bind(
departments, Date.now(),
JSON.stringify(departments),
end, end,
fcm_token: typeof senderTokenId === "string" ? senderTokenId : undefined, Number(hiatus),
hiatus, inactivityId,
open: true,
reason, reason,
start, start,
user: { JSON.stringify({
id: context.data.current_user.id, id: context.data.current_user.id,
email: context.data.current_user.email, email: context.data.current_user.email,
username: context.data.current_user.username, username: context.data.current_user.username,
},
}), }),
{
expirationTtl: 63072000,
},
);
await context.env.D1.prepare(
"INSERT INTO inactivity_notices (created_at, id, user) VALUES (?, ?, ?);",
) )
.bind(Date.now(), inactivityId, context.data.current_user.id)
.run(); .run();
if (typeof senderTokenId === "string") {
await context.env.D1.prepare(
"INSERT INTO push_notifications (created_at, event_id, event_type) VALUES (?, ?, ?);",
)
.bind(Date.now(), inactivityId, "inactivity")
.run();
}
const departmentsToNotify = []; const departmentsToNotify = [];
const departmentRoles = []; const departmentRoles = [];
const { env } = context; const { env } = context;

View File

@ -9,5 +9,13 @@ export async function onRequestGet(context: RequestContext) {
if (!success) return jsonError("Unable to retrieve appeals", 500); if (!success) return jsonError("Unable to retrieve appeals", 500);
return jsonResponse(JSON.stringify(results)); return jsonResponse(
JSON.stringify(
results.map((result) => {
result.user = JSON.parse(result.user as string);
return result;
}),
),
);
} }

View File

@ -6,19 +6,27 @@ export async function onRequestGet(context: RequestContext) {
if (!["appeal", "inactivity", "report"].includes(type as string)) if (!["appeal", "inactivity", "report"].includes(type as string))
return jsonError("Invalid type", 400); return jsonError("Invalid type", 400);
const data = (await context.env.DATA.get(`${type}_${id}`, { const tables: { [k: string]: string } = {
type: "json", appeal: "appeals",
})) as { inactivity: "inactivity_notices",
created_at: number; report: "reports",
id: string; };
open: boolean;
user?: { id: string; username: string };
} & { [k: string]: any };
if (data?.user?.id !== context.data.current_user.id) const data: Record<string, any> | null = await context.env.D1.prepare(
`SELECT *
FROM ${tables[type as string]}
WHERE id = ?;`,
)
.bind(id)
.first();
if (data?.user) data.user = JSON.parse(data.user);
if (!data || data.user?.id !== context.data.current_user.id)
return jsonError("Item does not exist", 404); return jsonError("Item does not exist", 404);
if (type === "report") { if (type === "report") {
data.attachments = JSON.parse(data.attachments);
const { AwsClient } = await import("aws4fetch"); const { AwsClient } = await import("aws4fetch");
const aws = new AwsClient({ const aws = new AwsClient({
accessKeyId: context.env.R2_ACCESS_KEY, accessKeyId: context.env.R2_ACCESS_KEY,

View File

@ -8,42 +8,12 @@ export async function onRequestGet(context: RequestContext) {
results: { id: string }[]; results: { id: string }[];
success: boolean; success: boolean;
} = await context.env.D1.prepare( } = await context.env.D1.prepare(
"SELECT id FROM reports WHERE user = ? ORDER BY created_at LIMIT 50;", "SELECT created_at, id, open, target_usernames FROM reports WHERE json_extract(user, '$.id') = ? ORDER BY created_at LIMIT 50;",
) )
.bind(context.data.current_user.id) .bind(context.data.current_user.id)
.all(); .all();
if (!success) return jsonError("Failed to retrieve reports", 500); if (!success) return jsonError("Failed to retrieve reports", 500);
const ids: string[] = []; return jsonResponse(JSON.stringify(results));
results.map((v) => ids.push(v.id));
const kvDataPromises = [];
for (const id of ids)
kvDataPromises.push(context.env.DATA.get(`report_${id}`, { type: "json" }));
let settledKvPromises;
try {
settledKvPromises = (await Promise.all(
kvDataPromises,
)) as ReportCardProps[];
} catch (e) {
console.log(e);
return jsonError("Failed to resolve reports", 500);
}
return jsonResponse(
JSON.stringify(
settledKvPromises.map((r) => {
return {
created_at: r.created_at,
id: r.id,
open: r.open,
target_usernames: r.target_usernames,
};
}),
),
);
} }

View File

@ -1,22 +1,24 @@
import { jsonError, jsonResponse } from "../../../common.js"; import { jsonError, jsonResponse } from "../../../common.js";
export async function onRequestGet(context: RequestContext) { export async function onRequestGet(context: RequestContext) {
const types: { [k: string]: { permissions: number[]; prefix: string } } = { const types: {
[k: string]: { permissions: number[]; table: string };
} = {
appeal: { appeal: {
permissions: [1 << 0, 1 << 1], permissions: [1 << 0, 1 << 1],
prefix: `appeal_`, table: "appeals",
}, },
gma: { gma: {
permissions: [1 << 5], permissions: [1 << 5],
prefix: "gameappeal_", table: "game_appeals",
}, },
inactivity: { inactivity: {
permissions: [1 << 0, 1 << 4, 1 << 6, 1 << 7, 1 << 11], permissions: [1 << 0, 1 << 4, 1 << 6, 1 << 7, 1 << 11],
prefix: "inactivity_", table: "inactivity_notices",
}, },
report: { report: {
permissions: [1 << 5], permissions: [1 << 5],
prefix: "report_", table: "reports",
}, },
}; };
@ -30,16 +32,15 @@ export async function onRequestGet(context: RequestContext) {
) )
return jsonError("You cannot use this filter", 403); return jsonError("You cannot use this filter", 403);
let item: { const item: Record<string, any> | null = await context.env.D1.prepare(
[k: string]: any; `SELECT *
} | null = await context.env.DATA.get(`${types[type].prefix}${itemId}`, { FROM ${types[type].table}
type: "json", WHERE id = ?;`,
}); )
.bind(itemId)
.first();
if (!item) if (!item) return jsonError("Item not found", 404);
item = await context.env.DATA.get(`closed${types[type].prefix}${itemId}`, {
type: "json",
});
if ( if (
type === "report" && type === "report" &&
@ -47,7 +48,11 @@ export async function onRequestGet(context: RequestContext) {
) )
return jsonError("Report is processing", 409); return jsonError("Report is processing", 409);
if (item) delete item.user?.email; if (item.user) {
item.user = JSON.parse(item.user);
delete item.user.email;
}
return item return item
? jsonResponse(JSON.stringify(item)) ? jsonResponse(JSON.stringify(item))

View File

@ -0,0 +1,92 @@
import { jsonError, jsonResponse } from "../../../common.js";
export async function onRequestGet(context: RequestContext): Promise<any> {
const type = context.params.type as string;
const { searchParams } = new URL(context.request.url);
const before = parseInt(searchParams.get("before") || `${Date.now()}`);
const showClosed = searchParams.get("showClosed") === "true";
const tables: { [k: string]: string } = {
appeal: "appeals",
gma: "game_appeals",
inactivity: "inactivity_notices",
report: "reports",
};
const permissions: { [k: string]: number[] } = {
appeal: [1 << 0, 1 << 1],
gma: [1 << 5],
inactivity: [1 << 4, 1 << 6, 1 << 7, 1 << 11, 1 << 12],
report: [1 << 5],
};
const { current_user: currentUser } = context.data;
if (!tables[type]) return jsonError("Invalid filter type", 400);
if (!permissions[type].find((p) => currentUser.permissions & p))
return jsonError("You cannot use this filter", 403);
if (isNaN(before)) return jsonError("Invalid `before` parameter", 400);
let rows: D1Result<Record<string, any>>;
switch (type) {
case "appeal":
rows = await context.env.D1.prepare(
`SELECT *
FROM appeals
WHERE created_at < ?
AND open ${showClosed ? "IS NOT" : "IS"} NULL
ORDER BY created_at DESC LIMIT 25;`,
)
.bind(before, !Number(showClosed))
.all();
rows.results.forEach((r) => {
r.user = JSON.parse(r.user);
delete r.user.email;
});
break;
case "gma":
rows = await context.env.D1.prepare(
"SELECT * FROM game_appeals WHERE created_at < ? ORDER BY created_at DESC LIMIT 25;",
)
.bind(before)
.all();
break;
case "inactivity":
rows = await context.env.D1.prepare(
`SELECT *,
(SELECT COUNT(*) FROM json_each(decisions)) as decision_count
FROM inactivity_notices
WHERE created_at < ?
AND decision_count ${showClosed ? "=" : "!="} json_array_length(departments)`,
)
.bind(before)
.all();
break;
case "report":
rows = await context.env.D1.prepare(
"SELECT * FROM reports WHERE created_at < ? AND open = ? ORDER BY created_at DESC LIMIT 25;",
)
.bind(before, !Number(showClosed))
.all();
rows.results.forEach((r) => {
r.attachments = JSON.parse(r.attachments);
r.target_ids = JSON.parse(r.target_ids);
r.target_usernames = JSON.parse(r.target_usernames);
if (!r.user) return;
r.user = JSON.parse(r.user);
});
break;
default:
return jsonError("Unknown filter error", 500);
}
return jsonResponse(JSON.stringify(rows.results));
}

View File

@ -1,90 +0,0 @@
import { jsonError, jsonResponse } from "../../common.js";
export async function onRequestGet(context: RequestContext) {
const { searchParams } = new URL(context.request.url);
const before = parseInt(searchParams.get("before") || `${Date.now()}`);
const entryType = searchParams.get("type");
const showClosed = searchParams.get("showClosed") === "true";
const tables: { [k: string]: string } = {
appeal: "appeals",
gma: "game_appeals",
inactivity: "inactivity_notices",
report: "reports",
};
const types: { [k: string]: string } = {
appeal: "appeal",
gma: "gameappeal",
inactivity: "inactivity",
report: "report",
};
const permissions: { [k: string]: number[] } = {
appeal: [1 << 0, 1 << 1],
gma: [1 << 5],
inactivity: [1 << 4, 1 << 6, 1 << 7, 1 << 11, 1 << 12],
report: [1 << 5],
};
const { current_user: currentUser } = context.data;
if (!entryType || !types[entryType])
return jsonError("Invalid filter type", 400);
if (!permissions[entryType].find((p) => currentUser.permissions & p))
return jsonError("You cannot use this filter", 403);
if (isNaN(before)) return jsonError("Invalid `before` parameter", 400);
const prefix = types[entryType];
const table = tables[entryType];
const items = [];
const { results }: { results?: { created_at: number; id: string }[] } =
/*
This is normally VERY BAD and can lead to injection attacks
However, there is no other way to do this, as using bindings for table names is unsupported apparently
To avoid any potential injection attacks we enforce a list of specific values and permissions for table names
*/
await context.env.D1.prepare(
`SELECT id
FROM ${table}
WHERE created_at < ? AND open = ?
ORDER BY created_at DESC LIMIT 25;`,
)
.bind(before, Number(!showClosed))
.all();
if (results)
for (const { id } of results) {
const item: { [k: string]: any } | null = await context.env.DATA.get(
`${prefix}_${id}`,
{
type: "json",
},
);
if (item) {
delete item.user?.email;
if (entryType === "inactivity") {
// Only include inactivity notices that a user can actually act on
const departments = {
DM: [1 << 11],
ET: [1 << 4, 1 << 12],
FM: [1 << 7],
WM: [1 << 6],
};
if (
!Object.entries(departments).find(
(dept) =>
item.departments.includes(dept[0]) &&
dept[1].find((p) => currentUser.permissions & p),
)
)
continue;
}
items.push({ ...item, id });
}
}
return jsonResponse(JSON.stringify(items.filter((v) => v !== null)));
}

View File

@ -5,15 +5,18 @@ import { sendPushNotification } from "../../../gcloud.js";
export async function onRequestPost(context: RequestContext) { export async function onRequestPost(context: RequestContext) {
const reportId = context.params.id as string; const reportId = context.params.id as string;
const reportData: (ReportCardProps & { fcm_token?: string }) | null = const report: {
await context.env.DATA.get(`report_${reportId}`, { type: "json" }); [k: string]: any;
} | null = await context.env.D1.prepare("SELECT * FROM reports WHERE id = ?")
.bind(reportId)
.first();
if (!reportData) return jsonError("Report does not exist", 404); if (!report) return jsonError("Report does not exist", 404);
const actionMap = context.data.body; const actionMap = context.data.body;
const newActions: { [k: string]: { BanType: number } } = {}; const newActions: { [k: string]: { BanType: number } } = {};
const logMap: { [k: string]: number } = {}; const logMap: { [k: string]: number } = {};
const { user } = reportData as ReportCardProps & { user?: { email: string } }; const user = JSON.parse(report.user);
for (const [user, action] of Object.entries(actionMap)) { for (const [user, action] of Object.entries(actionMap)) {
if ( if (
@ -64,35 +67,44 @@ export async function onRequestPost(context: RequestContext) {
await setBanList(context, Object.assign(banList, newActions)); await setBanList(context, Object.assign(banList, newActions));
} }
reportData.open = false; const pushNotificationData: Record<string, string> | null =
await context.env.D1.prepare(
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'report';",
)
.bind(reportId)
.first();
if (user?.email && !reportData.fcm_token) if (user?.email)
await sendEmail( await sendEmail(
user.email, user?.email,
context.env.MAILGUN_API_KEY, context.env.MAILGUN_API_KEY,
"Report Processed", "Report Processed",
"report_processed", "report_processed",
{ {
username: reportData.user?.username as string, username: report.user?.username as string,
}, },
); );
else if (reportData.fcm_token) else if (pushNotificationData)
await sendPushNotification( await sendPushNotification(
context.env, context.env,
"Report Processed", "Report Processed",
`Your report for ${reportData.target_usernames.toString()} has been reviewed.`, `Your report for ${JSON.parse(report.target_usernames).toString()} has been reviewed.`,
reportData.fcm_token, pushNotificationData.token,
); );
delete reportData.fcm_token; delete (report.user as { email?: string; id: string; username: string })
delete (reportData.user as { email?: string; id: string; username: string })
?.email; ?.email;
await context.env.DATA.put(`report_${reportId}`, JSON.stringify(reportData));
await context.env.D1.prepare("UPDATE reports SET open = 0 WHERE id = ?;") await context.env.D1.prepare("UPDATE reports SET open = 0 WHERE id = ?;")
.bind(reportId) .bind(reportId)
.run(); .run();
await context.env.D1.prepare(
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'report';",
)
.bind(reportId)
.run();
return new Response(null, { return new Response(null, {
status: 204, status: 204,
}); });

View File

@ -17,7 +17,11 @@ export async function onRequestPost(context: RequestContext) {
await context.env.DATA.delete(`reportprocessing_${id}`); await context.env.DATA.delete(`reportprocessing_${id}`);
const value = await context.env.DATA.get(`report_${id}`); const value = await context.env.D1.prepare(
"SELECT id FROM reports WHERE id = ?;",
)
.bind(id)
.first();
if (!value) return jsonError("Report is missing", 500); if (!value) return jsonError("Report is missing", 500);

View File

@ -15,12 +15,15 @@ export async function onRequestPost(context: RequestContext) {
) )
return jsonError("No processing report with that ID found", 404); return jsonError("No processing report with that ID found", 404);
const data: { [k: string]: any } | null = await context.env.DATA.get( const data: Record<string, any> | null = await context.env.D1.prepare(
`report_${id}`, "SELECT attachments FROM reports WHERE id = ?;",
{ type: "json" }, )
); .bind(id)
.first();
if (!data) return jsonError("Report doesn't exist", 404); if (!data) return jsonError("No report with that ID found", 404);
data.attachments = JSON.parse(data.attachments);
const accessToken = await GetAccessToken(context.env); const accessToken = await GetAccessToken(context.env);
const attachmentDeletePromises = []; const attachmentDeletePromises = [];
@ -47,7 +50,6 @@ export async function onRequestPost(context: RequestContext) {
); );
await Promise.allSettled(attachmentDeletePromises); await Promise.allSettled(attachmentDeletePromises);
await context.env.DATA.delete(`report_${id}`);
await context.env.D1.prepare("DELETE FROM reports WHERE id = ?;") await context.env.D1.prepare("DELETE FROM reports WHERE id = ?;")
.bind(id) .bind(id)
.run(); .run();

View File

@ -195,33 +195,25 @@ export async function onRequestPost(context: RequestContext) {
attachments.push(new URL(urlResult.value).pathname.replace(/^\/?t?\//, "")); 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: true,
user: currentUser
? {
email: currentUser.email,
id: currentUser.id,
username: currentUser.username,
}
: null,
target_ids: metaIDs,
target_usernames: metaNames,
}),
);
try {
await context.env.D1.prepare( await context.env.D1.prepare(
"INSERT INTO reports (created_at, id, open, user) VALUES (?, ?, ?, ?);", "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,
) )
.bind(Date.now(), reportId, 1, currentUser?.id || null)
.run(); .run();
} catch {}
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( return jsonResponse(
JSON.stringify({ id: reportId, upload_urls: uploadUrls }), JSON.stringify({ id: reportId, upload_urls: uploadUrls }),

7
index.d.ts vendored
View File

@ -10,6 +10,13 @@ declare global {
[k: string]: string; [k: string]: string;
} }
type FCMTokenResult = {
created_at: number;
event_id: string;
event_type: string;
token: string;
}
type RequestContext = EventContext<Env, string, { [k: string]: any }>; type RequestContext = EventContext<Env, string, { [k: string]: any }>;
interface AppealCardProps { interface AppealCardProps {

1694
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,27 +12,27 @@
"@chakra-ui/react": "^2.8.2", "@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@fontsource/plus-jakarta-sans": "^5.0.19", "@fontsource/plus-jakarta-sans": "^5.0.20",
"@remix-run/cloudflare": "^2.8.1", "@remix-run/cloudflare": "^2.9.2",
"@remix-run/cloudflare-pages": "^2.8.1", "@remix-run/cloudflare-pages": "^2.9.2",
"@remix-run/react": "^2.8.1", "@remix-run/react": "^2.9.2",
"@sentry/react": "^7.110.1", "@sentry/react": "^7.114.0",
"aws4fetch": "^1.0.18", "aws4fetch": "^1.0.18",
"framer-motion": "^11.0.28", "framer-motion": "^11.1.9",
"react": "^18.2.0", "react": "^18.3.1",
"react-dom": "^18.2.0" "react-dom": "^18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@remix-run/dev": "^2.8.1", "@remix-run/dev": "^2.9.2",
"@types/node": "^20.12.7", "@types/node": "^20.12.11",
"@types/react": "^18.2.78", "@types/react": "^18.3.2",
"@types/react-dom": "^18.2.25", "@types/react-dom": "^18.3.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"typescript": "^5.4.5" "typescript": "^5.4.5"
}, },
"overrides": { "overrides": {
"@cloudflare/workers-types": "^4.20240405.0" "@cloudflare/workers-types": "^4.20240502.0"
}, },
"prettier": { "prettier": {
"endOfLine": "auto" "endOfLine": "auto"