meow (#1330)
This commit is contained in:
+17
-12
@@ -74,19 +74,22 @@ def _present_players(game: dict[str, Any]) -> set[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _bar_color(game: dict[str, Any], guild_id: int) -> 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"):
|
if game.get("draw"):
|
||||||
return "draw"
|
return "draw"
|
||||||
guild_team = preferences.get_guild_team(guild_id)
|
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"
|
return "not_set"
|
||||||
my_id = str(guild_team["team_id"])
|
|
||||||
tss = game.get("tss") or {}
|
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 "")
|
winner = str(game.get("winner") or "")
|
||||||
if slot_ids.get(winner) == my_id:
|
if slot_names.get(winner) == my_name:
|
||||||
return "win"
|
return "win"
|
||||||
if my_id in slot_ids.values():
|
if my_name in slot_names.values():
|
||||||
return "loss"
|
return "loss"
|
||||||
return "not_involved"
|
return "not_involved"
|
||||||
|
|
||||||
@@ -176,26 +179,28 @@ async def process_game(game: dict[str, Any]) -> None:
|
|||||||
if not session_id:
|
if not session_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
present_team_ids, present_team_names = _present_teams(game)
|
_, present_team_names = _present_teams(game)
|
||||||
present_uids = _present_players(game)
|
present_uids = _present_players(game)
|
||||||
sent = _sent_channels_by_session.setdefault(session_id, set())
|
sent = _sent_channels_by_session.setdefault(session_id, set())
|
||||||
|
|
||||||
for guild_id, prefs in preferences.iter_guild_preferences():
|
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):
|
if not isinstance(entry, dict):
|
||||||
continue
|
continue
|
||||||
type_ = entry.get("Type")
|
type_ = entry.get("Type")
|
||||||
if type_ not in ("tss-team", "tss-player"):
|
if type_ not in ("tss-team", "tss-player", "tss-wildcard"):
|
||||||
continue
|
continue
|
||||||
channel_id, enabled = preferences.parse_channel(entry.get("Logs"))
|
channel_id, enabled = preferences.parse_channel(entry.get("Logs"))
|
||||||
if not channel_id or not enabled or channel_id in sent:
|
if not channel_id or not enabled or channel_id in sent:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if type_ == "tss-team":
|
if type_ == "tss-wildcard":
|
||||||
|
matched = True
|
||||||
|
elif type_ == "tss-team":
|
||||||
name = (entry.get("Name") or "").lower()
|
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
|
else: # tss-player
|
||||||
matched = str(entity_id) in present_uids
|
matched = str(entry.get("UID") or "") in present_uids
|
||||||
if not matched:
|
if not matched:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
+86
-10
@@ -58,8 +58,11 @@ async def team_autocomplete(interaction: discord.Interaction, current: str) -> L
|
|||||||
return []
|
return []
|
||||||
choices = []
|
choices = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
label = (r.get("long_name") or str(r["team_id"]))[:100]
|
# Teams are tracked by name (ids are per-tournament and meaningless), so the
|
||||||
choices.append(app_commands.Choice(name=label, value=str(r["team_id"])))
|
# 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
|
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
|
# Cog
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -123,15 +174,15 @@ class TssCommands(commands.Cog):
|
|||||||
resolved = await storage.resolve_team(team)
|
resolved = await storage.resolve_team(team)
|
||||||
if not resolved:
|
if not resolved:
|
||||||
return await interaction.followup.send(f"Could not find a team matching **{team}**.", ephemeral=True)
|
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)
|
preferences.set_guild_team(interaction.guild_id, int(resolved["team_id"]), name)
|
||||||
await interaction.followup.send(
|
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 ──────────────────────────────────────────────────────────
|
# ── /log-team ──────────────────────────────────────────────────────────
|
||||||
@app_commands.command(name="log-team", description="Send a team's matches to this channel")
|
@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 ID")
|
@app_commands.describe(team="TSS team name, or */all/everything to log every match")
|
||||||
@app_commands.autocomplete(team=team_autocomplete)
|
@app_commands.autocomplete(team=team_autocomplete)
|
||||||
@app_commands.checks.has_permissions(manage_guild=True)
|
@app_commands.checks.has_permissions(manage_guild=True)
|
||||||
@not_blacklisted()
|
@not_blacklisted()
|
||||||
@@ -139,12 +190,22 @@ class TssCommands(commands.Cog):
|
|||||||
await interaction.response.defer(ephemeral=True)
|
await interaction.response.defer(ephemeral=True)
|
||||||
if interaction.guild_id is None or interaction.channel_id is None:
|
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)
|
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)
|
resolved = await storage.resolve_team(team)
|
||||||
if not resolved:
|
if not resolved:
|
||||||
return await interaction.followup.send(f"Could not find a team matching **{team}**.", ephemeral=True)
|
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(
|
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(
|
await interaction.followup.send(
|
||||||
f"✅ Logging **{name}**'s matches to <#{interaction.channel_id}>.", ephemeral=True
|
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)
|
return await interaction.followup.send(_too_many_msg(candidates), ephemeral=True)
|
||||||
uid, nick = str(candidates[0]["uid"]), candidates[0]["nick"]
|
uid, nick = str(candidates[0]["uid"]), candidates[0]["nick"]
|
||||||
preferences.upsert_log_entry(
|
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(
|
await interaction.followup.send(
|
||||||
f"✅ Logging **{nick}**'s matches to <#{interaction.channel_id}>.", ephemeral=True
|
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)
|
embeds[-1].set_footer(text=FOOTER)
|
||||||
await interaction.followup.send(embeds=embeds)
|
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 ──────────────────────────────────────────────────────────────
|
# ── /help ──────────────────────────────────────────────────────────────
|
||||||
@app_commands.command(name="help", description="List TSSBOT commands and what they do")
|
@app_commands.command(name="help", description="List TSSBOT commands and what they do")
|
||||||
@not_blacklisted()
|
@not_blacklisted()
|
||||||
@@ -320,7 +393,10 @@ class TssCommands(commands.Cog):
|
|||||||
)
|
)
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="Info",
|
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,
|
inline=False,
|
||||||
)
|
)
|
||||||
embed.set_footer(text=FOOTER)
|
embed.set_footer(text=FOOTER)
|
||||||
|
|||||||
+30
-4
@@ -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)
|
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(
|
def upsert_log_entry(
|
||||||
guild_id: int | str,
|
guild_id: int | str,
|
||||||
entity_id: int | str,
|
key: str,
|
||||||
type_: str,
|
type_: str,
|
||||||
name: str,
|
name: str,
|
||||||
channel_id: int | str,
|
channel_id: int | str,
|
||||||
|
extra: Optional[dict[str, Any]] = None,
|
||||||
) -> bool:
|
) -> 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)
|
prefs = load_guild_preferences(guild_id)
|
||||||
entry = prefs.setdefault(str(entity_id), {})
|
entry = prefs.setdefault(key, {})
|
||||||
if not isinstance(entry, dict):
|
if not isinstance(entry, dict):
|
||||||
entry = {}
|
entry = {}
|
||||||
prefs[str(entity_id)] = entry
|
prefs[key] = entry
|
||||||
entry["Type"] = type_
|
entry["Type"] = type_
|
||||||
entry["Name"] = name
|
entry["Name"] = name
|
||||||
entry["Logs"] = channel_mention(channel_id)
|
entry["Logs"] = channel_mention(channel_id)
|
||||||
|
if extra:
|
||||||
|
entry.update(extra)
|
||||||
return save_guild_preferences(guild_id, prefs)
|
return save_guild_preferences(guild_id, prefs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user