forked from sass/tipibot
Creation-time placement only positions NEW team roles. Team roles made before divider placement existed (sitting at the bottom of the role list) stayed put. Add `reposition_team_roles(guild)` and expose it via `/teamsync reposition:true` (off by default, so the hourly task never reorders roles on its own). It moves each EXISTING team role under its game's divider, sheet-driven: - a role's game/divider comes from sheets.get_game_for_team; - roles whose team is no longer in the sheet (or game unknown) are left alone; - idempotent: a role already within its section band (below its own divider, above the next divider down) is skipped, so re-runs move nothing. Adds RepositionSummary, TEAMSYNC_UI/OPT strings, and tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6JZkyszyDFuFtk25WBbcR
111 lines
3.9 KiB
Python
111 lines
3.9 KiB
Python
"""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)
|