"""Tournament team-role sync for the economy/community guild. The tournament participants live in the *economy* (community) guild, not the internal dev guild, so team-role assignment runs here rather than as part of the member-roster sync in :mod:`commands.dev_member_commands`. This is deliberately roster-INDEPENDENT: it matches Discord usernames straight against the separate registration spreadsheet (``TEAM_SHEET_ID``) and never touches the member sheet. """ from __future__ import annotations import logging import discord from discord import app_commands from core import sheets from core.admin import bot_admin_check from core.member_sync import reposition_team_roles, sync_all_team_roles import strings as S def register_economy_team_commands( tree: app_commands.CommandTree, bot: discord.Client, log: logging.Logger, ) -> None: @tree.command(name="teamsync", description=S.CMD["teamsync"]) @app_commands.guild_only() @app_commands.describe(reposition=S.OPT["teamsync_reposition"]) @bot_admin_check() async def cmd_teamsync(interaction: discord.Interaction, reposition: bool = False): await interaction.response.defer(ephemeral=True) guild = interaction.guild if guild is None: await interaction.followup.send(S.ERR["guild_only"], ephemeral=True) return try: rosters = await sheets.refresh_teams() except Exception as e: await interaction.followup.send( S.TEAMSYNC_UI["refresh_error"].format(error=e), ephemeral=True ) return if not rosters: await interaction.followup.send(S.TEAMSYNC_UI["disabled"], ephemeral=True) return summary = await sync_all_team_roles(guild, log) message = _format_summary(summary) repo = None if reposition: repo = await reposition_team_roles(guild, log) message += "\n\n" + _format_reposition(repo) await interaction.followup.send(message, ephemeral=True) log.info( "/teamsync - scanned=%d, assigned=%d, removed=%d, created=%d, errors=%d%s", summary.scanned, summary.assigned, summary.removed, len(summary.created), len(summary.errors), f", repositioned={repo.moved}" if repo else "", ) def _format_summary(summary) -> str: lines = [ S.TEAMSYNC_UI["done"], S.TEAMSYNC_UI["scanned"].format(count=summary.scanned), S.TEAMSYNC_UI["assigned"].format(count=summary.assigned), S.TEAMSYNC_UI["removed"].format(count=summary.removed), ] if summary.created: # A team can be created only once, but the same role could surface for # several members in one run - de-dupe for the report. unique = list(dict.fromkeys(summary.created)) lines.append(S.TEAMSYNC_UI["created"].format(roles=", ".join(unique))) if summary.errors: lines.append(S.TEAMSYNC_UI["errors"].format(count=len(summary.errors))) text = "\n".join(lines) if summary.changes: shown = summary.changes[:20] text += "\n\n" + S.TEAMSYNC_UI["changes_header"] + "\n" + "\n".join(shown) if len(summary.changes) > 20: text += "\n" + S.TEAMSYNC_UI["changes_more"].format(count=len(summary.changes) - 20) else: text += "\n\n" + S.TEAMSYNC_UI["no_changes"] return text def _format_reposition(repo) -> str: lines = [S.TEAMSYNC_UI["reposition_header"]] if not repo.moved: lines.append(S.TEAMSYNC_UI["reposition_none"]) else: lines.append(S.TEAMSYNC_UI["repositioned"].format(count=repo.moved)) lines.extend(repo.moves[:20]) if len(repo.moves) > 20: lines.append(S.TEAMSYNC_UI["changes_more"].format(count=len(repo.moves) - 20)) if repo.errors: lines.append(S.TEAMSYNC_UI["errors"].format(count=len(repo.errors))) return "\n".join(lines)