all repos — discord-utils @ 18d7f68a096e1f5eeaac2722dfc37e9e1d247747

feat: call api for bans
vi did:web:vt3e.cat
Sun, 24 May 2026 20:02:32 +0100
commit

18d7f68a096e1f5eeaac2722dfc37e9e1d247747

parent

b2c00f4dd362361aace8e4afc9baf28c31a0cb18

5 files changed, 75 insertions(+), 14 deletions(-)

jump to
M src/commands/ban.tssrc/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.tssrc/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.tssrc/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.tssrc/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.tssrc/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(); }