feat: call api for bans
vi did:web:vt3e.cat
Sun, 24 May 2026 20:02:32 +0100
5 files changed,
75 insertions(+),
14 deletions(-)
M
src/commands/ban.ts
→
src/commands/ban.ts
@@ -30,11 +30,6 @@ for (const game of GAMES) {
const isSuccessful = successfulIds.has(game.id); const isFailed = failedIds.has(game.id); const isPending = pendingIds.has(game.id); - const isExcluded = excludedGameIds.has(game.id); - - container.logger.debug( - `isExcluded: ${isExcluded}, isSuccessful: ${isSuccessful}, isFailed: ${isFailed}, isPending: ${isPending}`, - ); const button = new ButtonBuilder() .setLabel(game.name)
M
src/config.ts
→
src/config.ts
@@ -32,6 +32,9 @@ const configSchema = z
.object({ discord: discordSchema, games: gamesSchema, + roblox: z.object({ + cookie: z.string(), + }), }) .transform((config) => { return {
M
src/discord/client.ts
→
src/discord/client.ts
@@ -1,7 +1,8 @@
-import { SapphireClient } from "@sapphire/framework"; +import { LogLevel, SapphireClient } from "@sapphire/framework"; import { PinoSapphireLogger } from "@/logger"; import "@/commands"; +import config from "@/config"; export const client = new SapphireClient({ intents: [],@@ -11,6 +12,9 @@ parse: [],
repliedUser: true, }, logger: { - instance: new PinoSapphireLogger({ level: "debug" }), + instance: new PinoSapphireLogger( + { level: "trace" }, + config.isProduction ? LogLevel.Info : LogLevel.Debug, + ), }, });
M
src/logger.ts
→
src/logger.ts
@@ -16,7 +16,7 @@ : { target: "pino-pretty", options: { colorize: true } },
base: { pid: false, }, - level: Bun.env.LOG_LEVEL ?? (config.isProduction ? "info" : "debug"), + level: "trace", ...options, }); }
M
src/roblox/index.ts
→
src/roblox/index.ts
@@ -1,3 +1,8 @@
+import { container } from "@sapphire/framework"; +import config from "@/config"; + +let CSRF_TOKEN: string | undefined; + export type RobloxUser = { username: string; displayName: string | null;@@ -13,9 +18,22 @@ displayName: includeDisplayName ? "stubDisplay" : null,
id: id, }; } + +export type BanResponse = { + path: string; + user: string; + gameJoinRestriction: { + active: boolean; + startTime: string; + privateReason: string; + displayReason: string; + excludeAltAccounts: boolean; + inherited: boolean; + }; +}; export async function ban( - id: string, + userId: string, universeId: string, options: { duration: string | undefined;@@ -23,10 +41,51 @@ excludeAltAccounts: boolean;
displayReason: string; privateReason: string; }, -): Promise<void> { - const delay = Math.floor(Math.random() * 750) + 250; - await new Promise((resolve) => setTimeout(resolve, delay)); +): Promise<BanResponse> { + container.logger.debug(`banning ${userId} in universe ${universeId}`, { + name: "ban", + ...options, + }); + + const makeRequest = async () => { + const res = await fetch( + `https://apis.roblox.com/user/cloud/v2/universes/${universeId}/user-restrictions/${userId}`, + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Cookie: config.roblox.cookie, + Host: "apis.roblox.com", + Origin: "https://create.roblox.com", + Referer: "https://create.roblox.com", + "x-csrf-token": CSRF_TOKEN ?? "", + }, + body: JSON.stringify({ + gameJoinRestriction: { + active: true, + ...options, + }, + }), + }, + ); + + const data = (await res.json()) as Record<string, unknown>; + container.logger.debug("ban response", { ...data }); + + if (res.status === 403) { + const csrfHeader = res.headers.get("x-csrf-token"); + if (csrfHeader && csrfHeader !== "") { + CSRF_TOKEN = csrfHeader; + return await makeRequest(); + } + } + + if (!res.ok) { + throw new Error(`failed to ban user: ${res.status} ${res.statusText}`); + } + + return data as BanResponse; + }; - const chance = Math.random(); - if (chance < 0.3) throw new Error("Failed to ban user"); + return await makeRequest(); }