From 062b976569f1fc8cd37dcb4e84f1d0a3f3327bdb Mon Sep 17 00:00:00 2001 From: Clove Twilight Date: Fri, 19 Jun 2026 19:04:43 +0100 Subject: [PATCH] Add a second user token (thanks Claude) --- .dev.vars.example | 5 +++++ src/discord/rest.ts | 47 ++++++++++++++++++++++++++++++++------------- src/types.ts | 3 +++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.dev.vars.example b/.dev.vars.example index 3e7c115..64decb9 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -10,3 +10,8 @@ DISCORD_BOT_TOKEN= # WARNING: using a user token is self-botting and against Discord's ToS; # it can get the account banned. Leave blank to run bot-token-only. DISCORD_USER_TOKEN= + +# OPTIONAL second user token. If set, rich profile fetches spread across both +# tokens and fail over on a 429, doubling the /profile rate-limit headroom. +# Same ToS warning applies — more accounts means more accounts at risk. +DISCORD_USER_TOKEN2= diff --git a/src/discord/rest.ts b/src/discord/rest.ts index 8220156..5cf0925 100644 --- a/src/discord/rest.ts +++ b/src/discord/rest.ts @@ -78,26 +78,47 @@ export interface UserProfileFetch { retryAfter: number; } +/** Configured user tokens (1 or 2), in order, skipping blanks. */ +function userTokens(env: Env): string[] { + return [env.DISCORD_USER_TOKEN, env.DISCORD_USER_TOKEN2].filter( + (t): t is string => !!t && t.trim().length > 0 + ); +} + /** - * Rich profile via USER token (self-bot — ToS risk). Reports the HTTP status so - * callers can tell a 429 rate-limit (back off) apart from a 401/403 token issue, - * rather than silently degrading to the bot token. + * Rich profile via USER token(s) (self-bot — ToS risk). If two tokens are + * configured, load is spread across them (random start) and a 429 on one fails + * over to the other — doubling the /profile rate-limit headroom. Reports the + * HTTP status so callers can tell a 429 (back off) from a 401/403 token issue. */ export async function fetchUserProfile(env: Env, id: string): Promise { - if (!env.DISCORD_USER_TOKEN) return { data: null, status: 0, retryAfter: 0 }; + const tokens = userTokens(env); + if (tokens.length === 0) return { data: null, status: 0, retryAfter: 0 }; + const url = `${apiBase(env)}/users/${id}/profile` + `?with_mutual_guilds=false&with_mutual_friends=false`; - const res = await fetch(url, { - headers: { Authorization: env.DISCORD_USER_TOKEN }, - }); - if (!res.ok) { - const retryAfter = Number(res.headers.get("retry-after")) || 0; + + // Spread load: start on a random token, then rotate to the next on a 429. + const start = Math.floor(Math.random() * tokens.length); + let lastStatus = 0; + let lastRetryAfter = 0; + + for (let i = 0; i < tokens.length; i++) { + const idx = (start + i) % tokens.length; + const res = await fetch(url, { headers: { Authorization: tokens[idx] } }); + if (res.ok) { + return { data: (await res.json()) as RawProfileResponse, status: 200, retryAfter: 0 }; + } + lastStatus = res.status; + lastRetryAfter = Number(res.headers.get("retry-after")) || 0; console.warn( - `[dough-restful] user-token /users/${id}/profile -> HTTP ${res.status}` + - (retryAfter ? ` (retry ${retryAfter}s)` : "") + `[dough-restful] user-token #${idx + 1} /users/${id}/profile -> HTTP ${res.status}` + + (lastRetryAfter ? ` (retry ${lastRetryAfter}s)` : "") ); - return { data: null, status: res.status, retryAfter }; + // Only a rate-limit is worth retrying on another token; 401/403/404 would + // behave the same (or signal a token problem we'd rather surface). + if (res.status !== 429) break; } - return { data: (await res.json()) as RawProfileResponse, status: 200, retryAfter: 0 }; + return { data: null, status: lastStatus, retryAfter: lastRetryAfter }; } diff --git a/src/types.ts b/src/types.ts index 37f879e..1f6ebae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,9 @@ export interface Env { DISCORD_BOT_TOKEN: string; /** Optional self-bot token for rich profile data. Off by default. */ DISCORD_USER_TOKEN?: string; + /** Optional second self-bot token; rich fetches spread across both and fail + * over on a 429, doubling the /profile rate-limit headroom. */ + DISCORD_USER_TOKEN2?: string; DISCORD_API_VERSION?: string; TRACKED_GUILD_IDS?: string;