Files
tipibot/commands/economy_team_commands.py
Rene Arumetsa a280ba05cc feat(teams): add Fienta as primary Discord->team source (sheet fallback)
The registration-log sheet only has in-game nicknames, so matching Discord
users to teams failed for ~10 of 40 teams. Fienta collects each competitor's
Discord username (+ sometimes user ID) and team name per ticket, giving a
reliable Discord-identity -> team mapping (validated: 201 usernames, 42 teams).

- core/fienta.py: token-auth client; fetch /events/{id}/tickets?attendees=true,
  parse competitor/coach/substitute tickets into {username|id -> team} and
  {team -> game}; exclude visitor/supporter/LAN/early-bird/waiting-list. No-op
  when FIENTA_API_TOKEN/FIENTA_EVENT_ID unset.
- member_sync: resolve_team() tries Fienta (id, then username) then the sheet;
  all_managed_team_names() and team_dividers() merge both sources.
- /teamsync + hourly task refresh Fienta alongside the sheet; enabled when
  either source is configured.
- config + .env.example: FIENTA_API_TOKEN, FIENTA_EVENT_ID.
- tests: fienta parsing (game detection, inclusion rules, id/username mapping).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPRsW4tazVtYi2jNzVQkre
2026-09-04 00:50:54 +03:00

99 lines
3.6 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 fienta, sheets
from core.admin import bot_admin_check
from core.member_sync import 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()
@bot_admin_check()
async def cmd_teamsync(interaction: discord.Interaction):
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()
fienta_teams = await fienta.refresh_teams()
except Exception as e:
await interaction.followup.send(
S.TEAMSYNC_UI["refresh_error"].format(error=e), ephemeral=True
)
return
if not rosters and not fienta_teams:
await interaction.followup.send(S.TEAMSYNC_UI["disabled"], ephemeral=True)
return
summary = await sync_all_team_roles(guild, log)
await interaction.followup.send(_format_summary(summary), ephemeral=True)
log.info(
"/teamsync - scanned=%d, assigned=%d, removed=%d, created=%d, "
"positioned=%d, divider_assigned=%d, divider_removed=%d, errors=%d",
summary.scanned,
summary.assigned,
summary.removed,
len(summary.created),
summary.positioned,
summary.divider_assigned,
summary.divider_removed,
len(summary.errors),
)
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.positioned:
lines.append(S.TEAMSYNC_UI["positioned"].format(count=summary.positioned))
if summary.divider_assigned:
lines.append(S.TEAMSYNC_UI["divider_assigned"].format(count=summary.divider_assigned))
if summary.divider_removed:
lines.append(S.TEAMSYNC_UI["divider_removed"].format(count=summary.divider_removed))
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