diff --git a/BOT/autologging.py b/BOT/autologging.py index 3fa2c1f..f3cf2cf 100644 --- a/BOT/autologging.py +++ b/BOT/autologging.py @@ -534,6 +534,19 @@ async def process_ws_replays(replays: list[dict]): ) 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) validated_games.append({ "sessionIdHex": hex_id, @@ -1132,14 +1145,6 @@ async def process_session( else: 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 lock = _scoreboard_locks.setdefault(session_id, asyncio.Lock()) async with lock: diff --git a/BOT/botscript.py b/BOT/botscript.py index 1c8bcb0..203b4e2 100644 --- a/BOT/botscript.py +++ b/BOT/botscript.py @@ -1233,6 +1233,16 @@ async def _diagnose_perms_logic(interaction: discord.Interaction, target_channel lines.append(t(lang, "diagnostics.premium_autolog_required")) 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 ── embed = discord.Embed( 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) async def tally_claim(interaction: discord.Interaction, username: str = "", squadron: str = ""): 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) if vc is None: await interaction.response.send_message(t(lang, "commands.tally.not_in_vc"), ephemeral=True) 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() squadron = squadron.strip() 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) async def tally_transfer(interaction: discord.Interaction, username: str): 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) if vc is None: await interaction.response.send_message(t(lang, "commands.tally.not_in_vc"), ephemeral=True) diff --git a/BOT/locales/en.json b/BOT/locales/en.json index 99d6176..101f98a 100644 --- a/BOT/locales/en.json +++ b/BOT/locales/en.json @@ -861,6 +861,11 @@ "status_line": "{base}: {verb} against {opponent}", "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.", + "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`.", "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`.", diff --git a/BOT/tally.py b/BOT/tally.py index 075711a..bf95344 100644 --- a/BOT/tally.py +++ b/BOT/tally.py @@ -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}") +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 # ---------------------------------------------------------------------------