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
@@ -27,6 +27,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.config import settings
|
||||
from app.internal import discord_leases as leases
|
||||
from app.internal import discord_rest
|
||||
from app.internal import discord_streams as streams
|
||||
from app.internal import firestore as fstore
|
||||
from app.internal.audit import write_audit
|
||||
@@ -244,7 +245,7 @@ async def remove_guild(guild_id: str, leave_radio_bots: bool = False,
|
||||
return _error(e)
|
||||
left: list = []
|
||||
if leave_radio_bots:
|
||||
left = await leases.leave_guild(guild_id, out["previous_org_id"] or acting_org)
|
||||
left = await discord_rest.leave_guild(guild_id, out["previous_org_id"] or acting_org)
|
||||
await write_audit(uid, email, "discord.guild.remove", details={
|
||||
"guild_id": guild_id, "previous_org_id": out["previous_org_id"], "acting_org_id": acting_org,
|
||||
"streams_removed": out["streams_removed"], "leave_radio_bots": leave_radio_bots,
|
||||
|
||||
@@ -4,6 +4,7 @@ from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from app.internal import firestore as fstore
|
||||
from app.internal import discord_leases as leases
|
||||
from app.internal import discord_rest
|
||||
from app.internal.auth import require_admin_token
|
||||
from app.internal.logger import logger
|
||||
|
||||
@@ -47,8 +48,8 @@ async def add_token(body: TokenCreate, _: dict = Depends(require_admin_token)):
|
||||
(server-26#204 item 5): a string Discord answers 401 for is refused; if
|
||||
Discord is merely unreachable the token is stored with membership unknown."""
|
||||
try:
|
||||
info = await leases.inspect_token(body.token.strip())
|
||||
except leases.TokenRejected as e:
|
||||
info = await discord_rest.inspect_token(body.token.strip())
|
||||
except discord_rest.TokenRejected as e:
|
||||
raise HTTPException(400, str(e))
|
||||
token_id = str(uuid.uuid4())
|
||||
doc = {
|
||||
@@ -73,7 +74,7 @@ async def refresh_token(token_id: str, _: dict = Depends(require_admin_token)):
|
||||
"""Re-check a bot with Discord (valid? which servers is it in?)."""
|
||||
if not await fstore.doc_get("bot_tokens", token_id):
|
||||
raise HTTPException(404, "Token not found.")
|
||||
await leases.refresh_membership([token_id])
|
||||
await discord_rest.refresh_membership([token_id])
|
||||
return _masked(await fstore.doc_get("bot_tokens", token_id) or {})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user