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:
+14
-2
@@ -655,8 +655,20 @@ async def obtain_clan_new_points(
|
|||||||
while isinstance(data, dict) and "member_ratings" not in data and len(data) == 1:
|
while isinstance(data, dict) and "member_ratings" not in data and len(data) == 1:
|
||||||
data = next(iter(data.values()))
|
data = next(iter(data.values()))
|
||||||
|
|
||||||
total_score = data["astat"]["dr_era5_hist"]
|
# A real clan record always carries a `members` roster. Deleted
|
||||||
raw_ratings = data["member_ratings"]
|
# 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:
|
except ClanInfoError as e:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -598,6 +598,15 @@ def gate_entitle(required_tier: str):
|
|||||||
|
|
||||||
async def permission_fail(interaction: discord.Interaction, error):
|
async def permission_fail(interaction: discord.Interaction, error):
|
||||||
"""Handle permission-related errors with appropriate embeds."""
|
"""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"
|
lang = await guild_lang(interaction.guild_id) if interaction.guild_id else "en"
|
||||||
if isinstance(error, BlacklistCheckFailure):
|
if isinstance(error, BlacklistCheckFailure):
|
||||||
reason = getattr(error, "reason", None)
|
reason = getattr(error, "reason", None)
|
||||||
|
|||||||
Reference in New Issue
Block a user