refactor(teams): move team-role sync to the economy/community bot

The team-role feature was aimed at the wrong bot. Tournament participants
live in the economy/community guild, but team-role sync had been bolted onto
the dev bot's roster sync (sync_member), which bails out for anyone missing
from the internal member sheet - so it could never reach its actual audience.

Decouple it: keep the (roster-independent) team-sheet parsing, pull the team
wiring off the dev/member-sync path, and re-home it on the economy bot.

- core/member_sync: revert sync_member to add-only (drop team block +
  SyncResult.roles_removed); add roster-independent sync_team_role and a
  whole-guild sync_all_team_roles returning a reporting summary. Still only
  ever touches role NAMES present in the team sheet.
- commands/economy_team_commands: new admin-only /teamsync command.
- bot.py: hourly team_sync_hourly task (economy-only, no-op unless
  TEAM_SHEET_ID is set; first tick at boot covers startup load); register
  /teamsync under the economy profile; drop the dev-side startup load.
- commands/dev_member_commands: /check no longer refreshes teams or reports
  removed roles.
- strings + .env.example: TEAMSYNC_UI, CMD[teamsync], document TEAM_SHEET_ID.
- tests: retarget sync tests to sync_team_role; add sync_all_team_roles case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6JZkyszyDFuFtk25WBbcR
This commit is contained in:
Rene Arumetsa
2026-08-28 15:04:23 +03:00
parent 52002c37fc
commit ce1ed28904
9 changed files with 331 additions and 103 deletions

52
bot.py
View File

@@ -23,7 +23,7 @@ import config
import strings as S
from core import economy, pb_client, sheets
from core.admin import is_bot_admin
from core.member_sync import SyncResult
from core.member_sync import SyncResult, sync_all_team_roles
from commands.dev_member_commands import register_dev_member_commands
from commands.dev_member_runtime import handle_member_join, run_birthday_daily
from commands.economy_admin_commands import register_economy_admin_commands
@@ -35,6 +35,7 @@ from commands.economy_prestige_commands import register_prestige_commands
from commands.economy_quests_commands import register_economy_quests_commands
from commands.economy_profile_commands import register_economy_profile_commands
from commands.economy_support_commands import register_economy_support_commands
from commands.economy_team_commands import register_economy_team_commands
from commands.ops_channel_commands import register_ops_channel_commands
from commands.ops_admin_commands import register_ops_admin_commands
from commands.info_commands import register_info_commands
@@ -336,6 +337,40 @@ async def before_birthday_daily():
await bot.wait_until_ready()
@tasks.loop(hours=1)
async def team_sync_hourly():
"""Reload the tournament registration sheet and re-apply team roles.
Economy profile only (the tournament players live in the community guild).
Runs the first iteration immediately on start, so this also covers the
initial load at boot. No-op when TEAM_SHEET_ID is unset.
"""
if IS_DEV_PROFILE or not config.TEAM_SHEET_ID:
return
try:
rosters = await sheets.refresh_teams()
except Exception as e:
log.error("team_sync_hourly: failed to load team sheet: %s", e)
return
if not rosters:
return
guild = bot.get_guild(config.GUILD_ID)
if guild is None:
log.warning("team_sync_hourly: guild %s not found", config.GUILD_ID)
return
summary = await sync_all_team_roles(guild, log)
if summary.assigned or summary.removed or summary.created:
log.info(
"team_sync_hourly: assigned=%d, removed=%d, created=%d, errors=%d",
summary.assigned, summary.removed, len(summary.created), len(summary.errors),
)
@team_sync_hourly.before_loop
async def before_team_sync_hourly():
await bot.wait_until_ready()
# ---------------------------------------------------------------------------
# Rotating rich presence
# ---------------------------------------------------------------------------
@@ -420,12 +455,6 @@ async def on_ready():
log.info("Loaded %d member rows from Google Sheets", len(data))
except Exception as e:
log.error("Failed to load sheet on startup: %s", e)
try:
rosters = await sheets.refresh_teams()
if rosters:
log.info("Loaded %d teams from the registration sheet", len(rosters))
except Exception as e:
log.error("Failed to load team sheet on startup: %s", e)
# Sync slash commands to the guild only; wipe any leftover global registrations
tree.copy_global_to(guild=GUILD_OBJ)
@@ -439,6 +468,11 @@ async def on_ready():
birthday_daily.start()
log.info("Birthday daily task started (fires 09:00 Tallinn time)")
# Start hourly tournament team-role sync (economy/community guild)
if not IS_DEV_PROFILE and config.TEAM_SHEET_ID and not team_sync_hourly.is_running():
team_sync_hourly.start()
log.info("Team-role sync task started (hourly, from the registration sheet)")
# Start rotating rich presence
if not _rotate_presence.is_running():
_rotate_presence.start()
@@ -496,6 +530,10 @@ if IS_DEV_PROFILE:
has_announced_today=_has_announced_today,
mark_announced_today=_mark_announced_today,
)
else:
# Tournament team-role sync lives on the economy/community bot, where the
# registered players actually are (see commands/economy_team_commands.py).
register_economy_team_commands(tree, bot, log)
register_ops_admin_commands(
tree,