This commit is contained in:
NotSoToothless
2026-06-17 01:21:00 -07:00
committed by GitHub
parent 7bd8a1b72c
commit c5b74de367
3 changed files with 133 additions and 26 deletions
+17 -12
View File
@@ -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
+86 -10
View File
@@ -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": "<English>",
"Français": "<French>",
"Italiano": "<Italian>",
"Deutsch": "<German>",
"Español": "<Spanish>",
"Русский": "<Russian>",
"Polski": "<Polish>",
"Čeština": "<Czech>",
"简体中文": "<Chinese>",
"Português": "<Portuguese>",
"Українська": "<Ukrainian>",
}
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)
+30 -4
View File
@@ -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)