lets get this party starteddddd (#1287)
This commit is contained in:
+4
-3
@@ -28,6 +28,7 @@ import discord
|
||||
# Local Module Imports
|
||||
from . import utils
|
||||
from data_parser import LangTableReader
|
||||
from shared_store import blacklisted_squadrons
|
||||
from .game_api import get_point_diff
|
||||
from .render_replay import load_gob_file, render_gob
|
||||
from .health import record_game_processed, record_ws_message
|
||||
@@ -42,7 +43,6 @@ from .utils import (
|
||||
SQ_BATTLES_DB_PATH,
|
||||
SQUADRONS_DB_PATH,
|
||||
BLACKLISTED_SERVER_IDS,
|
||||
BLACKLISTED_SQUADRONS,
|
||||
DEFAULT_FOOTER_CAT,
|
||||
compress_json,
|
||||
decompress_json,
|
||||
@@ -50,6 +50,7 @@ from .utils import (
|
||||
norm,
|
||||
resolve_clans,
|
||||
resolve_pref_key,
|
||||
is_foreign_pref_entry,
|
||||
load_features,
|
||||
remove_guild_pref_notification,
|
||||
PREMIUM_ACTIVATION_TS,
|
||||
@@ -653,7 +654,7 @@ async def build_hex_plus_guild(
|
||||
# under all of its known names so it can be found regardless of which
|
||||
# form ends up in the replay JSON.
|
||||
for key, cfg in prefs.items():
|
||||
if not isinstance(cfg, dict):
|
||||
if not isinstance(cfg, dict) or is_foreign_pref_entry(cfg):
|
||||
continue
|
||||
chan = cfg.get("Logs")
|
||||
if not chan or "DISABLED" in str(chan).upper():
|
||||
@@ -692,7 +693,7 @@ async def build_hex_plus_guild(
|
||||
|
||||
|
||||
# PHASE 2: Match games against pre-built lookup tables
|
||||
blacklisted_squad_norms = {norm(bl) for bl in BLACKLISTED_SQUADRONS}
|
||||
blacklisted_squad_norms = {norm(bl) for bl in blacklisted_squadrons()}
|
||||
|
||||
for g in games:
|
||||
sid = g.get("sessionIdHex", "")
|
||||
|
||||
+174
-5
@@ -48,6 +48,7 @@ from .analytics import (
|
||||
)
|
||||
from .autologging import init_players_db, load_replay_data_from_disk, build_scoreboard_view
|
||||
from data_parser import LangTableReader, count_unit_types, get_unit_type_abbrev, normalize_name
|
||||
from shared_store import get_linked_uid, save_player_link
|
||||
from .game_api import (
|
||||
ClanInfoError,
|
||||
obtain_clan_info_api,
|
||||
@@ -1140,6 +1141,8 @@ async def _diagnose_perms_logic(interaction: discord.Interaction, target_channel
|
||||
else:
|
||||
has_any_logs = False
|
||||
for key, cfg in prefs.items():
|
||||
if utils.is_foreign_pref_entry(cfg):
|
||||
continue
|
||||
logs_chan = cfg.get("Logs", "")
|
||||
if not logs_chan or "DISABLED" in str(logs_chan).upper():
|
||||
continue
|
||||
@@ -1227,7 +1230,7 @@ def _configured_pref_targets(preferences: dict[str, Any]) -> list[dict[str, Any]
|
||||
targets: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str, int]] = set()
|
||||
for squadron, settings in preferences.items():
|
||||
if not isinstance(settings, dict):
|
||||
if not isinstance(settings, dict) or utils.is_foreign_pref_entry(settings):
|
||||
continue
|
||||
pref_key = str(squadron)
|
||||
display_name = _format_pref_target_name(pref_key, settings)
|
||||
@@ -1288,7 +1291,7 @@ def _management_pref_rows(
|
||||
) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for squadron, settings in preferences.items():
|
||||
if not isinstance(settings, dict):
|
||||
if not isinstance(settings, dict) or utils.is_foreign_pref_entry(settings):
|
||||
continue
|
||||
for storage_type in _notif_types_for_management(notif_type):
|
||||
if storage_type in settings:
|
||||
@@ -3477,7 +3480,7 @@ class CardPlayerSelectView(View):
|
||||
async def card(
|
||||
interaction: discord.Interaction,
|
||||
season: str,
|
||||
player: str,
|
||||
player: str = "",
|
||||
theme: app_commands.Choice[str] | None = None,
|
||||
):
|
||||
"""Generate and send a season recap card PNG for a player.
|
||||
@@ -3505,6 +3508,18 @@ async def card(
|
||||
embed.set_footer(text=DEFAULT_FOOTER_CAT)
|
||||
return await interaction.followup.send(embed=embed, ephemeral=True)
|
||||
|
||||
# No player given — fall back to the caller's linked account, if any.
|
||||
if not player:
|
||||
linked_uid = get_linked_uid(interaction.user.id)
|
||||
if not linked_uid:
|
||||
return await interaction.followup.send(
|
||||
t(lang, "player.must_provide_or_link"), ephemeral=True
|
||||
)
|
||||
nick = await _latest_nick_for_uid(linked_uid)
|
||||
return await _send_player_card(
|
||||
interaction, int(linked_uid), nick, season, theme_value, lang, followup=True,
|
||||
)
|
||||
|
||||
try:
|
||||
async with aiosqlite.connect(SQ_BATTLES_DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
@@ -3960,11 +3975,17 @@ async def player_stats(interaction: discord.Interaction, username: str = "", uid
|
||||
await interaction.followup.send(t(lang, "common.database_error", error=error_str), ephemeral=True)
|
||||
return
|
||||
else:
|
||||
# No explicit input — fall back to the caller's linked account, if any.
|
||||
linked_uid = get_linked_uid(interaction.user.id)
|
||||
if not linked_uid:
|
||||
await interaction.response.send_message(
|
||||
t(lang, "player.must_provide_input"),
|
||||
t(lang, "player.must_provide_or_link"),
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
await interaction.response.defer(thinking=True)
|
||||
uid = linked_uid
|
||||
target_uid = linked_uid
|
||||
|
||||
# UID path: fetch vehicle stats (username path already has them from above).
|
||||
if uid:
|
||||
@@ -4039,6 +4060,144 @@ async def player_stats_perm_error(interaction, error):
|
||||
await permission_fail(interaction, error)
|
||||
|
||||
|
||||
async def _resolve_player_for_link(username: str) -> list:
|
||||
"""Return [{UID, nick}] rows matching a username (exact, then substring)."""
|
||||
async with aiosqlite.connect(SQ_BATTLES_DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT UID, MIN(nick) AS nick FROM player_games_hist "
|
||||
"WHERE nick = ? COLLATE NOCASE GROUP BY UID ORDER BY nick LIMIT 25",
|
||||
(username,),
|
||||
) as cursor:
|
||||
results = list(await cursor.fetchall())
|
||||
if not results:
|
||||
async with db.execute(
|
||||
"SELECT UID, MIN(nick) AS nick FROM player_games_hist "
|
||||
"WHERE nick LIKE ? COLLATE NOCASE GROUP BY UID ORDER BY nick LIMIT 25",
|
||||
(f"%{username}%",),
|
||||
) as cursor:
|
||||
results = list(await cursor.fetchall())
|
||||
return results
|
||||
|
||||
|
||||
async def _latest_nick_for_uid(uid: str) -> str:
|
||||
"""Best-effort latest nick for a UID; falls back to the UID string."""
|
||||
try:
|
||||
async with aiosqlite.connect(SQ_BATTLES_DB_PATH) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT nick FROM player_games_hist WHERE UID = ? ORDER BY session_id DESC LIMIT 1",
|
||||
(uid,),
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
if row:
|
||||
return row["nick"]
|
||||
except Exception:
|
||||
pass
|
||||
return uid
|
||||
|
||||
|
||||
class SetPlayerSelectView(View):
|
||||
"""Select which matched player to link to the invoking Discord user."""
|
||||
|
||||
def __init__(self, results, author: discord.abc.User, lang: str = "en"):
|
||||
super().__init__(timeout=30)
|
||||
self.author = author
|
||||
self.lang = lang
|
||||
options = [
|
||||
discord.SelectOption(
|
||||
label=row["nick"][:100],
|
||||
description=f"UID: {row['UID']}"[:100],
|
||||
value=str(row["UID"])[:100],
|
||||
)
|
||||
for row in results[:25]
|
||||
]
|
||||
self.select = Select(
|
||||
placeholder=t(lang, "player.select_player_placeholder"),
|
||||
options=options,
|
||||
min_values=1,
|
||||
max_values=1,
|
||||
)
|
||||
self.select.callback = self.select_callback
|
||||
self.add_item(self.select)
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
return interaction.user.id == self.author.id
|
||||
|
||||
async def select_callback(self, interaction: discord.Interaction):
|
||||
uid = self.select.values[0]
|
||||
nick = await _latest_nick_for_uid(uid)
|
||||
save_player_link(interaction.user.id, uid)
|
||||
await interaction.response.edit_message(
|
||||
content=t(self.lang, "player.link_success", nick=esc(nick), uid=uid),
|
||||
view=None,
|
||||
)
|
||||
|
||||
|
||||
@is_blacklisted()
|
||||
@bot.tree.command(
|
||||
name="set-player",
|
||||
description=command_locale("Link your Discord account to a War Thunder player", "commands.set_player.description"),
|
||||
)
|
||||
@app_commands.describe(
|
||||
username=command_locale("The WT username to link", "commands.set_player.username"),
|
||||
uid=command_locale("The WT UID to link", "commands.set_player.uid"),
|
||||
)
|
||||
@discord.app_commands.autocomplete(username=player_autocomplete)
|
||||
async def set_player(interaction: discord.Interaction, username: str = "", uid: str = ""):
|
||||
"""Link the invoking Discord user to a WT account in the shared PLAYERS.json.
|
||||
|
||||
Once linked, commands like /player-stats default to this account when run
|
||||
without an explicit username/uid.
|
||||
"""
|
||||
await collect_command_stats(interaction)
|
||||
lang = await guild_lang(interaction.guild.id) if interaction.guild else 'en'
|
||||
|
||||
if uid:
|
||||
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||
nick = await _latest_nick_for_uid(uid)
|
||||
save_player_link(interaction.user.id, uid)
|
||||
await interaction.followup.send(
|
||||
t(lang, "player.link_success", nick=esc(nick), uid=uid), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
if username:
|
||||
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||
try:
|
||||
results = await _resolve_player_for_link(username)
|
||||
except Exception as e:
|
||||
error_str = str(e)[:1800]
|
||||
await interaction.followup.send(t(lang, "common.database_error", error=error_str), ephemeral=True)
|
||||
return
|
||||
if not results:
|
||||
await interaction.followup.send(
|
||||
t(lang, "player.no_players_found", username=username), ephemeral=True
|
||||
)
|
||||
return
|
||||
if len(results) > 1:
|
||||
await interaction.followup.send(
|
||||
t(lang, "player.link_select"),
|
||||
view=SetPlayerSelectView(results, interaction.user, lang=lang),
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
target_uid = str(results[0]["UID"])
|
||||
save_player_link(interaction.user.id, target_uid)
|
||||
await interaction.followup.send(
|
||||
t(lang, "player.link_success", nick=esc(results[0]["nick"]), uid=target_uid),
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
await interaction.response.send_message(t(lang, "player.must_provide_input"), ephemeral=True)
|
||||
|
||||
|
||||
@set_player.error
|
||||
async def set_player_perm_error(interaction, error):
|
||||
await permission_fail(interaction, error)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# /view-player-games — Last 20 games for a player
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -4112,7 +4271,7 @@ class FindPlayerView(discord.ui.View):
|
||||
)
|
||||
@app_commands.describe(player=command_locale("The player's username", "commands.common.player_username"))
|
||||
@discord.app_commands.autocomplete(player=player_autocomplete)
|
||||
async def view_player_games(interaction: discord.Interaction, player: str):
|
||||
async def view_player_games(interaction: discord.Interaction, player: str = ""):
|
||||
"""Display a player's recent squadron battle sessions with win/loss, comps, and opponents.
|
||||
|
||||
Resolves the player nickname to a UID, queries sessions from the last 8 hours
|
||||
@@ -4127,6 +4286,16 @@ async def view_player_games(interaction: discord.Interaction, player: str):
|
||||
lang = await guild_lang(interaction.guild.id) if interaction.guild else 'en'
|
||||
await interaction.response.defer(ephemeral=False)
|
||||
|
||||
if not player:
|
||||
# Fall back to the caller's linked account, if any.
|
||||
linked_uid = get_linked_uid(interaction.user.id)
|
||||
if not linked_uid:
|
||||
return await interaction.followup.send(
|
||||
t(lang, "player.must_provide_or_link"), ephemeral=True
|
||||
)
|
||||
target_uid = linked_uid
|
||||
player_nick = esc(await _latest_nick_for_uid(linked_uid))
|
||||
else:
|
||||
# Resolve player nick → most-recently-seen UID
|
||||
try:
|
||||
async with aiosqlite.connect(SQ_BATTLES_DB_PATH) as db:
|
||||
|
||||
+9
-1
@@ -244,7 +244,10 @@
|
||||
"not_found_desc": "No game history found for `{player}`.",
|
||||
"no_players_found": "No players found matching **{username}**\nTry using `/website` to search on the website.",
|
||||
"multiple_matches": "Multiple matches found, choose the correct one below:",
|
||||
"must_provide_input": "You must provide at least a UID or username."
|
||||
"must_provide_input": "You must provide at least a UID or username.",
|
||||
"must_provide_or_link": "You must provide a UID or username, or link your account with `/set-player` first.",
|
||||
"link_select": "Multiple players match — select which account to link to your Discord:",
|
||||
"link_success": "✅ Linked your Discord account to **{nick}** (UID `{uid}`).\nCommands like `/player-stats` will now default to this account."
|
||||
},
|
||||
"player_games": {
|
||||
"no_recent_title": "No Recent Games",
|
||||
@@ -759,6 +762,11 @@
|
||||
"description": "Set the squadron tag for this server",
|
||||
"abbreviated_name": "The short name of the squadron to set"
|
||||
},
|
||||
"set_player": {
|
||||
"description": "Link your Discord account to a War Thunder player",
|
||||
"username": "The WT username to link",
|
||||
"uid": "The WT UID to link"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Set up the bot for this server"
|
||||
},
|
||||
|
||||
+22
-27
@@ -34,6 +34,7 @@ from wcwidth import wcswidth
|
||||
# BOT/__init__.py has already put BOTS/SHARED on sys.path; re-export it
|
||||
# under a public name so peer modules can use it for asset paths.
|
||||
from . import SHARED_DIR # noqa: F401 — re-exported for siblings
|
||||
from shared_store import check_user_blacklist
|
||||
from data_parser import (
|
||||
LangTableReader,
|
||||
UnitTags,
|
||||
@@ -129,23 +130,9 @@ def decompress_json(data):
|
||||
BLACKLISTED_SERVER_IDS = [
|
||||
]
|
||||
|
||||
BLACKLISTED_SQUADRONS = [
|
||||
"FIR3",
|
||||
"F4WRD",
|
||||
]
|
||||
|
||||
BLACKLISTED_USER_IDS = [
|
||||
635917136619110411, #wolfhunter4374
|
||||
498231384238850048, #liquidtaeja
|
||||
1166571862789193769, #astro
|
||||
872791644947234847, #nova
|
||||
591290628722393090, #rayan
|
||||
1129994258267512842, #squawk
|
||||
1417443909918916670, #maverickfighter9
|
||||
1034483237197713539, #HK416A6
|
||||
(601497771266277387, "Z supporter + racism"), #lowe/koniglion
|
||||
815535178122002463, #markboss
|
||||
]
|
||||
# Blacklisted users and squadrons now live in the shared, version-controlled
|
||||
# BOTS/SHARED/BLACKLIST.json (read via shared_store) so both bots share one
|
||||
# source of truth. Use check_user_blacklist() / shared_store.blacklisted_squadrons().
|
||||
|
||||
# ── Premium / Entitlements ────────────────────────────────────────────────────
|
||||
PREMIUM_ACTIVATION_TS: int = 1775459700 # Unix timestamp when premium gating activates
|
||||
@@ -540,8 +527,9 @@ async def is_dev_team(interaction: discord.Interaction) -> bool:
|
||||
def is_blacklisted():
|
||||
"""Return an app-command check that rejects blacklisted users or guilds.
|
||||
|
||||
Entries in BLACKLISTED_USER_IDS may be plain ints or
|
||||
``(user_id, reason)`` tuples.
|
||||
Blacklisted users come from the shared BLACKLIST.json (see
|
||||
``shared_store.check_user_blacklist``); entries there may be a plain id or
|
||||
``[id, reason]``.
|
||||
|
||||
Raises:
|
||||
BlacklistCheckFailure: If the guild or user is blacklisted,
|
||||
@@ -552,13 +540,8 @@ def is_blacklisted():
|
||||
if guild is not None and guild.id in BLACKLISTED_SERVER_IDS:
|
||||
raise BlacklistCheckFailure(t("en", "common.access_denied_desc"))
|
||||
|
||||
uid = interaction.user.id
|
||||
for entry in BLACKLISTED_USER_IDS:
|
||||
if isinstance(entry, tuple):
|
||||
blocked_id, reason = entry
|
||||
else:
|
||||
blocked_id, reason = entry, None
|
||||
if uid == blocked_id:
|
||||
blocked, reason = check_user_blacklist(interaction.user.id)
|
||||
if blocked:
|
||||
raise BlacklistCheckFailure(reason)
|
||||
return True
|
||||
return app_commands.check(predicate)
|
||||
@@ -778,9 +761,21 @@ def is_notif_enabled(entry: Any, notif_type: str) -> bool:
|
||||
return bool(re.search(r"\d{17,19}", raw))
|
||||
|
||||
|
||||
def is_foreign_pref_entry(entry: Any) -> bool:
|
||||
"""True if a preferences entry belongs to another bot (TSSBOT) and SRE should skip it.
|
||||
|
||||
Both bots share ``STORAGE/PREFERENCES/<guild>-preferences.json``. TSSBOT entries
|
||||
carry a ``Type`` of ``tss-team``/``tss-player``; SRE entries have no such Type.
|
||||
"""
|
||||
return isinstance(entry, dict) and str(entry.get("Type", "")).lower().startswith("tss")
|
||||
|
||||
|
||||
def enabled_pref_keys_for(prefs: Dict[str, Any], notif_type: str) -> List[str]:
|
||||
"""Squadron keys (in JSON insertion order) whose entry has this notif enabled."""
|
||||
return [k for k, v in prefs.items() if is_notif_enabled(v, notif_type)]
|
||||
return [
|
||||
k for k, v in prefs.items()
|
||||
if not is_foreign_pref_entry(v) and is_notif_enabled(v, notif_type)
|
||||
]
|
||||
|
||||
|
||||
def allowed_pref_keys_for(prefs: Dict[str, Any], tier: Optional[str], notif_type: str) -> set[str]:
|
||||
|
||||
Reference in New Issue
Block a user