feat(tally): fix live VC status updates and add permission pre-flight check
- Move tally hook from process_session (per-guild, gated by Logs subs) to process_ws_replays (once per game, all guilds) via on_game_finished - Add set_voice_channel_status permission check at /tally-claim time so failures are immediate and visible rather than silent on every game - Remove entitlement gate from tally_claim and tally_transfer - Add VC tally permission section to /diagnose-perms when run in a VC - Add 5 new locale keys to en.json for the permission messages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+13
-8
@@ -534,6 +534,19 @@ async def process_ws_replays(replays: list[dict]):
|
|||||||
)
|
)
|
||||||
local_data["scoreboard_context"] = scoreboard_context
|
local_data["scoreboard_context"] = scoreboard_context
|
||||||
|
|
||||||
|
# Notify all active tallies immediately after the game is saved.
|
||||||
|
# Done here rather than inside process_session so it fires for all
|
||||||
|
# guilds regardless of their Logs channel subscriptions.
|
||||||
|
try:
|
||||||
|
await tally.on_game_finished(
|
||||||
|
local_data.get("teams", []),
|
||||||
|
local_data.get("winning_team_squadron") or None,
|
||||||
|
bool(local_data.get("draw", False)),
|
||||||
|
hex_id,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[TALLY] on_game_finished failed for {hex_id}: {e}")
|
||||||
|
|
||||||
forwarded_replays.append(local_data)
|
forwarded_replays.append(local_data)
|
||||||
validated_games.append({
|
validated_games.append({
|
||||||
"sessionIdHex": hex_id,
|
"sessionIdHex": hex_id,
|
||||||
@@ -1132,14 +1145,6 @@ async def process_session(
|
|||||||
else:
|
else:
|
||||||
new_wl = get_standings(squadrons_clean)
|
new_wl = get_standings(squadrons_clean)
|
||||||
|
|
||||||
# Update any live voice-channel tallies for this guild against this game.
|
|
||||||
try:
|
|
||||||
await tally.on_session_processed(
|
|
||||||
guild_id, teams, winner, is_draw, session_id
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"[TALLY] hook failed for session {session_id}: {e}")
|
|
||||||
|
|
||||||
# Scoreboard Build
|
# Scoreboard Build
|
||||||
lock = _scoreboard_locks.setdefault(session_id, asyncio.Lock())
|
lock = _scoreboard_locks.setdefault(session_id, asyncio.Lock())
|
||||||
async with lock:
|
async with lock:
|
||||||
|
|||||||
+13
-6
@@ -1233,6 +1233,16 @@ async def _diagnose_perms_logic(interaction: discord.Interaction, target_channel
|
|||||||
lines.append(t(lang, "diagnostics.premium_autolog_required"))
|
lines.append(t(lang, "diagnostics.premium_autolog_required"))
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
|
# ── 5. Voice channel tally permission ──
|
||||||
|
if isinstance(channel, discord.VoiceChannel):
|
||||||
|
vc_mention = f"<#{channel.id}>"
|
||||||
|
lines.append(t(lang, "commands.tally.vc_perm_header", vc=vc_mention))
|
||||||
|
if channel.permissions_for(bot_member).set_voice_channel_status:
|
||||||
|
lines.append(f" ✅ {t(lang, 'commands.tally.vc_perm_ok', vc=vc_mention)}")
|
||||||
|
else:
|
||||||
|
lines.append(f" ❌ {t(lang, 'commands.tally.no_vc_perm_diagnose', vc=vc_mention)}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
# ── Send ──
|
# ── Send ──
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=t(lang, "diagnostics.title"),
|
title=t(lang, "diagnostics.title"),
|
||||||
@@ -4267,13 +4277,13 @@ def _invoker_voice_channel(interaction: discord.Interaction):
|
|||||||
@discord.app_commands.autocomplete(username=player_autocomplete, squadron=squadron_autocomplete)
|
@discord.app_commands.autocomplete(username=player_autocomplete, squadron=squadron_autocomplete)
|
||||||
async def tally_claim(interaction: discord.Interaction, username: str = "", squadron: str = ""):
|
async def tally_claim(interaction: discord.Interaction, username: str = "", squadron: str = ""):
|
||||||
lang = await guild_lang(interaction.guild.id) if interaction.guild else "en"
|
lang = await guild_lang(interaction.guild.id) if interaction.guild else "en"
|
||||||
if not interaction.guild_id or not await is_guild_entitled(interaction.guild_id):
|
|
||||||
await interaction.response.send_message(t(lang, "commands.tally.premium_required"), ephemeral=True)
|
|
||||||
return
|
|
||||||
vc = _invoker_voice_channel(interaction)
|
vc = _invoker_voice_channel(interaction)
|
||||||
if vc is None:
|
if vc is None:
|
||||||
await interaction.response.send_message(t(lang, "commands.tally.not_in_vc"), ephemeral=True)
|
await interaction.response.send_message(t(lang, "commands.tally.not_in_vc"), ephemeral=True)
|
||||||
return
|
return
|
||||||
|
if not vc.permissions_for(vc.guild.me).set_voice_channel_status:
|
||||||
|
await interaction.response.send_message(t(lang, "commands.tally.no_vc_perm"), ephemeral=True)
|
||||||
|
return
|
||||||
username = username.strip()
|
username = username.strip()
|
||||||
squadron = squadron.strip()
|
squadron = squadron.strip()
|
||||||
if bool(username) == bool(squadron): # neither or both
|
if bool(username) == bool(squadron): # neither or both
|
||||||
@@ -4311,9 +4321,6 @@ async def tally_claim(interaction: discord.Interaction, username: str = "", squa
|
|||||||
@discord.app_commands.autocomplete(username=player_autocomplete)
|
@discord.app_commands.autocomplete(username=player_autocomplete)
|
||||||
async def tally_transfer(interaction: discord.Interaction, username: str):
|
async def tally_transfer(interaction: discord.Interaction, username: str):
|
||||||
lang = await guild_lang(interaction.guild.id) if interaction.guild else "en"
|
lang = await guild_lang(interaction.guild.id) if interaction.guild else "en"
|
||||||
if not interaction.guild_id or not await is_guild_entitled(interaction.guild_id):
|
|
||||||
await interaction.response.send_message(t(lang, "commands.tally.premium_required"), ephemeral=True)
|
|
||||||
return
|
|
||||||
vc = _invoker_voice_channel(interaction)
|
vc = _invoker_voice_channel(interaction)
|
||||||
if vc is None:
|
if vc is None:
|
||||||
await interaction.response.send_message(t(lang, "commands.tally.not_in_vc"), ephemeral=True)
|
await interaction.response.send_message(t(lang, "commands.tally.not_in_vc"), ephemeral=True)
|
||||||
|
|||||||
@@ -861,6 +861,11 @@
|
|||||||
"status_line": "{base}: {verb} against {opponent}",
|
"status_line": "{base}: {verb} against {opponent}",
|
||||||
"not_in_vc": "You must be connected to a voice channel to use this.",
|
"not_in_vc": "You must be connected to a voice channel to use this.",
|
||||||
"premium_required": "This is a premium feature. Use /unlock to enable it for this server.",
|
"premium_required": "This is a premium feature. Use /unlock to enable it for this server.",
|
||||||
|
"no_vc_perm": "The bot is missing the **Set Voice Channel Status** permission in this voice channel. Ask a server admin to grant it.",
|
||||||
|
"no_vc_perm_diagnose": "Missing **Set Voice Channel Status** — `/tally-claim` will fail in {vc}. Grant the bot this permission.",
|
||||||
|
"vc_perm_ok": "**Set Voice Channel Status** — `/tally-claim` can update {vc}.",
|
||||||
|
"vc_perm_header": "Voice Channel Tally ({vc})",
|
||||||
|
"vc_perm_not_in_vc": "Not in a voice channel — join one and re-run to check tally permissions.",
|
||||||
"need_one_input": "Provide exactly one of `username` or `squadron`.",
|
"need_one_input": "Provide exactly one of `username` or `squadron`.",
|
||||||
"already_active": "A tally is already active in **{channel}** tracking **{target}**. Use /tally-transfer or /tally-clear first.",
|
"already_active": "A tally is already active in **{channel}** tracking **{target}**. Use /tally-transfer or /tally-clear first.",
|
||||||
"claimed": "Now tracking **{target}** in **{channel}**. Status set to `0W-0L`.",
|
"claimed": "Now tracking **{target}** in **{channel}**. Status set to `0W-0L`.",
|
||||||
|
|||||||
@@ -266,6 +266,14 @@ async def on_session_processed(guild_id: int, teams: list[dict],
|
|||||||
logging.error(f"[TALLY] on_session_processed error (guild={guild_id}): {e}")
|
logging.error(f"[TALLY] on_session_processed error (guild={guild_id}): {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def on_game_finished(teams: list[dict], winner_short: Optional[str],
|
||||||
|
is_draw: bool, session_id: str) -> None:
|
||||||
|
"""Update all active tallies across every guild for one finished session."""
|
||||||
|
guild_ids = {gid for (gid, _) in _REGISTRY}
|
||||||
|
for guild_id in guild_ids:
|
||||||
|
await on_session_processed(guild_id, teams, winner_short, is_draw, session_id)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Idle sweep
|
# Idle sweep
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user