Discord REST client, cache and #225 settings (server-26#225 S2)
New app/internal/discord_rest.py: one shared httpx client with the
DiscordBot User-Agent, per-token rate-limit buckets (X-RateLimit-Bucket +
guild), one retry on a 429 of <=3s, global-429 pause per token, and a
process-wide breaker at 4000 401/403/unshared-429 responses per 10 min so
C2 can never get the shared egress IP banned. Tokens are never logged.
inspect_token / refresh_membership / leave_guild / TokenRejected move off
discord_leases onto it. /users/@me/guilds now follows after= pages (it read
one page of 200 and silently dropped the rest); a failed page is unknown,
never a truncated list.
guild_snapshot(token_doc, guild_id): cached, single-flight read of
/guilds/{g}, the bot's own member and /channels for S3's permission check.
401 marks the token invalid; 10004/10007 drop the server from guild_ids;
50001 is confirmed with one membership read and never changes guild_ids
on its own. TTLs: ok 120s, not-member/invalid 600s, unknown 30s.
config.py gets every setting the #225 slices use (perm-check kill switch,
TTLs, breaker budget, OAuth redirect URI, discord_pool_bot_guild_cap=90).
No behaviour change beyond pagination, User-Agent and the breaker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c4e1f9aa61
commit
2b3b875cdc
@@ -256,6 +256,44 @@ class Settings(BaseSettings):
|
||||
# never falls back to this org either way.
|
||||
discord_unlinked_guild_org_id: str = "founding"
|
||||
|
||||
# --- Discord permission checks, REST client, OAuth linking (server-26#225)
|
||||
# Every setting the #225 slices use lives here, added once (S2).
|
||||
# Kill switch (S3): False restores today's token picking exactly — no
|
||||
# REST permission check before a bot is handed to a node.
|
||||
discord_perm_check_enabled: bool = True
|
||||
# Cache lifetimes for per-(bot, server) results (app/internal/discord_rest.py):
|
||||
# a result that lets a bot in; denied / not-a-member / invalid token; and
|
||||
# "couldn't tell" — short, but long enough that the reconciler's 30 s loop
|
||||
# never becomes a REST loop against Discord.
|
||||
discord_access_ttl_seconds: int = 120
|
||||
discord_access_negative_ttl_seconds: int = 600
|
||||
discord_access_unknown_ttl_seconds: int = 30
|
||||
# S3: how many bots start_stream checks at once, and the total time it
|
||||
# waits before failing open (unknown = allowed).
|
||||
discord_perm_check_concurrency: int = 4
|
||||
discord_perm_check_budget_seconds: float = 3.0
|
||||
# Process-wide breaker: Discord bans the egress IP (shared with the main
|
||||
# bot) at 10,000 401/403/429 responses per 10 min. Stop well short.
|
||||
discord_rest_invalid_budget: int = 4000
|
||||
discord_rest_invalid_window_seconds: int = 600
|
||||
# A rate-limited request waits at most this long (one retry); longer = unknown.
|
||||
discord_rest_max_retry_after_seconds: float = 3.0
|
||||
discord_rest_timeout_seconds: float = 10.0
|
||||
# S5: refresh a server's voice-channel list from a radio bot's REST view
|
||||
# when the stored directory is older than this.
|
||||
discord_directory_refresh_seconds: int = 300
|
||||
# S6: per-bot OAuth "Connect a Discord server". Empty = OAuth off (routes
|
||||
# 404, UI hides the button). Must match the redirect URI registered on
|
||||
# each bot application exactly, e.g. https://drb.cusano.net/discord/connected
|
||||
discord_oauth_redirect_uri: str = ""
|
||||
discord_oauth_state_ttl_seconds: int = 600
|
||||
# S6: time allowed for the permission check run right after a link.
|
||||
discord_oauth_link_check_seconds: float = 5.0
|
||||
# Pool bots: unverified Discord bots can't join more than 100 servers.
|
||||
# Connect picks a pool bot under this cap; add bots when all are full
|
||||
# (owner decision Q3, 2026-10-05).
|
||||
discord_pool_bot_guild_cap: int = 90
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user