From e847807f9413df75349502e26b5afc08fca10a1a Mon Sep 17 00:00:00 2001 From: Chrostyy Date: Fri, 17 Jul 2026 00:58:28 +0530 Subject: [PATCH 1/2] Fix get_foreground_host_activity call for API 9 Replace deprecated _babase.get_foreground_host_activity() with bs.get_foreground_host_activity() to be compatible with BombSquad API 9. _babase private module calls died in API 9. --- .../mods/chathandle/chatcommands/commands/management.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dist/ba_root/mods/chathandle/chatcommands/commands/management.py b/dist/ba_root/mods/chathandle/chatcommands/commands/management.py index 28161d4..d125a5d 100644 --- a/dist/ba_root/mods/chathandle/chatcommands/commands/management.py +++ b/dist/ba_root/mods/chathandle/chatcommands/commands/management.py @@ -368,7 +368,7 @@ def remove(arguments): def slow_motion(): - activity = _babase.get_foreground_host_activity() + activity = bs.get_foreground_host_activity() if not activity.globalsnode.slow_motion: activity.globalsnode.slow_motion = True @@ -414,7 +414,7 @@ def tint(arguments): def pause(): - activity = _babase.get_foreground_host_activity() + activity = bs.get_foreground_host_activity() if not activity.globalsnode.paused: activity.globalsnode.paused = True @@ -424,7 +424,7 @@ def pause(): def rotate_camera(): - activity = _babase.get_foreground_host_activity() + activity = bs.get_foreground_host_activity() if activity.globalsnode.camera_mode != 'rotate': activity.globalsnode.camera_mode = 'rotate' From a2d7d72d8a57ad8aeef420d8ea458821bcb3f970 Mon Sep 17 00:00:00 2001 From: Ayush Saini <36878972+imayushsaini@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:22:27 +0530 Subject: [PATCH 2/2] server check cleanup --- .../chatcommands/commands/management.py | 61 ++- dist/ba_root/mods/custom_hooks.py | 42 ++ dist/ba_root/mods/playersdata/pdata.py | 36 -- dist/ba_root/mods/tools/servercheck.py | 463 +++++++++++------- 4 files changed, 377 insertions(+), 225 deletions(-) diff --git a/dist/ba_root/mods/chathandle/chatcommands/commands/management.py b/dist/ba_root/mods/chathandle/chatcommands/commands/management.py index 119de33..d1d7bb6 100644 --- a/dist/ba_root/mods/chathandle/chatcommands/commands/management.py +++ b/dist/ba_root/mods/chathandle/chatcommands/commands/management.py @@ -24,7 +24,8 @@ def unban(arguments: list[str], clientid: int, accountid: str) -> None: for account in serverdata.recents: if account['client_id'] == target_cl_id: pdata.unban_player(account["pbid"]) - logger.log(f'unbanned {account["pbid"]} by chat command, recents') + logger.log( + f'unbanned {account["pbid"]} by chat command, recents') except (ValueError, TypeError): pass @@ -33,7 +34,8 @@ def unban(arguments: list[str], clientid: int, accountid: str) -> None: def recents(arguments: list[str], clientid: int, accountid: str) -> None: """List recent players.""" for players in serverdata.recents: - send(f"{players['client_id']} {players['deviceId']} {players['pbid']}", clientid) + send( + f"{players['client_id']} {players['deviceId']} {players['pbid']}", clientid) @registry.register(['info'], category='Manage') @@ -75,7 +77,8 @@ def createteam(arguments: list[str], clientid: int, accountid: str) -> None: session.sessionteams.append(SessionTeam( team_id=len(session.sessionteams) + 1, name=str(arguments[0]), - color=(random.uniform(0, 1.2), random.uniform(0, 1.2), random.uniform(0, 1.2)) + color=(random.uniform(0, 1.2), random.uniform( + 0, 1.2), random.uniform(0, 1.2)) )) from bascenev1._lobby import Lobby session.lobby = Lobby() @@ -121,17 +124,19 @@ def ban(arguments: list[str], clientid: int, accountid: str) -> None: try: cl_id = int(arguments[0]) duration = float(arguments[1]) if len(arguments) >= 2 else 0.5 - + for ros in bs.get_game_roster(): if ros["client_id"] == cl_id: - pdata.ban_player(ros['account_id'], duration, "by chat command") + pdata.ban_player(ros['account_id'], + duration, "by chat command") logger.log(f'banned {ros["display_string"]} by chat command') for account in serverdata.recents: if account['client_id'] == cl_id: pdata.ban_player(account["pbid"], duration, "by chat command") - logger.log(f'banned {account["pbid"]} by chat command, recents') - + logger.log( + f'banned {account["pbid"]} by chat command, recents') + kick_player(cl_id) except (ValueError, TypeError): pass @@ -157,23 +162,25 @@ def kickvote(arguments: list[str], clientid: int, accountid: str) -> None: return action = arguments[0] target = arguments[1] - + if action == 'enable': if target == 'all': - _babase.set_enable_default_kick_voting(True) + _bascenev1.set_enable_default_kick_voting(True) else: try: cl_id = int(target) for ros in bs.get_game_roster(): if ros["client_id"] == cl_id: pdata.enable_kick_vote(ros["account_id"]) - logger.log(f'kick vote enabled for {ros["account_id"]} {ros["display_string"]}') - send("Upon server restart, Kick-vote will be enabled for this person", clientid) + logger.log( + f'kick vote enabled for {ros["account_id"]} {ros["display_string"]}') + send( + "Upon server restart, Kick-vote will be enabled for this person", clientid) except (ValueError, TypeError): pass elif action == 'disable': if target == 'all': - _babase.set_enable_default_kick_voting(False) + _bascenev1.set_enable_default_kick_voting(False) else: try: cl_id = int(target) @@ -181,8 +188,10 @@ def kickvote(arguments: list[str], clientid: int, accountid: str) -> None: if ros["client_id"] == cl_id: _bascenev1.disable_kickvote(ros["account_id"]) send("Kick-vote disabled for this person", clientid) - logger.log(f'kick vote disabled for {ros["account_id"]} {ros["display_string"]}') - pdata.disable_kick_vote(ros["account_id"], 2, "by chat command") + logger.log( + f'kick vote disabled for {ros["account_id"]} {ros["display_string"]}') + pdata.disable_kick_vote( + ros["account_id"], 2, "by chat command") except (ValueError, TypeError): pass @@ -190,13 +199,13 @@ def kickvote(arguments: list[str], clientid: int, accountid: str) -> None: @registry.register(['hideid'], category='Manage') def hideid(arguments: list[str], clientid: int, accountid: str) -> None: """Hide player device ID.""" - _babase.hide_player_device_id(True) + _bascenev1.hide_player_device_id(True) @registry.register(['showid'], category='Manage') def showid(arguments: list[str], clientid: int, accountid: str) -> None: """Show player device ID.""" - _babase.hide_player_device_id(False) + _bascenev1.hide_player_device_id(False) @registry.register(['lm'], category='Manage') @@ -213,7 +222,8 @@ def gp(arguments: list[str], clientid: int, accountid: str) -> None: player_id = int(arguments[0]) session = bs.get_foreground_host_session() if session and 0 <= player_id < len(session.sessionplayers): - profiles = session.sessionplayers[player_id].inputdevice.get_player_profiles() + profiles = session.sessionplayers[player_id].inputdevice.get_player_profiles( + ) for num, profile in enumerate(profiles, 1): send(f"{num})- {profile}", clientid) except (ValueError, TypeError, IndexError): @@ -252,11 +262,13 @@ def mute(arguments: list[str], clientid: int, accountid: str) -> None: for ros in bs.get_game_roster(): if ros["client_id"] == cl_id: logger.log(f'muted {ros["display_string"]}') - pdata.mute(ros['account_id'], duration, "muted by chat command") + pdata.mute(ros['account_id'], duration, + "muted by chat command") return for account in serverdata.recents: if account['client_id'] == cl_id: - pdata.mute(account["pbid"], duration, "muted by chat command, from recents") + pdata.mute(account["pbid"], duration, + "muted by chat command, from recents") except (ValueError, TypeError): pass @@ -277,7 +289,8 @@ def unmute(arguments: list[str], clientid: int, accountid: str) -> None: for account in serverdata.recents: if account['client_id'] == cl_id: pdata.unmute(account["pbid"]) - logger.log(f'unmuted {account["pbid"]} by chat command, recents') + logger.log( + f'unmuted {account["pbid"]} by chat command, recents') except (ValueError, TypeError): pass @@ -321,7 +334,7 @@ def nv(arguments: list[str], clientid: int, accountid: str) -> None: if activity and activity.globalsnode: nv_tint = (0.5, 0.5, 1.0) nv_ambient = (1.5, 1.5, 1.5) - + if is_close(activity.globalsnode.tint, nv_tint): activity.globalsnode.tint = (1.0, 1.0, 1.0) activity.globalsnode.ambient_color = (1.0, 1.0, 1.0) @@ -415,7 +428,8 @@ def removerole(arguments: list[str], clientid: int, accountid: str) -> None: if session: for player in session.sessionplayers: if player.inputdevice.client_id == target_cl_id: - pdata.remove_player_role(role, player.get_v1_account_id()) + pdata.remove_player_role( + role, player.get_v1_account_id()) except (ValueError, TypeError): pass @@ -430,7 +444,8 @@ def getroles(arguments: list[str], clientid: int, accountid: str) -> None: if session: for player in session.sessionplayers: if player.inputdevice.client_id == target_cl_id: - roles = pdata.get_player_roles(player.get_v1_account_id()) + roles = pdata.get_player_roles( + player.get_v1_account_id()) reply = ",".join(roles) send(reply, clientid) break diff --git a/dist/ba_root/mods/custom_hooks.py b/dist/ba_root/mods/custom_hooks.py index 1629da0..27c33a7 100644 --- a/dist/ba_root/mods/custom_hooks.py +++ b/dist/ba_root/mods/custom_hooks.py @@ -27,6 +27,7 @@ from bascenev1._session import Session from bascenev1._map import Map from bascenev1._activitytypes import ScoreScreenActivity from baclassic._servermode import ServerController +from efro.terminal import Clr import setting import bauiv1 as bui from baclassic._appmode import ClassicAppMode @@ -178,6 +179,46 @@ def playerspaz_init(playerspaz: bs.Player, node: bs.Node, player: bs.Player): modifyspaz.main(playerspaz, node, player) +def verify_account_token() -> None: + """Verifies the account API token on server start.""" + import urllib.request + import urllib.error + import json + token = settings.get("accountApiToken") + warning_msg = ( + "invalid token found , update settings.json with api token " + "else server functionaly will break." + ) + if not token: + logging.warning(warning_msg) + print(f'{Clr.BRED}{warning_msg}{Clr.RST}', flush=True) + return + + try: + url = "https://www.ballistica.net/api/v1/accounts/me" + req = urllib.request.Request( + url, + headers={ + "Authorization": f"Bearer {token}" + } + ) + with urllib.request.urlopen(req, timeout=10) as response: + status = response.getcode() + if status == 200: + data = json.loads(response.read().decode('utf-8')) + tag = data.get("tag", "unknown") + print( + f"{Clr.BGRN}api token verified sucfessfuly using {tag} token.{Clr.RST}", flush=True) + logging.info( + f"api token verified sucfessfuly using {tag} token.") + else: + logging.warning(warning_msg) + print(f'{Clr.BRED}{warning_msg}{Clr.RST}', flush=True) + except Exception: + logging.warning(warning_msg) + print(f'{Clr.BRED}{warning_msg}{Clr.RST}', flush=True) + + def bootstraping(): """Bootstarps the server.""" logging.warning("Bootstraping mods...") @@ -185,6 +226,7 @@ def bootstraping(): # check for auto update stats _thread.start_new_thread(mystats.refreshStats, ()) + _thread.start_new_thread(verify_account_token, ()) pdata.load_cache() _thread.start_new_thread(pdata.dump_cache, ()) _thread.start_new_thread(notification_manager.dump_cache, ()) diff --git a/dist/ba_root/mods/playersdata/pdata.py b/dist/ba_root/mods/playersdata/pdata.py index bfdbe05..fdaec24 100644 --- a/dist/ba_root/mods/playersdata/pdata.py +++ b/dist/ba_root/mods/playersdata/pdata.py @@ -203,42 +203,6 @@ def add_profile( "lastSpam": time.time(), "totaltimeplayer": 0, } - - serverdata.clients[account_id] = profiles[account_id] - serverdata.clients[account_id].update({ - "warnCount": 0, - "lastWarned": time.time(), - "verified": False, - "rejoincount": 1, - "lastJoin": time.time() - }) - - cid = -1 - for ros in bs.get_game_roster(): - if ros.get('account_id') == account_id: - cid = ros.get('client_id', -1) - break - - if cid != -1: - ip = _bascenev1.get_client_ip(cid) - serverdata.clients[account_id]["lastIP"] = ip - device_id = _bascenev1.get_client_public_device_uuid( - cid) or _bascenev1.get_client_device_uuid(cid) - serverdata.clients[account_id]["deviceUUID"] = device_id - - serverdata.recents.append( - {"client_id": cid, "deviceId": display_string, - "pbid": account_id, "ip": ip, "device_uuid": device_id} - ) - serverdata.recents = serverdata.recents[-20:] - - checkSpammer({'id': account_id, 'display': display_string, - 'ip': ip, 'device': device_id}) - - blacklist = get_blacklist() - if device_id in blacklist["ban"]["deviceids"] or account_id in blacklist["ban"]["ids"]: - bs.disconnect_client(cid) - CacheData.profiles = profiles diff --git a/dist/ba_root/mods/tools/servercheck.py b/dist/ba_root/mods/tools/servercheck.py index b811915..056dc7e 100644 --- a/dist/ba_root/mods/tools/servercheck.py +++ b/dist/ba_root/mods/tools/servercheck.py @@ -1,6 +1,6 @@ # Released under the MIT License. See LICENSE for details. from __future__ import annotations -from bacommon.restapi.v1.accounts import AccountResponse + import _thread import json import threading @@ -8,13 +8,17 @@ import time import urllib.request from dataclasses import dataclass, field from datetime import datetime, timedelta -from typing import Any, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional + from efro.dataclassio import dataclass_from_json +from bacommon.restapi.v1.accounts import AccountResponse + import _babase import _bascenev1 import babase import bascenev1 as bs from babase._general import Call + from features import profanity from playersdata import pdata from repository import profiles @@ -55,7 +59,7 @@ class IPJoin: class ServerCheck: """A class to check for new players and handle their joining process.""" - def __init__(self): + def __init__(self) -> None: self.players: List[str] = [] self.ip_client_map: Dict[str, List[int]] = {} self.device_client_map: Dict[str, List[int]] = {} @@ -66,7 +70,7 @@ class ServerCheck: """ Checks for new players, handles their joining process, and performs various checks. """ - new_players = [] + new_players: List[str] = [] self.ip_client_map.clear() self.device_client_map.clear() @@ -89,7 +93,7 @@ class ServerCheck: self.players = new_players - def _get_device_id(self, client_id: int) -> Optional[str]: + def _get_device_id(self, client_id: int) -> str: """ Returns the public or private device UUID for a given client_id. """ @@ -127,12 +131,9 @@ class ServerCheck: def _disconnect_player(self, client_id: int, message: str, log_message: str) -> None: """ - Disconnects a player with a given message and logs the disconnection. """ - bs.chatmessage(message, clients=[client_id]) - bs.disconnect_client(client_id) - logger.log(log_message, "playerjoin") + kick_client(client_id, message, log_message, use_chat=True, chat_func=bs.chatmessage) def _handle_new_player(self, ros: Dict[str, Any], ip: str, device_id: str) -> None: """ @@ -159,73 +160,75 @@ class ServerCheck: ) return - logger.log( - f'{display_string} || {account_id} || joined server', "playerjoin") + logger.log(f'{display_string} || {account_id} || joined server', "playerjoin") logger.log(f'{account_id} {ip} {device_id}') - if account_id in serverdata.clients: - on_player_join_server( - account_id, serverdata.clients[account_id], ip, device_id) - else: - LoadProfile(account_id, ip, device_id).start() + on_player_join_server(account_id, ip, device_id) -def on_player_join_server(pbid: str, player_data: Optional[Dict[str, Any]], ip: str, device_id: str) -> None: - """ - Handles the joining process for a player on the server. - """ - global ipjoin - now = time.time() - client_id = -1 - display_string = "" - +def get_roster_by_pb_id(pb_id: str) -> Optional[Dict[str, Any]]: + """Returns the game roster entry for the given player ID if currently connected.""" for ros in bs.get_game_roster(): - if ros["account_id"] == pbid: - client_id = ros["client_id"] - display_string = ros['display_string'] - break + if ros.get("account_id") == pb_id: + return ros + return None - if client_id == -1: - return - if ip in ipjoin: - ip_info = ipjoin[ip] - last_join = ip_info.last_join - join_count = ip_info.count - if now - last_join < 15: - join_count += 1 - if join_count > 2: - bs.broadcastmessage( - "Joining too fast, slow down dude", - color=(1, 0, 1), - transient=True, - clients=[client_id], - ) - logger.log(f'{pbid} || kicked for joining too fast') - bs.disconnect_client(client_id) - _thread.start_new_thread(report_spam, (pbid,)) - return +def kick_client( + client_id: int, + message: str, + log_message: Optional[str] = None, + log_category: Optional[str] = None, + use_chat: bool = False, + chat_func: Optional[Callable] = None, + color: Optional[tuple] = None, +) -> None: + """Disconnects a client with a message, log, and optional display parameters.""" + if use_chat: + fn = chat_func or bs.chatmessage + fn(message, clients=[client_id]) + else: + if color is not None: + bs.broadcastmessage(message, color=color, transient=True, clients=[client_id]) else: - join_count = 0 - ip_info.count = join_count - ip_info.last_join = now - else: - ipjoin[ip] = IPJoin(last_join=now, count=0) + bs.broadcastmessage(message, transient=True, clients=[client_id]) - if pbid in serverdata.clients: - serverdata.clients[pbid]["lastJoin"] = now + bs.disconnect_client(client_id) - if player_data is not None: - handle_existing_player(pbid, player_data, ip, - device_id, client_id, display_string) - else: - handle_new_player_data(pbid, display_string, client_id) + if log_message: + if log_category: + logger.log(log_message, log_category) + else: + logger.log(log_message) -def handle_existing_player(pbid: str, player_data: Dict[str, Any], ip: str, device_id: str, client_id: int, display_string: str) -> None: - """ - Handles the joining process for an existing player. - """ +def kick_by_pb_id(pb_id: str, msg: str) -> None: + """Kicks a player by their account/PB ID.""" + ros = get_roster_by_pb_id(pb_id) + if ros is not None and ros.get("client_id", -1) != -1: + kick_client(ros["client_id"], msg) + + +def notify_web_player_joined(pb_id: str) -> None: + """Helper to notify the web service about a player joining if enabled.""" + if settings.get("ballistica_web", {}).get("enable"): + from . import notification_manager + notification_manager.player_joined(pb_id) + + +def _fetch_and_verify_device_accounts(pb_id: str, display_string: str) -> None: + """Helper to fetch device accounts and start verification callback.""" + thread = FetchThread( + target=get_device_accounts, + callback=save_ids, + pb_id=pb_id, + display_string=display_string, + ) + thread.start() + + +def _add_to_recents(client_id: int, display_string: str, pbid: str, ip: str, device_id: str) -> None: + """Helper to record the recently joined player.""" serverdata.recents.append( { "client_id": client_id, @@ -237,23 +240,9 @@ def handle_existing_player(pbid: str, player_data: Dict[str, Any], ip: str, devi ) serverdata.recents = serverdata.recents[-20:] - if check_ban(ip, device_id, pbid): - _bascenev1.chatmessage( - 'sad, your account is flagged contact server owner for unban', clients=[client_id]) - bs.disconnect_client(client_id) - return - - if get_account_age(player_data["creationDate"]) < settings["minAgeToJoinInHours"]: - bs.broadcastmessage( - "New Accounts not allowed here, come back later", - color=(1, 0, 0), - transient=True, - clients=[client_id], - ) - logger.log(pbid + " | kicked > reason:Banned account") - bs.disconnect_client(client_id) - return +def _setup_client_data_structures(pbid: str, player_data: Dict[str, Any], ip: str, device_id: str) -> None: + """Helper to initialize server cache metadata for the player.""" current_time = datetime.now() if pbid not in serverdata.clients: serverdata.clients[pbid] = player_data @@ -262,48 +251,183 @@ def handle_existing_player(pbid: str, player_data: Dict[str, Any], ip: str, devi serverdata.clients[pbid]["verified"] = False serverdata.clients[pbid]["rejoincount"] = 1 serverdata.clients[pbid]["lastJoin"] = time.time() - if pbid in blacklist["kick-vote-disabled"] and current_time < datetime.strptime( - blacklist["kick-vote-disabled"][pbid]["till"], "%Y-%m-%d %H:%M:%S" - ): - _bascenev1.disable_kickvote(pbid) + + # Check kick-vote disabled from blacklist + kv_blacklist = blacklist.get("kick-vote-disabled", {}) + if pbid in kv_blacklist: + try: + till_time = datetime.strptime(kv_blacklist[pbid]["till"], "%Y-%m-%d %H:%M:%S") + if current_time < till_time: + _bascenev1.disable_kickvote(pbid) + except (ValueError, TypeError): + pass serverdata.clients[pbid]["lastIP"] = ip serverdata.clients[pbid]["deviceUUID"] = device_id - verify_account(pbid, player_data) - logger.log( - f'{pbid} ip: {serverdata.clients[pbid]["lastIP"]}, Device id: {device_id}') - bs.broadcastmessage( - settings["regularWelcomeMsg"] + " " + display_string, - color=(0.6, 0.8, 0.6), - transient=True, - clients=[client_id], - ) - if settings["ballistica_web"]["enable"]: - from . import notification_manager - notification_manager.player_joined(pbid) -def handle_new_player_data(pbid: str, display_string: str, client_id: int) -> None: +def _check_ip_join_rate_limit(ip: str, pbid: str, client_id: int) -> bool: + """Checks and updates IP join rate limits. Returns True if allowed, False if kicked.""" + global ipjoin + now = time.time() + + if ip in ipjoin: + ip_info = ipjoin[ip] + if now - ip_info.last_join < 15: + ip_info.count += 1 + if ip_info.count > 2: + kick_client( + client_id, + "Joining too fast, slow down dude", + log_message=f"{pbid} || kicked for joining too fast", + color=(1, 0, 1), + ) + _thread.start_new_thread(report_spam, (pbid,)) + return False + else: + ip_info.count = 0 + ip_info.last_join = now + else: + ipjoin[ip] = IPJoin(last_join=now, count=0) + + return True + + +def on_player_join_server(pbid: str, ip: str, device_id: str) -> None: """ - Handles the joining process for a player with no existing data. + Stage 1: Ingress / Initial Checks. + This is called when a player connects. """ + ros = get_roster_by_pb_id(pbid) + if ros is None or ros.get("client_id", -1) == -1: + return - thread = FetchThread( - target=my_acc_age, - callback=save_age, - pb_id=pbid, - display_string=display_string, - ) - thread.start() + client_id = ros["client_id"] + display_string = ros["display_string"] + + # 1. IP rate limiting check + if not _check_ip_join_rate_limit(ip, pbid, client_id): + return + + # 2. Ban checking (applies to all players, new and existing) + if check_ban(ip, device_id, pbid): + kick_client( + client_id, + "sad, your account is flagged contact server owner for unban", + use_chat=True, + chat_func=_bascenev1.chatmessage, + ) + return + + # 3. Transition to database loading or verification + if pbid in serverdata.clients: + # Fast path: Already cached in memory, proceed directly to Stage 3 + _setup_and_verify_player(pbid, serverdata.clients[pbid], ip, device_id, client_id, display_string, is_new=False) + else: + # Slow path: Not cached, load profile from database in background + LoadProfile(pbid, ip, device_id, client_id, display_string).start() + + +def _on_profile_loaded(pbid: str, player_data: Optional[Dict[str, Any]], ip: str, device_id: str, client_id: int, display_string: str) -> None: + """ + Stage 2: Database Load Result. + Processes the loaded profile, routing to creation/API fetch if missing. + """ + ros = get_roster_by_pb_id(pbid) + if ros is None or ros.get("client_id", -1) == -1: + return + + if player_data is not None: + # Profile exists in database, proceed directly to Stage 3 + _setup_and_verify_player(pbid, player_data, ip, device_id, client_id, display_string, is_new=False) + else: + # Profile doesn't exist, proceed to Stage 2b (Account creation date fetch) + _fetch_and_register_new_player(pbid, ip, device_id, client_id, display_string) + + +def _fetch_and_register_new_player(pbid: str, ip: str, device_id: str, client_id: int, display_string: str) -> None: + """ + Stage 2b: API creation date fetch & database registration. + """ + # Print first time welcome message bs.broadcastmessage( settings["firstTimeJoinMsg"], color=(0.6, 0.8, 0.6), transient=True, clients=[client_id], ) - if settings["ballistica_web"]["enable"]: - from . import notification_manager - notification_manager.player_joined(pbid) + notify_web_player_joined(pbid) + + # Start API call to fetch registration date + def api_callback(account_creation_date: Optional[str], pb_id: str, display_name: str) -> None: + if not account_creation_date: + return + + # Register new profile in database + register_new_profile(pb_id, display_name, account_creation_date) + + # Load the newly created profile data + new_player_data = pdata.get_info(pb_id) + if new_player_data is not None: + # Profile successfully registered, proceed to Stage 3 + _setup_and_verify_player(pb_id, new_player_data, ip, device_id, client_id, display_name, is_new=True) + + thread = FetchThread( + target=get_account_creation_date, + callback=api_callback, + pb_id=pbid, + display_string=display_string, + ) + thread.start() + + +def _setup_and_verify_player(pbid: str, player_data: Dict[str, Any], ip: str, device_id: str, client_id: int, display_string: str, is_new: bool = False) -> None: + """ + Stage 3: Verification, Age Checks, Caching & Welcoming. + Runs for all players (cached, uncached existing, and brand-new after creation). + """ + # 1. Update recents & server client caching + _add_to_recents(client_id, display_string, pbid, ip, device_id) + _setup_client_data_structures(pbid, player_data, ip, device_id) + + # 2. Enforce the minimum account age requirement + if get_account_age(player_data.get("creationDate", "")) < settings["minAgeToJoinInHours"]: + kick_client( + client_id, + "New Accounts not allowed here, come back later", + log_message=f"{pbid} | kicked > reason:Banned account", + color=(1, 0, 0), + ) + return + + # 3. Verify display name authenticity + verify_account(pbid, player_data) + + logger.log(f'{pbid} ip: {ip}, Device id: {device_id}') + + # 4. Display welcome message for returning/existing players + if not is_new: + bs.broadcastmessage( + settings["regularWelcomeMsg"] + " " + display_string, + color=(0.6, 0.8, 0.6), + transient=True, + clients=[client_id], + ) + notify_web_player_joined(pbid) + + +# Keep compatibility aliases for legacy external calls (if any exist) +def handle_existing_player(pbid: str, player_data: Dict[str, Any], ip: str, device_id: str, client_id: int, display_string: str) -> None: + _setup_and_verify_player(pbid, player_data, ip, device_id, client_id, display_string, is_new=False) + + +def handle_new_player_data(pbid: str, display_string: str, client_id: int) -> None: + # Retrieve connection info to pass along + ros = get_roster_by_pb_id(pbid) + if ros is not None: + ip = _bascenev1.get_client_ip(client_id) + device_id = _bascenev1.get_client_public_device_uuid(client_id) or _bascenev1.get_client_device_uuid(client_id) + _fetch_and_register_new_player(pbid, ip, device_id, client_id, display_string) def check_ban(ip: str, device_id: str, pbid: str, log: bool = True) -> bool | str: @@ -313,10 +437,13 @@ def check_ban(ip: str, device_id: str, pbid: str, log: bool = True) -> bool | st current_time = datetime.now() def check_ban_list(ban_list: Dict[str, Any], key: str, ban_type: str) -> Optional[str]: - if key in ban_list and current_time < datetime.strptime( - ban_list[key]["till"], "%Y-%m-%d %H:%M:%S" - ): - return f'reason: matched {ban_type} | {ban_list[key]["reason"]}, Till: {ban_list[key]["till"]}' + if key in ban_list: + try: + till_time = datetime.strptime(ban_list[key]["till"], "%Y-%m-%d %H:%M:%S") + if current_time < till_time: + return f'reason: matched {ban_type} | {ban_list[key]["reason"]}, Till: {ban_list[key]["till"]}' + except (ValueError, TypeError): + pass return None ban_msg = check_ban_list(blacklist["ban"]["ips"], ip, "IP") @@ -339,25 +466,20 @@ def verify_account(pb_id: str, p_data: Dict[str, Any]) -> None: Verifies a player's account by checking their display string against their device accounts. """ if _bascenev1.protocol_version() > 35: - serverdata.clients[pb_id]["verified"] = True + if pb_id in serverdata.clients: + serverdata.clients[pb_id]["verified"] = True return - display_string = "" - for ros in bs.get_game_roster(): - if ros['account_id'] == pb_id: - display_string = ros['display_string'] - break + ros = get_roster_by_pb_id(pb_id) + if ros is None: + return + display_string = ros["display_string"] if display_string not in p_data.get('display_string', []): - thread2 = FetchThread( - target=get_device_accounts, - callback=save_ids, - pb_id=pb_id, - display_string=display_string, - ) - thread2.start() + _fetch_and_verify_device_accounts(pb_id, display_string) else: - serverdata.clients[pb_id]["verified"] = True + if pb_id in serverdata.clients: + serverdata.clients[pb_id]["verified"] = True def _make_request_safe(request: Callable, retries: int = 2, raise_err: bool = True) -> Any: @@ -379,7 +501,6 @@ def get_account_creation_date(pb_id: str) -> Optional[str]: Gets the account creation date for a given player ID. """ if _bascenev1.protocol_version() > 35: - try: req = urllib.request.Request( f"https://www.ballistica.net/api/v1/accounts/{pb_id}", @@ -434,17 +555,18 @@ class LoadProfile(threading.Thread): A thread to load a player's profile from pdata. """ - def __init__(self, pb_id: str, ip: str, device_id: str): + def __init__(self, pb_id: str, ip: str, device_id: str, client_id: int, display_string: str) -> None: super().__init__() self.pbid = pb_id self.ip = ip self.device_id = device_id + self.client_id = client_id + self.display_string = display_string def run(self) -> None: player_data = pdata.get_info(self.pbid) _babase.pushcall( - Call(on_player_join_server, self.pbid, - player_data, self.ip, self.device_id), + Call(_on_profile_loaded, self.pbid, player_data, self.ip, self.device_id, self.client_id, self.display_string), from_other_thread=True, ) @@ -454,7 +576,7 @@ class FetchThread(threading.Thread): A thread to fetch data from a URL and execute a callback with the result. """ - def __init__(self, target: Callable, callback: Optional[Callable] = None, pb_id: str = "ji", display_string: str = "XXX"): + def __init__(self, target: Callable, callback: Optional[Callable] = None, pb_id: str = "ji", display_string: str = "XXX") -> None: super().__init__(target=self.target_with_callback, args=(pb_id, display_string)) self.callback = callback self.method = target @@ -472,42 +594,51 @@ def my_acc_age(pb_id: str) -> Optional[str]: return get_account_creation_date(pb_id) -def save_age(age: Optional[str], pb_id: str, display_string: str) -> None: - if age: - pdata.add_profile(pb_id, display_string, display_string, age) - if _bascenev1.protocol_version() <= 35: - time.sleep(2) - thread2 = FetchThread( - target=get_device_accounts, - callback=save_ids, - pb_id=pb_id, - display_string=display_string, - ) - thread2.start() - if get_account_age(age) < settings["minAgeToJoinInHours"]: - msg = "New Accounts not allowed to play here, come back tmrw." - logger.log(f"{pb_id} || kicked > new account") - kick_by_pb_id(pb_id, msg) +def register_new_profile(pb_id: str, display_string: str, creation_date: str) -> None: + """Registers a new profile in the database/player data cache.""" + pdata.add_profile(pb_id, display_string, display_string, creation_date) + + +def check_and_enforce_account_age(pb_id: str, creation_date: str) -> None: + """Kicks the player if their account age is below the configured threshold.""" + if get_account_age(creation_date) < settings["minAgeToJoinInHours"]: + msg = "New Accounts not allowed to play here, come back tmrw." + logger.log(f"{pb_id} || kicked > new account") + kick_by_pb_id(pb_id, msg) + + +def on_account_creation_date_fetched(account_creation_date: Optional[str], pb_id: str, display_string: str) -> None: + """Legacy callback for account creation date.""" + if not account_creation_date: + return + ros = get_roster_by_pb_id(pb_id) + if ros is not None: + ip = _bascenev1.get_client_ip(ros["client_id"]) + device_id = _bascenev1.get_client_public_device_uuid(ros["client_id"]) or _bascenev1.get_client_device_uuid(ros["client_id"]) + + register_new_profile(pb_id, display_string, account_creation_date) + new_player_data = pdata.get_info(pb_id) + if new_player_data is not None: + _setup_and_verify_player(pb_id, new_player_data, ip, device_id, ros["client_id"], display_string, is_new=True) + + +def save_age(account_creation_date: Optional[str], pb_id: str, display_string: str) -> None: + """Compatibility alias for on_account_creation_date_fetched.""" + on_account_creation_date_fetched(account_creation_date, pb_id, display_string) def save_ids(ids: List[str], pb_id: str, display_string: str) -> None: pdata.update_display_string(pb_id, ids) - if display_string not in ids: + + is_spoofed = display_string not in ids + + if pb_id in serverdata.clients: + serverdata.clients[pb_id]["verified"] = not is_spoofed + + if is_spoofed: msg = "Spoofed Id detected, Goodbye" kick_by_pb_id(pb_id, msg) - serverdata.clients[pb_id]["verified"] = False logger.log(f"{pb_id} || kicked, for using spoofed id {display_string}") - else: - serverdata.clients[pb_id]["verified"] = True - - -def kick_by_pb_id(pb_id: str, msg: str) -> None: - for ros in bs.get_game_roster(): - if ros['account_id'] == pb_id: - bs.broadcastmessage(msg, transient=True, - clients=[ros['client_id']]) - bs.disconnect_client(ros['client_id']) - break def get_account_age(ct: str) -> float: @@ -520,10 +651,10 @@ def get_account_age(ct: str) -> float: def report_spam(pbid: str) -> None: now = time.time() - profiles = pdata.get_profiles() - if pbid in profiles: - spam_count = profiles[pbid].get("spamCount", 0) - last_spam = profiles[pbid].get("lastSpam", 0) + profiles_dict = pdata.get_profiles() + if pbid in profiles_dict: + spam_count = profiles_dict[pbid].get("spamCount", 0) + last_spam = profiles_dict[pbid].get("lastSpam", 0) if now - last_spam < 2 * 24 * 3600: spam_count += 1 if spam_count > 3: @@ -531,8 +662,8 @@ def report_spam(pbid: str) -> None: pdata.ban_player(pbid, 1, "auto ban exceed warn count") else: spam_count = 0 - profiles[pbid]["spamCount"] = spam_count - profiles[pbid]["lastSpam"] = now + profiles_dict[pbid]["spamCount"] = spam_count + profiles_dict[pbid]["lastSpam"] = now def on_join_request(ip: str) -> None: