fix: tolerate season-reset payloads in /sq-info and stop duplicate error embeds

obtain_clan_new_points treated a missing `astat` or `member_ratings` block as
"squadron unavailable or deleted". After a season reset the game API omits those
blocks for inactive/unranked squadrons even though the clan and its roster still
exist, so /sq-info wrongly reported real squadrons (e.g. DSPL, 513th) as
nonexistent. Require a `members` roster to consider the payload valid (genuinely
deleted clans return CLAN_IS_NOT_EXISTS JSON handled upstream) and default both
stat blocks to zero, so the squadron renders at 0 points instead of erroring.

permission_fail was invoked twice per error because discord.py dispatches to
both the command-local `@cmd.error` handler and the global `@tree.error`
handler. Combined with a public defer, this produced a duplicated error message
(one public, one ephemeral). Guard on interaction.extras so the embed is sent
at most once per interaction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
deploy
2026-07-01 12:28:22 +00:00
parent 3af950b464
commit 61236a8267
2 changed files with 23 additions and 2 deletions
+14 -2
View File
@@ -655,8 +655,20 @@ async def obtain_clan_new_points(
while isinstance(data, dict) and "member_ratings" not in data and len(data) == 1:
data = next(iter(data.values()))
total_score = data["astat"]["dr_era5_hist"]
raw_ratings = data["member_ratings"]
# A real clan record always carries a `members` roster. Deleted
# clans come back as JSON (handled above), and truncated payloads
# raise inside bin_blk_to_json, so a parsed BLK without `members`
# is genuinely unavailable.
if not isinstance(data, dict) or "members" not in data:
raise KeyError("members")
# After a season reset the game API can omit the aggregate `astat`
# block and/or the per-member `member_ratings` block for inactive
# squadrons even though the clan and its roster still exist. Default
# both to zero so the squadron renders at 0 points instead of being
# reported as deleted.
total_score = (data.get("astat") or {}).get("dr_era5_hist", 0)
raw_ratings = data.get("member_ratings") or {}
except ClanInfoError as e:
try:
+9
View File
@@ -598,6 +598,15 @@ def gate_entitle(required_tier: str):
async def permission_fail(interaction: discord.Interaction, error):
"""Handle permission-related errors with appropriate embeds."""
# discord.py dispatches a command error to BOTH the command's local
# `@cmd.error` handler and the global `@tree.error` handler (see
# app_commands/tree.py). Since both route here, guard against sending the
# error embed twice for the same interaction — the second send would land
# as a separate (and, after a public defer, differently-scoped) message.
if interaction.extras.get("_permission_fail_handled"):
return
interaction.extras["_permission_fail_handled"] = True
lang = await guild_lang(interaction.guild_id) if interaction.guild_id else "en"
if isinstance(error, BlacklistCheckFailure):
reason = getattr(error, "reason", None)