diff --git a/BOT/autologging.py b/BOT/autologging.py index db14332..89ded69 100644 --- a/BOT/autologging.py +++ b/BOT/autologging.py @@ -74,19 +74,22 @@ def _present_players(game: dict[str, Any]) -> set[str]: def _bar_color(game: dict[str, Any], guild_id: int) -> str: - """Header/separator tint from the guild's own team vs the result.""" + """Header/separator tint from the guild's own team vs the result. + + Compared by team *name* (case-insensitive) — TSS team ids are per-tournament. + """ if game.get("draw"): return "draw" guild_team = preferences.get_guild_team(guild_id) - if not guild_team or guild_team.get("team_id") is None: + my_name = str((guild_team or {}).get("TM_Name") or "").casefold() + if not my_name: return "not_set" - my_id = str(guild_team["team_id"]) tss = game.get("tss") or {} - slot_ids = {s: str((tss.get(s) or {}).get("team_id")) for s in ("1", "2")} + slot_names = {s: str((tss.get(s) or {}).get("team_name") or "").casefold() for s in ("1", "2")} winner = str(game.get("winner") or "") - if slot_ids.get(winner) == my_id: + if slot_names.get(winner) == my_name: return "win" - if my_id in slot_ids.values(): + if my_name in slot_names.values(): return "loss" return "not_involved" @@ -176,26 +179,28 @@ async def process_game(game: dict[str, Any]) -> None: if not session_id: return - present_team_ids, present_team_names = _present_teams(game) + _, present_team_names = _present_teams(game) present_uids = _present_players(game) sent = _sent_channels_by_session.setdefault(session_id, set()) for guild_id, prefs in preferences.iter_guild_preferences(): - for entity_id, entry in prefs.items(): + for _pref_key, entry in prefs.items(): if not isinstance(entry, dict): continue type_ = entry.get("Type") - if type_ not in ("tss-team", "tss-player"): + if type_ not in ("tss-team", "tss-player", "tss-wildcard"): continue channel_id, enabled = preferences.parse_channel(entry.get("Logs")) if not channel_id or not enabled or channel_id in sent: continue - if type_ == "tss-team": + if type_ == "tss-wildcard": + matched = True + elif type_ == "tss-team": name = (entry.get("Name") or "").lower() - matched = str(entity_id) in present_team_ids or (bool(name) and name in present_team_names) + matched = bool(name) and name in present_team_names else: # tss-player - matched = str(entity_id) in present_uids + matched = str(entry.get("UID") or "") in present_uids if not matched: continue diff --git a/BOT/commands.py b/BOT/commands.py index 88b0019..a595c6d 100644 --- a/BOT/commands.py +++ b/BOT/commands.py @@ -58,8 +58,11 @@ async def team_autocomplete(interaction: discord.Interaction, current: str) -> L return [] choices = [] for r in rows: - label = (r.get("long_name") or str(r["team_id"]))[:100] - choices.append(app_commands.Choice(name=label, value=str(r["team_id"]))) + # Teams are tracked by name (ids are per-tournament and meaningless), so the + # autocomplete value is the name itself. + name = str(r.get("name") or "") + if name: + choices.append(app_commands.Choice(name=name[:100], value=name[:100])) return choices @@ -83,6 +86,54 @@ def _too_many_msg(candidates: List[dict]) -> str: ) +# --------------------------------------------------------------------------- +# Language picker (mirrors SREBOT's /language dropdown) +# --------------------------------------------------------------------------- + +# Displayed language name -> canonical stored value (the LangTableReader column, +# shared with SREBOT's lang/units.csv). Kept identical to SREBOT's mapping. +LANGUAGE_MAPPING = { + "English": "", + "Français": "", + "Italiano": "", + "Deutsch": "", + "Español": "", + "Русский": "", + "Polski": "", + "Čeština": "", + "简体中文": "", + "Português": "", + "Українська": "", +} + + +class LanguageSelect(discord.ui.Select): + """Dropdown for choosing the server's vehicle-name language.""" + + def __init__(self): + options = [discord.SelectOption(label=label, value=label) for label in LANGUAGE_MAPPING] + super().__init__(placeholder="Select a language", min_values=1, max_values=1, options=options) + + async def callback(self, interaction: discord.Interaction): + if interaction.guild is None: + return await interaction.response.send_message("This command must be used in a server.", ephemeral=True) + selected = self.values[0] + canonical = LANGUAGE_MAPPING.get(selected, f"<{selected}>") + features = preferences.load_features(interaction.guild.id) + features["Language"] = canonical + preferences.save_features(interaction.guild.id, features) + log.info("Guild %s (%s) set language to %s", interaction.guild.name, interaction.guild.id, canonical) + await interaction.response.send_message(f"✅ Language set to **{selected}**.", ephemeral=True) + + +class LanguageView(discord.ui.View): + """View wrapper holding the language dropdown.""" + + def __init__(self): + super().__init__() + self.add_item(LanguageSelect()) + + # --------------------------------------------------------------------------- # Cog # --------------------------------------------------------------------------- @@ -123,15 +174,15 @@ class TssCommands(commands.Cog): resolved = await storage.resolve_team(team) if not resolved: return await interaction.followup.send(f"Could not find a team matching **{team}**.", ephemeral=True) - name = resolved.get("long_name") or str(resolved["team_id"]) + name = resolved.get("name") or str(resolved["team_id"]) preferences.set_guild_team(interaction.guild_id, int(resolved["team_id"]), name) await interaction.followup.send( - f"✅ This server's team is now **{name}** (id `{resolved['team_id']}`).", ephemeral=True + f"✅ This server's team is now **{name}**.", ephemeral=True ) # ── /log-team ────────────────────────────────────────────────────────── - @app_commands.command(name="log-team", description="Send a team's matches to this channel") - @app_commands.describe(team="TSS team name or ID") + @app_commands.command(name="log-team", description="Send a team's matches to this channel (or * for all)") + @app_commands.describe(team="TSS team name, or */all/everything to log every match") @app_commands.autocomplete(team=team_autocomplete) @app_commands.checks.has_permissions(manage_guild=True) @not_blacklisted() @@ -139,12 +190,22 @@ class TssCommands(commands.Cog): await interaction.response.defer(ephemeral=True) if interaction.guild_id is None or interaction.channel_id is None: return await interaction.followup.send("This command must be used in a server channel.", ephemeral=True) + # Wildcard: log every TSS match to this channel. + if team.strip().lower() in preferences.WILDCARD_TOKENS: + preferences.upsert_log_entry( + interaction.guild_id, preferences.WILDCARD_KEY, "tss-wildcard", + "All matches", interaction.channel_id, + ) + return await interaction.followup.send( + f"✅ Logging **all TSS matches** to <#{interaction.channel_id}>.", ephemeral=True + ) resolved = await storage.resolve_team(team) if not resolved: return await interaction.followup.send(f"Could not find a team matching **{team}**.", ephemeral=True) - name = resolved.get("long_name") or str(resolved["team_id"]) + # Tracked by name — team ids are per-tournament and not stable. + name = resolved.get("name") or str(resolved["team_id"]) preferences.upsert_log_entry( - interaction.guild_id, int(resolved["team_id"]), "tss-team", name, interaction.channel_id + interaction.guild_id, preferences.team_pref_key(name), "tss-team", name, interaction.channel_id ) await interaction.followup.send( f"✅ Logging **{name}**'s matches to <#{interaction.channel_id}>.", ephemeral=True @@ -167,7 +228,8 @@ class TssCommands(commands.Cog): return await interaction.followup.send(_too_many_msg(candidates), ephemeral=True) uid, nick = str(candidates[0]["uid"]), candidates[0]["nick"] preferences.upsert_log_entry( - interaction.guild_id, uid, "tss-player", nick, interaction.channel_id + interaction.guild_id, preferences.player_pref_key(uid), "tss-player", nick, + interaction.channel_id, extra={"UID": uid}, ) await interaction.followup.send( f"✅ Logging **{nick}**'s matches to <#{interaction.channel_id}>.", ephemeral=True @@ -291,6 +353,17 @@ class TssCommands(commands.Cog): embeds[-1].set_footer(text=FOOTER) await interaction.followup.send(embeds=embeds) + # ── /languages ───────────────────────────────────────────────────────── + @app_commands.command(name="language", description="Set the language used for vehicle names") + @app_commands.checks.has_permissions(manage_guild=True) + @not_blacklisted() + async def language(self, interaction: discord.Interaction): + if interaction.guild_id is None: + return await interaction.response.send_message("This command must be used in a server.", ephemeral=True) + await interaction.response.send_message( + "Choose this server's language for vehicle names:", view=LanguageView(), ephemeral=True + ) + # ── /help ────────────────────────────────────────────────────────────── @app_commands.command(name="help", description="List TSSBOT commands and what they do") @not_blacklisted() @@ -320,7 +393,10 @@ class TssCommands(commands.Cog): ) embed.add_field( name="Info", - value="`/news` — latest announcements\n`/help` — this message", + value=( + "`/language` — set the language for vehicle names\n" + "`/news` — latest announcements\n`/help` — this message" + ), inline=False, ) embed.set_footer(text=FOOTER) diff --git a/BOT/preferences.py b/BOT/preferences.py index fe08c96..349ec48 100644 --- a/BOT/preferences.py +++ b/BOT/preferences.py @@ -119,22 +119,48 @@ def save_guild_preferences(guild_id: int | str, prefs: dict[str, Any]) -> bool: return _write_json(_guild_pref_path(guild_id), prefs) +# Tokens a user can type to mean "log everything". +WILDCARD_TOKENS: frozenset[str] = frozenset({"*", "all", "everything"}) +# Storage key for the wildcard route. NOT "*" — SREBOT treats keys in {"*","all", +# "everything"} as ITS wildcard and would post SRE boards to this channel. Prefixed +# keys also stop a numeric team-name/uid colliding with an SRE clan_id key. +WILDCARD_KEY = "tss-wildcard" + + +def team_pref_key(name: str) -> str: + """Storage key for a team route (teams have no stable id — keyed by name).""" + return f"tss-team:{name.casefold()}" + + +def player_pref_key(uid: int | str) -> str: + """Storage key for a player route (keyed by stable uid).""" + return f"tss-player:{uid}" + + def upsert_log_entry( guild_id: int | str, - entity_id: int | str, + key: str, type_: str, name: str, channel_id: int | str, + extra: Optional[dict[str, Any]] = None, ) -> bool: - """Add/replace a ``tss-team``/``tss-player`` route for a guild.""" + """Add/replace a ``tss-team``/``tss-player``/``tss-wildcard`` route for a guild. + + ``key`` is the storage slot (use ``team_pref_key``/``player_pref_key``/ + ``WILDCARD_KEY``); the matchable value lives in fields (``Name`` for teams, + ``UID`` for players) so the key stays SRE-collision-safe. + """ prefs = load_guild_preferences(guild_id) - entry = prefs.setdefault(str(entity_id), {}) + entry = prefs.setdefault(key, {}) if not isinstance(entry, dict): entry = {} - prefs[str(entity_id)] = entry + prefs[key] = entry entry["Type"] = type_ entry["Name"] = name entry["Logs"] = channel_mention(channel_id) + if extra: + entry.update(extra) return save_guild_preferences(guild_id, prefs)