From 9178ce19ac5bd24d36f6a30cd07ae82383850cce Mon Sep 17 00:00:00 2001 From: Regalijan Date: Tue, 30 Jun 2026 21:54:41 -0400 Subject: [PATCH] Add rtbf webhook --- functions/api/rtbf.ts | 95 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 functions/api/rtbf.ts diff --git a/functions/api/rtbf.ts b/functions/api/rtbf.ts new file mode 100644 index 0000000..3581af9 --- /dev/null +++ b/functions/api/rtbf.ts @@ -0,0 +1,95 @@ +import { jsonError } from "../common.js"; +import { Report } from "../../generated/prisma/client.js"; +import { getBanList, setBanList } from "../roblox-open-cloud.js"; + +export async function onRequestPost(context: RequestContext) { + const body = await context.request.text(); + const secret = context.env.ROBLOX_WEBHOOK_SECRET; + + if (!secret) return jsonError("This endpoint is currently disabled", 503); + + const sigHeader = context.request.headers.get("roblox-signature"); + + if (!sigHeader) return jsonError("Missing signature", 401); + + const [timestamp, sig] = sigHeader.split(","); + + if ( + parseInt(timestamp.replace("t=", "")) < + Math.floor(Date.now() / 1000) - 600 + ) + return jsonError("This request is stale", 406); + + const textEncode = (text: string) => new TextEncoder().encode(text); + const key = await crypto.subtle.importKey( + "raw", + textEncode(secret), + { + hash: "SHA-256", + name: "HMAC", + }, + false, + ["verify"], + ); + + if ( + !(await crypto.subtle.verify( + "HMAC", + key, + Uint8Array.from(atob(sig.replace("v1=", "")), (c) => c.charCodeAt(0)), + textEncode(`${timestamp.replace("t=", "")}.${body}`), + )) + ) + return jsonError("Invalid signature", 403); + + const data = JSON.parse(body); + + if (data.EventType === "SampleNotification") + return new Response(null, { + status: 204, + }); + + if (data.EventType !== "RightToErasureRequest") + return jsonError("Invalid event type", 400); + + const { prisma } = context.data; + const userId = data.EventPrisma.UserId; + + if (typeof userId !== "number") return jsonError("Invalid user id", 400); + + await prisma.gameModLog.deleteMany({ + where: { + target: userId, + }, + }); + + const associatedReports = await prisma.$queryRaw< + Report[] + >`SELECT attachments, id FROM reports WHERE EXISTS (SELECT 1 FROM json_each(target_ids) WHERE VALUE = ${userId});`; + + const idsToDelete: string[] = []; + for (const report of associatedReports) { + for (const attachment of report.attachments as string[]) { + await context.env.R2.delete(attachment); + } + idsToDelete.push(report.id); + } + + await prisma.report.deleteMany({ + where: { + id: { + in: idsToDelete, + }, + }, + }); + + const { etag, value } = await getBanList(context); + + delete value[userId]; + + await setBanList(context, value, etag); + + return new Response(null, { + status: 204, + }); +}