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 {
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 {
alert("Failed to load mod queue");

View File

@ -20,11 +20,25 @@ export async function onRequestPost(context: RequestContext) {
context.data.targetId = id;
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 (

View File

@ -3,15 +3,21 @@ import sendEmail from "../../../email.js";
import { sendPushNotification } from "../../../gcloud.js";
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(
context.env,
"Appeal Accepted",
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 {
const emailResponse = await sendEmail(
appeal.user.email,
@ -19,8 +25,8 @@ export async function onRequestPost(context: RequestContext) {
"Appeal Accepted",
"appeal_accepted",
{
note: context.data.body.feedback || "No note provided."
}
note: context.data.body.feedback || "No note provided.",
},
);
if (!emailResponse.ok) {
@ -32,29 +38,21 @@ export async function onRequestPost(context: RequestContext) {
const { current_user: currentUser } = context.data;
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)
.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(
`https://discord.com/api/v10/guilds/242263977986359297/bans/${appeal.user.id}`,
{
headers: {
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, {
@ -67,19 +65,19 @@ export async function onRequestPost(context: RequestContext) {
fields: [
{
name: "Moderator",
value: `${currentUser.username} (${currentUser.id})`
}
]
}
]
value: `${currentUser.username} (${currentUser.id})`,
},
],
},
],
}),
headers: {
"content-type": "application/json"
"content-type": "application/json",
},
method: "POST"
method: "POST",
});
return new Response(null, {
status: 204
status: 204,
});
}

View File

@ -3,16 +3,22 @@ import sendEmail from "../../../email.js";
import { sendPushNotification } from "../../../gcloud.js";
export async function onRequestPost(context: RequestContext) {
const { appeal } = context.data;
if (appeal.fcm_token) {
const { appeal, fcm_token } = context.data;
if (fcm_token) {
await sendPushNotification(
context.env,
"Appeal Denied",
`Unfortunately, we have decided to deny your appeal for the following reason: ${
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 {
const emailResponse = await sendEmail(
appeal.user.email,
@ -31,22 +37,13 @@ export async function onRequestPost(context: RequestContext) {
}
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)
.run();
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, {
body: JSON.stringify({
embeds: [

View File

@ -41,18 +41,17 @@ export async function onRequestPost(context: RequestContext) {
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(
`blockedappeal_${currentUser.id}`,
);
if (
existingBlockedAppeal ||
existingAppeals.keys.find(
(appeal) => (appeal.metadata as { [k: string]: any })?.open,
(await context.env.D1.prepare(
"SELECT approved FROM appeals WHERE approved IS NULL AND json_extract(user, '$.id') = ?;",
)
.bind(currentUser.id)
.first())
)
return jsonError("Appeal already submitted", 403);
@ -74,33 +73,31 @@ export async function onRequestPost(context: RequestContext) {
.randomUUID()
.replaceAll("-", "")}`;
await context.env.DATA.put(
`appeal_${appealId}`,
JSON.stringify({
ban_reason: whyBanned,
created_at: Date.now(),
fcm_token: typeof senderTokenId === "string" ? senderTokenId : undefined,
await context.env.D1.prepare(
"INSERT INTO appeals (ban_reason, created_at, id, learned, reason_for_unban, user) VALUES (?, ?, ?, ?, ?, ?);",
)
.bind(
whyBanned,
Date.now(),
appealId,
learned,
id: appealId,
open: true,
reason_for_unban: whyUnban,
user: {
whyUnban,
JSON.stringify({
email: currentUser.email,
id: currentUser.id,
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();
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, {
body: JSON.stringify({
embeds: [

View File

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

View File

@ -3,18 +3,20 @@ import { jsonError } from "../../../common.js";
export async function onRequestPost(context: RequestContext) {
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);
const appealData = JSON.parse(appeal);
await context.env.DATA.delete(`gameappeal_${appealId}`);
await context.env.D1.prepare("DELETE FROM game_appeals WHERE id = ?;")
.bind(appealId)
.run();
await context.env.DATA.put(
`gameappealblock_${appealData.roblox_id}`,
`gameappealblock_${appeal.roblox_id}`,
`${Date.now() + 2592000000}`,
{ expirationTtl: 2592000 },
);

View File

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

View File

@ -43,25 +43,14 @@ export async function onRequestPost(context: RequestContext) {
if (!precheckData.can_appeal)
return jsonError(precheckData.reason as string, 400);
const appealId = `${id}${context.request.headers
.get("cf-ray")
?.split("-")[0]}${Date.now()}`;
await context.env.DATA.put(
`gameappeal_${appealId}`,
JSON.stringify({
id: appealId,
reasonForUnban,
roblox_id: id,
roblox_username: username,
whatHappened,
}),
);
const appealId = `${id}${
context.request.headers.get("cf-ray")?.split("-")[0]
}${Date.now()}`;
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();
await fetch(context.env.REPORTS_WEBHOOK, {

View File

@ -1,17 +1,18 @@
import { jsonError } from "../../common.js";
import sendEmail from "../../email.js";
import { sendPushNotification } from "../../gcloud.js";
import validateInactivityNotice from "./validate.js";
export async function onRequestDelete(context: RequestContext) {
const kvResult = await context.env.DATA.get(
`inactivity_${context.params.id}`,
);
const result = await context.env.D1.prepare(
"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 (
JSON.parse(kvResult).user.id !== context.data.current_user.id &&
result.uid !== context.data.current_user.id &&
!(context.data.current_user.permissions & (1 << 0))
)
return jsonError(
@ -19,7 +20,6 @@ export async function onRequestDelete(context: RequestContext) {
403,
);
await context.env.DATA.delete(`inactivity_${context.params.id}`);
await context.env.D1.prepare("DELETE FROM inactivity_notices WHERE id = ?;")
.bind(context.params.id)
.run();
@ -50,40 +50,61 @@ export async function onRequestPost(context: RequestContext) {
return jsonError("You are not a manager of any departments", 403);
const requestedNotice: { [k: string]: any } | null =
await context.env.DATA.get(`inactivity_${context.params.id as string}`, {
type: "json",
});
await context.env.D1.prepare(
"SELECT decisions, departments, user FROM inactivity_notices WHERE id = ?;",
)
.bind(context.params.id)
.first();
if (!requestedNotice)
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)
decisions[department] = accepted;
requestedNotice.decisions = decisions;
const applicableDepartments = JSON.parse(requestedNotice.departments).length;
if (Object.values(decisions).length === requestedNotice.departments.length) {
requestedNotice.open = false;
await context.env.D1.prepare(
"UPDATE inactivity_notices SET decisions = ?, user = json_remove(user, '*.email') WHERE id = ?;",
)
.bind(JSON.stringify(decisions), context.params.id)
.run();
const approved = !Object.values(decisions).filter((d) => !d).length;
await context.env.D1.prepare(
"UPDATE inactivity_notices SET approved = ?, open = 0 WHERE id = ?;",
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(Number(approved), context.params.id)
.run();
.bind(context.params.id)
.first();
if (fcmTokenResult) {
let status = "Approved";
if (denied) status = "Denied";
else if (!approved) status = "Partially Approved";
if (requestedNotice.fcm_token) {
await sendPushNotification(
context.env,
`Inactivity Request ${approved ? "Approved" : "Denied"}`,
`Inactivity Request ${status}`,
accepted
? "Your inactivity request was approved."
: "Your inactivity request was denied, please reach out to management if you require more details.",
requestedNotice.fcm_token,
: `Your inactivity request was ${denied ? "denied" : "partially approved"}, please reach out to management if you require more details.`,
fcmTokenResult.token,
);
await context.env.D1.prepare(
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'inactivity';",
).bind(context.params.id);
} else {
await sendEmail(
requestedNotice.user.email,
@ -93,96 +114,8 @@ export async function onRequestPost(context: RequestContext) {
{ 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, {
status: 204,
});

View File

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

View File

@ -9,5 +9,13 @@ export async function onRequestGet(context: RequestContext) {
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))
return jsonError("Invalid type", 400);
const data = (await context.env.DATA.get(`${type}_${id}`, {
type: "json",
})) as {
created_at: number;
id: string;
open: boolean;
user?: { id: string; username: string };
} & { [k: string]: any };
const tables: { [k: string]: string } = {
appeal: "appeals",
inactivity: "inactivity_notices",
report: "reports",
};
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);
if (type === "report") {
data.attachments = JSON.parse(data.attachments);
const { AwsClient } = await import("aws4fetch");
const aws = new AwsClient({
accessKeyId: context.env.R2_ACCESS_KEY,

View File

@ -8,42 +8,12 @@ export async function onRequestGet(context: RequestContext) {
results: { id: string }[];
success: boolean;
} = 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)
.all();
if (!success) return jsonError("Failed to retrieve reports", 500);
const ids: string[] = [];
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,
};
}),
),
);
return jsonResponse(JSON.stringify(results));
}

View File

@ -1,22 +1,24 @@
import { jsonError, jsonResponse } from "../../../common.js";
export async function onRequestGet(context: RequestContext) {
const types: { [k: string]: { permissions: number[]; prefix: string } } = {
const types: {
[k: string]: { permissions: number[]; table: string };
} = {
appeal: {
permissions: [1 << 0, 1 << 1],
prefix: `appeal_`,
table: "appeals",
},
gma: {
permissions: [1 << 5],
prefix: "gameappeal_",
table: "game_appeals",
},
inactivity: {
permissions: [1 << 0, 1 << 4, 1 << 6, 1 << 7, 1 << 11],
prefix: "inactivity_",
table: "inactivity_notices",
},
report: {
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);
let item: {
[k: string]: any;
} | null = await context.env.DATA.get(`${types[type].prefix}${itemId}`, {
type: "json",
});
const item: Record<string, any> | null = await context.env.D1.prepare(
`SELECT *
FROM ${types[type].table}
WHERE id = ?;`,
)
.bind(itemId)
.first();
if (!item)
item = await context.env.DATA.get(`closed${types[type].prefix}${itemId}`, {
type: "json",
});
if (!item) return jsonError("Item not found", 404);
if (
type === "report" &&
@ -47,7 +48,11 @@ export async function onRequestGet(context: RequestContext) {
)
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
? 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) {
const reportId = context.params.id as string;
const reportData: (ReportCardProps & { fcm_token?: string }) | null =
await context.env.DATA.get(`report_${reportId}`, { type: "json" });
const report: {
[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 newActions: { [k: string]: { BanType: 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)) {
if (
@ -64,35 +67,44 @@ export async function onRequestPost(context: RequestContext) {
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(
user.email,
user?.email,
context.env.MAILGUN_API_KEY,
"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(
context.env,
"Report Processed",
`Your report for ${reportData.target_usernames.toString()} has been reviewed.`,
reportData.fcm_token,
`Your report for ${JSON.parse(report.target_usernames).toString()} has been reviewed.`,
pushNotificationData.token,
);
delete reportData.fcm_token;
delete (reportData.user as { email?: string; id: string; username: string })
delete (report.user as { email?: string; id: string; username: string })
?.email;
await context.env.DATA.put(`report_${reportId}`, JSON.stringify(reportData));
await context.env.D1.prepare("UPDATE reports SET open = 0 WHERE id = ?;")
.bind(reportId)
.run();
await context.env.D1.prepare(
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'report';",
)
.bind(reportId)
.run();
return new Response(null, {
status: 204,
});

View File

@ -17,7 +17,11 @@ export async function onRequestPost(context: RequestContext) {
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);

View File

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

View File

@ -195,33 +195,25 @@ export async function onRequestPost(context: RequestContext) {
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(
"INSERT INTO reports (created_at, id, open, user) VALUES (?, ?, ?, ?);",
await context.env.D1.prepare(
"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();
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();
} catch {}
return jsonResponse(
JSON.stringify({ id: reportId, upload_urls: uploadUrls }),

7
index.d.ts vendored
View File

@ -10,6 +10,13 @@ declare global {
[k: string]: string;
}
type FCMTokenResult = {
created_at: number;
event_id: string;
event_type: string;
token: string;
}
type RequestContext = EventContext<Env, string, { [k: string]: any }>;
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",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fontsource/plus-jakarta-sans": "^5.0.19",
"@remix-run/cloudflare": "^2.8.1",
"@remix-run/cloudflare-pages": "^2.8.1",
"@remix-run/react": "^2.8.1",
"@sentry/react": "^7.110.1",
"@fontsource/plus-jakarta-sans": "^5.0.20",
"@remix-run/cloudflare": "^2.9.2",
"@remix-run/cloudflare-pages": "^2.9.2",
"@remix-run/react": "^2.9.2",
"@sentry/react": "^7.114.0",
"aws4fetch": "^1.0.18",
"framer-motion": "^11.0.28",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"framer-motion": "^11.1.9",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@remix-run/dev": "^2.8.1",
"@types/node": "^20.12.7",
"@types/react": "^18.2.78",
"@types/react-dom": "^18.2.25",
"@remix-run/dev": "^2.9.2",
"@types/node": "^20.12.11",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"dotenv": "^16.4.5",
"prettier": "^3.2.5",
"typescript": "^5.4.5"
},
"overrides": {
"@cloudflare/workers-types": "^4.20240405.0"
"@cloudflare/workers-types": "^4.20240502.0"
},
"prettier": {
"endOfLine": "auto"