Add a second user token (thanks Claude)

This commit is contained in:
Clove 2026-06-19 19:04:43 +01:00
parent b3418d1460
commit 062b976569
3 changed files with 42 additions and 13 deletions

View File

@ -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=

View File

@ -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<UserProfileFetch> {
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 };
}

View File

@ -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;