add SREBOT, SHARED, TSSBOT contents (fixup for #1223)
PR #1223 only staged the deletions of the old paths because the new top-level directories were still untracked when the commit was authored. This commit adds the actual restructured tree: SREBOT/ (existing bot), SHARED/ (vromfs, data_parser, ICONS/MAPS/FONTS, DAGOR_FILES, update_game_files), and TSSBOT/ (skeleton). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Diagnostic: query Discord's entitlements API directly and report what it returns
|
||||
right now. Compare against the local entitlements.db cache.
|
||||
|
||||
Usage (on the server):
|
||||
cd ~/GitHub/SREBOT_MEOW
|
||||
source .venv/bin/activate
|
||||
python scripts/diag_entitlements.py
|
||||
|
||||
Targeted guild: 1379510072815779961 (bot owner's max-tier server).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
APPLICATION_ID = "1254679514466877540"
|
||||
TARGET_GUILD = "1379510072815779961"
|
||||
TOKEN = os.environ.get("DISCORD_KEY", "")
|
||||
_storage_env = os.environ.get("SREBOT_STORAGE_VOL_PATH", "").strip()
|
||||
if not _storage_env:
|
||||
raise RuntimeError("SREBOT_STORAGE_VOL_PATH must be set")
|
||||
STORAGE = Path(_storage_env)
|
||||
DB_PATH = STORAGE / "entitlements.db"
|
||||
|
||||
|
||||
async def fetch_all_entitlements() -> tuple[list[dict], list[tuple[int, int, str, str]]]:
|
||||
"""
|
||||
Returns (entitlements, page_logs).
|
||||
page_logs: list of (page_num, count, status, after_cursor)
|
||||
"""
|
||||
if not TOKEN:
|
||||
print("ERROR: DISCORD_KEY env var not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
headers = {"Authorization": f"Bot {TOKEN}"}
|
||||
url = f"https://discord.com/api/v10/applications/{APPLICATION_ID}/entitlements"
|
||||
params: dict[str, str] = {"exclude_ended": "true", "limit": "100"}
|
||||
|
||||
all_ents: list[dict] = []
|
||||
page_logs: list[tuple[int, int, str, str]] = []
|
||||
page = 0
|
||||
after: str | None = None
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=60)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
while True:
|
||||
page += 1
|
||||
q = dict(params)
|
||||
if after:
|
||||
q["after"] = after
|
||||
t0 = time.monotonic()
|
||||
async with session.get(url, headers=headers, params=q) as resp:
|
||||
elapsed = time.monotonic() - t0
|
||||
status = f"{resp.status} ({elapsed:.2f}s)"
|
||||
if resp.status != 200:
|
||||
body = await resp.text()
|
||||
page_logs.append((page, 0, status, after or ""))
|
||||
print(f"[!] page {page} failed: {status}\n {body[:300]}")
|
||||
break
|
||||
batch = await resp.json()
|
||||
page_logs.append((page, len(batch), status, after or ""))
|
||||
all_ents.extend(batch)
|
||||
if len(batch) < 100:
|
||||
break
|
||||
# paginate forward by id
|
||||
after = batch[-1]["id"]
|
||||
# safety break
|
||||
if page > 50:
|
||||
print("[!] hit 50-page safety limit, stopping")
|
||||
break
|
||||
|
||||
return all_ents, page_logs
|
||||
|
||||
|
||||
def read_db_state() -> dict[str, int]:
|
||||
if not DB_PATH.exists():
|
||||
return {"db_missing": 1}
|
||||
conn = sqlite3.connect(str(DB_PATH))
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
out: dict[str, int] = {}
|
||||
out["guild_entitlements_total"] = cur.execute("SELECT COUNT(*) FROM guild_entitlements").fetchone()[0]
|
||||
out["guild_entitlements_active"] = cur.execute("SELECT COUNT(*) FROM guild_entitlements WHERE status='active'").fetchone()[0]
|
||||
out["manual_entitlements_total"] = cur.execute("SELECT COUNT(*) FROM manual_entitlements").fetchone()[0]
|
||||
out["discord_entitlements_total"] = cur.execute("SELECT COUNT(*) FROM discord_entitlements").fetchone()[0]
|
||||
row = cur.execute("SELECT * FROM discord_entitlements WHERE guild_id=?", (TARGET_GUILD,)).fetchone()
|
||||
out["target_in_discord_table"] = 1 if row else 0
|
||||
return out
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=" * 70)
|
||||
print(f"Discord Entitlements Diagnostic — {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}")
|
||||
print("=" * 70)
|
||||
|
||||
print(f"\n[1] Local DB state ({DB_PATH}):")
|
||||
for k, v in read_db_state().items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
print("\n[2] Discord API: GET /applications/{app_id}/entitlements?exclude_ended=true")
|
||||
ents, pages = await fetch_all_entitlements()
|
||||
|
||||
print("\n Page log:")
|
||||
for p, n, status, after in pages:
|
||||
print(f" page {p:>2} count={n:>3} status={status:<18} after={after}")
|
||||
|
||||
print(f"\n Total entitlements returned: {len(ents)}")
|
||||
|
||||
# Group by guild
|
||||
by_guild: dict[str, list[dict]] = {}
|
||||
for e in ents:
|
||||
gid = e.get("guild_id")
|
||||
if gid:
|
||||
by_guild.setdefault(str(gid), []).append(e)
|
||||
|
||||
print(f" Unique guild_ids: {len(by_guild)}")
|
||||
print(f" Entitlements with NULL guild_id: {sum(1 for e in ents if not e.get('guild_id'))}")
|
||||
|
||||
# Status breakdown
|
||||
deleted = sum(1 for e in ents if e.get("deleted"))
|
||||
consumed = sum(1 for e in ents if e.get("consumed"))
|
||||
starts_in_future = sum(1 for e in ents if e.get("starts_at") and e["starts_at"] > time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()))
|
||||
ends_in_past = sum(1 for e in ents if e.get("ends_at") and e["ends_at"] < time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()))
|
||||
print(f" deleted={deleted} consumed={consumed} starts_in_future={starts_in_future} ends_in_past={ends_in_past}")
|
||||
|
||||
print(f"\n[3] Target guild {TARGET_GUILD}:")
|
||||
target_ents = by_guild.get(TARGET_GUILD, [])
|
||||
if not target_ents:
|
||||
print(f" *** NOT FOUND in Discord API response ***")
|
||||
# Show sample of what we DID get to confirm we're not just paginating wrong
|
||||
sample_guilds = list(by_guild.keys())[:5]
|
||||
print(f" Sample of guild_ids returned: {sample_guilds}")
|
||||
else:
|
||||
for e in target_ents:
|
||||
print(f" id={e.get('id')} sku_id={e.get('sku_id')} type={e.get('type')} "
|
||||
f"deleted={e.get('deleted')} starts_at={e.get('starts_at')} ends_at={e.get('ends_at')}")
|
||||
|
||||
print("\n[4] SKU breakdown across all returned entitlements:")
|
||||
sku_counts: dict[str, int] = {}
|
||||
for e in ents:
|
||||
sku = str(e.get("sku_id") or "")
|
||||
sku_counts[sku] = sku_counts.get(sku, 0) + 1
|
||||
for sku, c in sorted(sku_counts.items(), key=lambda x: -x[1]):
|
||||
print(f" {sku}: {c}")
|
||||
|
||||
print("\n[5] Recommendation:")
|
||||
if not target_ents:
|
||||
print(" Discord is NOT returning the target guild's entitlement.")
|
||||
print(" Either Discord's API is degraded right now, OR the entitlement no longer exists.")
|
||||
print(" Re-run this script in a few minutes to see if results change.")
|
||||
else:
|
||||
print(" Discord IS returning the target guild's entitlement.")
|
||||
print(" The bot's cache must be stale or wholesale-replaced by an empty result.")
|
||||
print(" Force a refresh: restart bot OR call refresh_entitled_guilds(force=True).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user