forked from sass/tipibot
feat(teams): resolve team dividers by role ID instead of name
Divider placement matched the divider role by its exact Discord name, so renaming the role in Discord silently broke positioning. Switch the TEAM_DIVIDER_<SUFFIX> config to hold a role ID; resolve the ID to the role's current name in apply_team_role_positions and keep the existing name-based ordering maths downstream unchanged. - config._parse_team_dividers now parses values as ints (rejects non-ints) - resolve_divider / _team_divider cache / get_team_dividers return IDs - apply_team_role_positions resolves each ID via guild.get_role once - .env.example documents IDs and ships the CS2/LoL divider role IDs - resolve_divider tests updated to assert IDs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPRsW4tazVtYi2jNzVQkre
This commit is contained in:
@@ -363,16 +363,32 @@ async def apply_team_role_positions(
|
||||
"""Move every team role directly beneath its configured divider role.
|
||||
|
||||
Driven by the ``TEAM_DIVIDER_*`` config: teams whose sheet section matched
|
||||
one are placed under that role, the rest are left exactly where they are.
|
||||
Returns ``(roles_moved, errors)``; a no-op returns ``(0, [])``.
|
||||
one are placed under that divider role, the rest are left exactly where they
|
||||
are. Dividers are configured by role ID, so a rename never breaks placement;
|
||||
the ID is resolved to the role's current name here, and the ordering maths
|
||||
downstream is name-based. Returns ``(roles_moved, errors)``; a no-op returns
|
||||
``(0, [])``.
|
||||
"""
|
||||
placements: dict[str, list[str]] = {}
|
||||
for team, divider in sheets.get_team_dividers().items():
|
||||
placements.setdefault(divider, []).append(team)
|
||||
if not placements:
|
||||
team_dividers = sheets.get_team_dividers() # {team: divider role ID}
|
||||
if not team_dividers:
|
||||
return 0, [] # no dividers configured, or nothing matched
|
||||
|
||||
errors: list[str] = []
|
||||
# Resolve each configured divider ID to its role once, then key placements by
|
||||
# that role's current name for the name-based positioning maths below.
|
||||
placements: dict[str, list[str]] = {}
|
||||
resolved: dict[int, discord.Role | None] = {}
|
||||
for team, divider_id in team_dividers.items():
|
||||
if divider_id not in resolved:
|
||||
resolved[divider_id] = guild.get_role(divider_id)
|
||||
if resolved[divider_id] is None:
|
||||
errors.append(f"Eraldajarolli ID {divider_id} ei leitud serverist")
|
||||
divider = resolved[divider_id]
|
||||
if divider is not None:
|
||||
placements.setdefault(divider.name, []).append(team)
|
||||
if not placements:
|
||||
return 0, errors
|
||||
|
||||
by_name: dict[str, discord.Role] = {}
|
||||
for role in sorted(guild.roles, key=lambda r: r.position):
|
||||
by_name.setdefault(role.name, role)
|
||||
|
||||
@@ -280,7 +280,7 @@ _LINEUP_HEADER_PREFIX = "lineup"
|
||||
_team_roster: dict[str, list[str]] = {} # team name -> [Discord username, ...]
|
||||
_team_by_username: dict[str, str] = {} # normalized username -> team name
|
||||
_team_names: set[str] = set() # universe of all team names
|
||||
_team_divider: dict[str, str] = {} # team name -> divider role name
|
||||
_team_divider: dict[str, int] = {} # team name -> divider role ID
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -390,8 +390,8 @@ def parse_team_rosters(rows: list[list]) -> dict[str, list[str]]:
|
||||
return rosters
|
||||
|
||||
|
||||
def resolve_divider(title: str, dividers: dict[str, str] | None = None) -> str | None:
|
||||
"""Return the divider role name configured for a section title, if any.
|
||||
def resolve_divider(title: str, dividers: dict[str, int] | None = None) -> int | None:
|
||||
"""Return the divider role ID configured for a section title, if any.
|
||||
|
||||
A ``TEAM_DIVIDER_<SUFFIX>`` entry matches when every underscore-separated
|
||||
part of its suffix appears as a whole word in the title, so ``CS2`` matches
|
||||
@@ -402,15 +402,15 @@ def resolve_divider(title: str, dividers: dict[str, str] | None = None) -> str |
|
||||
if dividers is None:
|
||||
dividers = config.TEAM_DIVIDERS
|
||||
haystack = title.lower()
|
||||
best_name: str | None = None
|
||||
best_id: int | None = None
|
||||
best_parts = 0
|
||||
for suffix, role_name in dividers.items():
|
||||
for suffix, role_id in dividers.items():
|
||||
parts = [p for p in suffix.split("_") if p]
|
||||
if not parts or len(parts) <= best_parts:
|
||||
continue
|
||||
if all(re.search(rf"\b{re.escape(p)}\b", haystack) for p in parts):
|
||||
best_name, best_parts = role_name, len(parts)
|
||||
return best_name
|
||||
best_id, best_parts = role_id, len(parts)
|
||||
return best_id
|
||||
|
||||
|
||||
def build_username_index(rosters: dict[str, list[str]]) -> dict[str, str]:
|
||||
@@ -441,7 +441,7 @@ def _refresh_teams_sync() -> dict[str, list[str]]:
|
||||
spreadsheet = client.open_by_key(config.TEAM_SHEET_ID)
|
||||
|
||||
rosters: dict[str, list[str]] = {}
|
||||
dividers: dict[str, str] = {}
|
||||
dividers: dict[str, int] = {}
|
||||
for ws in spreadsheet.worksheets():
|
||||
for section in parse_team_sections(ws.get_all_values()):
|
||||
divider = resolve_divider(section.title)
|
||||
@@ -483,8 +483,8 @@ def get_team_rosters() -> dict[str, list[str]]:
|
||||
return _team_roster
|
||||
|
||||
|
||||
def get_team_dividers() -> dict[str, str]:
|
||||
"""Current {team: divider role name} cache.
|
||||
def get_team_dividers() -> dict[str, int]:
|
||||
"""Current {team: divider role ID} cache.
|
||||
|
||||
Only teams whose section title matched a configured TEAM_DIVIDER_* entry
|
||||
appear here, so an empty dict means positioning is switched off.
|
||||
|
||||
Reference in New Issue
Block a user