From ba27a96ede4fd7350a3dfb30263aa2e17544064a Mon Sep 17 00:00:00 2001 From: brostosjoined Date: Sun, 26 Jan 2025 20:33:06 +0300 Subject: [PATCH] Api 9 release --- plugins/utilities.json | 16 + plugins/utilities/discord_richpresence.py | 817 +++++++++++++--------- plugins/utilities/natpmp_upnp.py | 517 +++++++++----- plugins/utilities/wave_emote.py | 50 ++ 4 files changed, 867 insertions(+), 533 deletions(-) create mode 100644 plugins/utilities/wave_emote.py diff --git a/plugins/utilities.json b/plugins/utilities.json index e1d1cd7..ff27283 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -1439,6 +1439,7 @@ } ], "versions": { + "1.5.2" : null, "1.5.1": { "api_version": 9, "commit_sha": "a93fda3", @@ -1681,6 +1682,7 @@ } ], "versions": { + "1.5.0": null, "1.0.0": { "api_version": 8, "commit_sha": "48bd0da", @@ -1688,6 +1690,20 @@ "md5sum": "15f969c23d19118d4898570cfae71c7b" } } + }, + "wave_emote": { + "description": "Type `hello` in chat while in game and get a wave emote", + "external_url": "", + "authors": [ + { + "name": "brostosjoined", + "email": "", + "discord": "brostos" + } + ], + "versions": { + "1.5.0": null + } } } } \ No newline at end of file diff --git a/plugins/utilities/discord_richpresence.py b/plugins/utilities/discord_richpresence.py index 30f567f..eb70ce8 100644 --- a/plugins/utilities/discord_richpresence.py +++ b/plugins/utilities/discord_richpresence.py @@ -4,6 +4,9 @@ # ba_meta require api 9 #!"Made to you by @brostos & @Dliwk" +# TODO +# - Update to the latest libs +# - Use account id to hash the tkn from __future__ import annotations @@ -12,12 +15,13 @@ from pathlib import Path from os import getcwd, remove from bauiv1lib.popup import PopupWindow - import asyncio +import sys import http.client import ast import uuid import json +import socket import time import threading import shutil @@ -35,17 +39,93 @@ if TYPE_CHECKING: from typing import Any, Tuple +MAPNAME_ID = { + "bombsquadicon": "963448129900908595", + "zigzagpreview": "963448133130522624", + "tiptoppreview": "963448133168279582", + "towerdpreview": "963448135886200912", + "thepadpreview": "963448137916248084", + "steprightuppreview": "963448141728862248", + "roundaboutpreview": "963448143997972550", + "rampagepreview": "963448146422296676", + "monkeyfacepreview": "963448151182831626", + "footballstadiumpreview": "963448158719983646", + "doomshroompreview": "963448160993292368", + "cragcastlepreview": "963448163048513536", + "courtyardpreview": "963448166127120504", + "bridgitpreview": "963448169180565654", + "biggpreview": "963448172905127996", + "alwayslandpreview": "963448174163423252", + "bigg": "1013013392455376977", + "bridgit": "1013013400139333632", + "courtyard": "1013013410776096788", + "cragcastle": "1013013423132528700", + "doomshroom": "1013013438223622224", + "footballstadium": "1013013452517810226", + "hockeystadium": "1013013464060547112", + "monkeyface": "1013013477721383023", + "rampage": "1013013484830728273", + "roundabout": "1013013508323037264", + "steprightup": "1013013567768907826", + "thepad": "1013013577197699163", + "tiptop": "1013013593089904721", + "towerd": "1013013604531970131", + "zigzag": "1013013618188619816", + "bombsquadlogo2": "1013016083701190726", + "windows": "1084050785488338984", + "linux": "1084078945944739920", + "lobby": "1084180821973418226", + "ranking": "1084224689272004719", + "rampagelevelcolor": "1086989941541703741", + "landmine": "1087000404866371766", + "rgbstripes": "1087000416492990474", + "shrapnel1color": "1087151233225195590", + "bonescolor": "1087151164077899928", + "bridgitlevelcolor": "1087151178674094182", + "crossout": "1087151197963681902", + "naturebackgroundcolor": "1087151209896476782", + "zigzaglevelcolor": "1087151253206876241", + "zoeicon": "1087151266989363240", + "bg": "1087564057890000906", + "alwayslandlevelcolor": "1087564765406167080", + "hockeystadiumpreview": "1087574349285961768", + "mac": "1087584375287336992", + "flyer": "1087584543147561051", + "replay": "1087592122393301102", + "coop": "1097697042891018311", + "ffa": "1097697050214269008", + "lobbysmall": "1097697055926923335", + "replaysmall": "1097697062746853386", + "teams": "1097697068727935036", + "bacongreece": "1097700754623565894", + "basketballstadium": "1097700771501441167", + "flapland": "1097700783622979664", + "alwaysland": "1097700794213613610", + "hoveringwood": "1097700802321199224", + "jrmponslaught": "1097700810479124520", + "jrmprunaround": "1097700817194205286", + "lakefrigid": "1097700828023898203", + "mushfeud": "1097700836920000594", + "pillar_bases": "1097700846340407427", + "powerup_factory": "1097700854422851656", + "snowballpit": "1097700869673341009", + "stoneishfort": "1097700887826272308", + "toiletdonut": "1097700898584666193", + "whereeaglesdare": "1097700904972587109", + "android": "1097728392280932453", +} ANDROID = babase.app.classic.platform == "android" -DIRPATH = Path( - f"{_babase.app.python_directory_user if build_number < 21282 else _babase.app.env.python_directory_user}/image_id.json") APP_VERSION = _babase.app.version if build_number < 21282 else ( _babase.app.env.engine_version if build_number > 21823 else _babase.app.env.version) + if ANDROID: # !can add ios in future # Installing websocket def get_module(): - install_path = Path(f"{getcwd()}/ba_data/python") # For the guys like me on windows + install_path = Path( + f"{getcwd()}/ba_data/python" + ) # For the guys like me on windows path = Path(f"{install_path}/websocket.tar.gz") file_path = Path(f"{install_path}/websocket") source_dir = Path(f"{install_path}/websocket-client-1.6.1/websocket") @@ -60,8 +140,11 @@ if ANDROID: # !can add ios in future filename, headers = urlretrieve(url, filename=path) with open(filename, "rb") as f: content = f.read() - assert hashlib.md5(content).hexdigest() == "86bc69b61947943627afc1b351c0b5db" - shutil.unpack_archive(filename, install_path) + assert ( + hashlib.md5(content).hexdigest() + == "86bc69b61947943627afc1b351c0b5db" + ) + shutil.unpack_archive(filename, install_path, format='gztar') remove(path) shutil.copytree(source_dir, file_path) shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) @@ -70,6 +153,7 @@ if ANDROID: # !can add ios in future shutil.rmtree(Path(f"{install_path}/websocket-client-1.6.1")) else: pass + get_module() from websocket import WebSocketConnectionClosedException @@ -79,16 +163,18 @@ if ANDROID: # !can add ios in future class PresenceUpdate: def __init__(self): - self.ws = websocket.WebSocketApp("wss://gateway.discord.gg/?encoding=json&v=10", - on_open=self.on_open, - on_message=self.on_message, - on_error=self.on_error, - on_close=self.on_close) + self.ws = websocket.WebSocketApp( + "wss://gateway.discord.gg/?encoding=json&v=10", + on_open=self.on_open, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close, + ) self.heartbeat_interval = int(41250) self.resume_gateway_url: str | None = None self.session_id: str | None = None self.stop_heartbeat_thread = threading.Event() - self.do_once = True + self.do_once: bool = True self.state: str | None = "In Game" self.details: str | None = "Main Menu" self.start_timestamp = time.time() @@ -96,7 +182,8 @@ if ANDROID: # !can add ios in future self.large_image_text: str | None = "BombSquad Icon" self.small_image_key: str | None = None self.small_image_text: str | None = ( - f"{_babase.app.classic.platform.capitalize()}({APP_VERSION})") + f"{_babase.app.classic.platform.capitalize()}({APP_VERSION})" + ) self.media_proxy = "mp:/app-assets/963434684669382696/{}.png" self.identify: bool = False self.party_id: str = str(uuid.uuid4()) @@ -104,15 +191,13 @@ if ANDROID: # !can add ios in future self.party_max = 8 def presence(self): - with open(DIRPATH, "r") as maptxt: - largetxt = json.load(maptxt)[self.large_image_key] - with open(DIRPATH, "r") as maptxt: - smalltxt = json.load(maptxt)[self.small_image_key] + largetxt = MAPNAME_ID[self.large_image_key] + smalltxt = MAPNAME_ID[self.small_image_key] presencepayload = { "op": 3, "d": { - "since": None, # used to show how long the user went idle will add afk to work with this and then set the status to idle + "since": start_time, # Fixed the unlimited time bug "status": "online", "afk": "false", "activities": [ @@ -122,12 +207,10 @@ if ANDROID: # !can add ios in future "application_id": "963434684669382696", "state": self.state, "details": self.details, - "timestamps": { - "start": start_time - }, + # "timestamps": {"start": start_time}, "party": { "id": self.party_id, - "size": [self.party_size, self.party_max] + "size": [self.party_size, self.party_max], }, "assets": { "large_image": self.media_proxy.format(largetxt), @@ -172,7 +255,13 @@ if ANDROID: # !can add ios in future babase.print_exception(error) def on_close(self, ws, close_status_code, close_msg): - print("Closed Discord Connection Successfully") + ( + print("Closed Discord Connection Successfully") + if close_status_code == 1000 + else print( + f"Closed Discord Connection with code {close_status_code} and message {close_msg}" + ) + ) def on_open(self, ws): print("Connected to Discord Websocket") @@ -189,8 +278,8 @@ if ANDROID: # !can add ios in future def identify(): """Identifying to the gateway and enable by using user token and the intents we will be using e.g 256->For Presence""" - with open(f"{_babase.app.env.python_directory_user}/__pycache__/token.txt", 'r') as f: - token = bytes.fromhex(f.read()).decode('utf-8') + byt_tkn = babase.app.config.get("token") + token = bytes.fromhex(byt_tkn).decode("utf-8") identify_payload = { "op": 2, "d": { @@ -204,6 +293,7 @@ if ANDROID: # !can add ios in future }, } # step 3 send an identify self.ws.send(json.dumps(identify_payload)) + identify() while True: heartbeat_payload = {"op": 1, "d": self.heartbeat_interval} @@ -221,8 +311,34 @@ if ANDROID: # !can add ios in future threading.Thread(target=heartbeats, daemon=True, name="heartbeat").start() def start(self): - if Path(f"{_babase.app.env.python_directory_user}/__pycache__/token.txt").exists(): - threading.Thread(target=self.ws.run_forever, daemon=True, name="websocket").start() + if ( + Path( + f"{_babase.app.env.python_directory_user}/__pycache__/token.txt" + ).exists() + or Path(f"{getcwd()}/token.txt").exists() + ): + try: + with open(f"{getcwd()}/token.txt", "r") as f: + token = bytes.fromhex(f.read()).decode("utf-8") + except FileNotFoundError: + with open( + f"{_babase.app.env.python_directory_user}/__pycache__/token.txt", + "r", + ) as f: + token = bytes.fromhex(f.read()).decode("utf-8") + babase.app.config["token"] = token + babase.app.config.commit() + + if babase.app.config.get("token"): + try: + while True: + urlopen("http://www.google.com", timeout=5) + threading.Thread( + target=self.ws.run_forever, daemon=True, name="websocket" + ).start() + return + except Exception: + return def close(self): self.stop_heartbeat_thread.set() @@ -243,62 +359,25 @@ if not ANDROID: filename, headers = urlretrieve(url, filename=path) with open(filename, "rb") as f: content = f.read() - assert hashlib.md5(content).hexdigest() == "f7c163cdd001af2456c09e241b90bad7" - shutil.unpack_archive(filename, install_path) + assert ( + hashlib.md5(content).hexdigest() + == "f7c163cdd001af2456c09e241b90bad7" + ) + shutil.unpack_archive(filename, install_path, format='gztar') shutil.copytree(source_dir, file_path) shutil.rmtree(Path(f"{install_path}/pypresence-4.3.0")) remove(path) except: pass - # Make modifications for it to work on windows - if babase.app.classic.platform == "windows": - with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "r") as file: - data = file.readlines() - data[45] = """ -def get_event_loop(force_fresh=False): - loop = asyncio.ProactorEventLoop() if sys.platform == 'win32' else asyncio.new_event_loop() - if force_fresh: - return loop - try: - running = asyncio.get_running_loop() - except RuntimeError: - return loop - if running.is_closed(): - return loop - else: - if sys.platform in ('linux', 'darwin'): - return running - if sys.platform == 'win32': - if isinstance(running, asyncio.ProactorEventLoop): - return running - else: - return loop""" - # Thanks Loup - with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "w") as file: - for number, line in enumerate(data): - if number not in range(46, 56): - file.write(line) - # fix the mess i did with the previous - elif file_path.exists(): - with open(Path(f"{getcwd()}/ba_data/python/pypresence/utils.py"), "r") as file: - data = file.readlines() - first_line = data[0].rstrip("\n") - if not first_line == '"""Util functions that are needed but messy."""': - shutil.rmtree(file_path) - get_module() get_module() from pypresence import PipeClosed, DiscordError, DiscordNotFound from pypresence.utils import get_event_loop import pypresence - import socket DEBUG = True - _last_server_addr = 'localhost' - _last_server_port = 43210 - def print_error(err: str, include_exception: bool = False) -> None: if DEBUG: if include_exception: @@ -327,8 +406,9 @@ def get_event_loop(force_fresh=False): global _last_server_addr global _last_server_port old_connect(*args, **kwargs) - c = kwargs.get("address") or args[0] - _last_server_port = kwargs.get("port") or args[1] + _last_server_addr = kwargs.get("address") or args[0] + # ! Joining a game on same device as host NB check what happens if host is port forwarded you join it and check joining a server port forwarded or not + _last_server_port = kwargs.get("port") or args[1] if len(args) > 1 else 43210 bs.connect_to_party = new_connect @@ -353,6 +433,7 @@ def get_event_loop(force_fresh=False): self._last_secret_update_time: float = 0 self._last_connect_time: float = 0 self.should_close = False + self.connection_to_host_info = None @staticmethod def is_discord_running(): @@ -360,45 +441,49 @@ def get_event_loop(force_fresh=False): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.01) try: - conn = s.connect_ex(('localhost', i)) + conn = s.connect_ex(("localhost", i)) s.close() - if (conn == 0): + if conn == 0: s.close() - return (True) + return True except: s.close() - return (False) + return False def _generate_join_secret(self): # resp = requests.get('https://legacy.ballistica.net/bsAccessCheck').text try: - connection_info = bs.get_connection_to_host_info_2() + connection_info = self.connection_to_host_info if connection_info: addr = _last_server_addr port = _last_server_port else: - with urlopen( - "https://legacy.ballistica.net/bsAccessCheck" - ) as resp: + with urlopen("https://legacy.ballistica.net/bsAccessCheck") as resp: resp = resp.read().decode() resp = ast.literal_eval(resp) addr = resp["address"] - port = 43210 - secret_dict = { - "format_version": 1, - "hostname": addr, - "port": port, - } - self.join_secret = json.dumps(secret_dict) - except: + port = resp["port"] + addr, port = addr, port + secret_dict = { + "format_version": 1, + "hostname": addr, + "port": port, + } + + self.join_secret = json.dumps(secret_dict) + except Exception as _: pass def _update_secret(self): + #! use in game thread threading.Thread(target=self._generate_join_secret, daemon=True).start() self._last_secret_update_time = time.time() def run(self) -> None: + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) asyncio.set_event_loop(get_event_loop()) + while not self.should_close: if time.time() - self._last_update_time > 0.1: self._do_update_presence() @@ -456,17 +541,16 @@ def get_event_loop(force_fresh=False): party_id=self.party_id, party_size=[self.party_size, self.party_max], join=self.join_secret, - # buttons = [ #!cant use buttons together with join - # { - # "label": "Discord Server", - # "url": "https://ballistica.net/discord" - # }, - # { - # "label": "Download Bombsquad", - # "url": "https://bombsquad.ga/download"} - # ] ) - + # buttons = [ #!cant use buttons together with join + # { + # "label": "Discord Server", + # "url": "https://ballistica.net/discord" + # }, + # { + # "label": "Download Bombsquad", + # "url": "https://bombsquad.ga/download"} + # ] self.handle_event(data) except (PipeClosed, DiscordError, AssertionError, AttributeError): try: @@ -503,28 +587,31 @@ def get_event_loop(force_fresh=False): user = data.get("user", {}) uid = user.get("id") username = user.get("username") - discriminator = user.get("discriminator", None) avatar = user.get("avatar") - self.on_join_request(username, uid, discriminator, avatar) + self.on_join_request(username, uid, avatar) def _connect_to_party(self, hostname, port) -> None: babase.pushcall( babase.Call(bs.connect_to_party, hostname, port), from_other_thread=True ) - def on_join_request(self, username, uid, discriminator, avatar) -> None: + def on_join_request(self, username, uid, avatar) -> None: del uid # unused del avatar # unused babase.pushcall( babase.Call( bui.screenmessage, - "Discord: {}{} wants to join!".format( - username, discriminator if discriminator != "#0" else ""), + "Discord: {} wants to join!".format( + username + ), color=(0.0, 1.0, 0.0), ), from_other_thread=True, ) - babase.pushcall(lambda: bui.getsound('bellMed').play(), from_other_thread=True) + #! check this function for sound creation error + babase.pushcall( + lambda: bui.getsound("bellMed").play(), from_other_thread=True + ) class Discordlogin(PopupWindow): @@ -533,92 +620,112 @@ class Discordlogin(PopupWindow): # pylint: disable=too-many-locals _uiscale = bui.app.ui_v1.uiscale self._transitioning_out = False - s = 1.25 if _uiscale is babase.UIScale.SMALL else 1.27 if _uiscale is babase.UIScale.MEDIUM else 1.3 + s = ( + 1.25 + if _uiscale is babase.UIScale.SMALL + else 1.27 if _uiscale is babase.UIScale.MEDIUM else 1.3 + ) self._width = 380 * s self._height = 150 + 150 * s - self.path = Path(f"{_babase.app.env.python_directory_user}/__pycache__/token.txt") bg_color = (0.5, 0.4, 0.6) - log_btn_colour = (0.10, 0.95, 0.10) if not self.path.exists() else (1.00, 0.15, 0.15) - log_txt = "LOG IN" if not self.path.exists() else "LOG OUT" + log_btn_colour = ( + (0.10, 0.95, 0.10) + if not babase.app.config.get("token") + else (1.00, 0.15, 0.15) + ) + log_txt = "LOG IN" if not babase.app.config.get("token") else "LOG OUT" self.code = False self.resp = "Placeholder" self.headers = { - 'user-agent': "Mozilla/5.0", - 'content-type': "application/json", + "user-agent": "Mozilla/5.0", + "content-type": "application/json", } # creates our _root_widget - PopupWindow.__init__(self, - position=(0.0, 0.0), - size=(self._width, self._height), - scale=(2.1 if _uiscale is babase.UIScale.SMALL else 1.5 - if _uiscale is babase.UIScale.MEDIUM else 1.0), - bg_color=bg_color) + PopupWindow.__init__( + self, + position=(0.0, 0.0), + size=(self._width, self._height), + scale=( + 2.1 + if _uiscale is babase.UIScale.SMALL + else 1.5 if _uiscale is babase.UIScale.MEDIUM else 1.0 + ), + bg_color=bg_color, + ) self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(25, self._height - 40), size=(50, 50), scale=0.58, - label='', + label="", color=bg_color, on_activate_call=self._on_cancel_press, autoselect=True, - icon=bui.gettexture('crossOut'), - iconscale=1.2) + icon=bui.gettexture("crossOut"), + iconscale=1.2, + ) - bui.imagewidget(parent=self.root_widget, - position=(180, self._height - 55), - size=(32 * s, 32 * s), - texture=bui.gettexture("discordLogo"), - color=(10 - 0.32, 10 - 0.39, 10 - 0.96)) + bui.imagewidget( + parent=self.root_widget, + position=(180, self._height - 55), + size=(32 * s, 32 * s), + texture=bui.gettexture("discordLogo"), + color=(10 - 0.32, 10 - 0.39, 10 - 0.96), + ) - self.email_widget = bui.textwidget(parent=self.root_widget, - text="Email/Phone Number", - size=(400, 70), - position=(50, 180), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) + self.email_widget = bui.textwidget( + parent=self.root_widget, + text="Email/Phone Number", + size=(400, 70), + position=(50, 180), + h_align="left", + v_align="center", + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220, + ) - self.password_widget = bui.textwidget(parent=self.root_widget, - text="Password", - size=(400, 70), - position=(50, 120), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) + self.password_widget = bui.textwidget( + parent=self.root_widget, + text="Password", + size=(400, 70), + position=(50, 120), + h_align="left", + v_align="center", + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220, + ) - bui.containerwidget(edit=self.root_widget, - cancel_button=self._cancel_button) + bui.containerwidget(edit=self.root_widget, cancel_button=self._cancel_button) bui.textwidget( parent=self.root_widget, position=(265, self._height - 37), size=(0, 0), - h_align='center', - v_align='center', + h_align="center", + v_align="center", scale=1.0, text="Discord", maxwidth=200, - color=(0.80, 0.80, 0.80)) + color=(0.80, 0.80, 0.80), + ) bui.textwidget( parent=self.root_widget, position=(265, self._height - 78), size=(0, 0), - h_align='center', - v_align='center', + h_align="center", + v_align="center", scale=1.0, - text="💀Use at your own risk💀\n ⚠️discord account might get terminated⚠️", + text="💀Use at your own risk💀\n ⚠︝discord account might get terminated⚠︝", maxwidth=200, - color=(1.00, 0.15, 0.15)) + color=(1.00, 0.15, 0.15), + ) self._login_button = bui.buttonwidget( parent=self.root_widget, @@ -628,7 +735,8 @@ class Discordlogin(PopupWindow): label=log_txt, color=log_btn_colour, on_activate_call=self.login, - autoselect=True) + autoselect=True, + ) def _on_cancel_press(self) -> None: self._transition_out() @@ -636,10 +744,10 @@ class Discordlogin(PopupWindow): def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True - bui.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition="out_scale") def on_bascenev1libup_cancel(self) -> None: - bui.getsound('swish').play() + bui.getsound("swish").play() self._transition_out() def backup_2fa_code(self, tickt): @@ -647,16 +755,18 @@ class Discordlogin(PopupWindow): self.email_widget.delete() self.password_widget.delete() - self.backup_2fa_widget = bui.textwidget(parent=self.root_widget, - text="2FA/Discord Backup code", - size=(400, 70), - position=(50, 120), - h_align='left', - v_align='center', - editable=True, - scale=0.8, - autoselect=True, - maxwidth=220) + self.backup_2fa_widget = bui.textwidget( + parent=self.root_widget, + text="2FA/Discord Backup code", + size=(400, 70), + position=(50, 120), + h_align="left", + v_align="center", + editable=True, + scale=0.8, + autoselect=True, + maxwidth=220, + ) json_data_2FA = { "code": bui.textwidget(query=self.backup_2fa_widget), @@ -664,70 +774,70 @@ class Discordlogin(PopupWindow): "ticket": tickt, } - if json_data_2FA['code'] != "2FA/Discord Backup code": + if json_data_2FA["code"] != "2FA/Discord Backup code": try: payload_2FA = json.dumps(json_data_2FA) conn_2FA = http.client.HTTPSConnection("discord.com") - conn_2FA.request("POST", "/api/v9/auth/mfa/totp", payload_2FA, self.headers) + conn_2FA.request( + "POST", "/api/v9/auth/mfa/totp", payload_2FA, self.headers + ) res_2FA = conn_2FA.getresponse().read() - token = json.loads(res_2FA)['token'].encode().hex().encode() - - with open(self.path, 'wb') as f: - f.write(token) + token = json.loads(res_2FA)["token"].encode().hex().encode() + babase.app.config["token"] = token + babase.app.config.commit() bui.screenmessage("Successfully logged in", (0.21, 1.0, 0.20)) - bui.getsound('shieldUp').play() + bui.getsound("shieldUp").play() self.on_bascenev1libup_cancel() PresenceUpdate().start() except: self.code = True bui.screenmessage("Incorrect code", (1.00, 0.15, 0.15)) - bui.getsound('error').play() + bui.getsound("error").play() def login(self): - if not self.path.exists() and self.code == False: + if not babase.app.config.get("token") and self.code == False: try: json_data = { - 'login': bui.textwidget(query=self.email_widget), - 'password': bui.textwidget(query=self.password_widget), - 'undelete': False, - 'captcha_key': None, - 'login_source': None, - 'gift_code_sku_id': None, + "login": bui.textwidget(query=self.email_widget), + "password": bui.textwidget(query=self.password_widget), + "undelete": False, + "captcha_key": None, + "login_source": None, + "gift_code_sku_id": None, } conn = http.client.HTTPSConnection("discord.com") payload = json.dumps(json_data) - # conn.request("POST", "/api/v9/auth/login", payload, headers) - # res = conn.getresponse().read() conn.request("POST", "/api/v9/auth/login", payload, self.headers) res = conn.getresponse().read() try: - token = json.loads(res)['token'].encode().hex().encode() - with open(self.path, 'wb') as f: - f.write(token) - bui.screenmessage("Successfully logged in", (0.21, 1.0, 0.20)) - bui.getsound('shieldUp').play() - self.on_bascenev1libup_cancel() - PresenceUpdate().start() + token = json.loads(res)["token"].encode().hex().encode() + babase.app.config["token"] = token + babase.app.config.commit() + bui.screenmessage("Successfully logged in", (0.21, 1.0, 0.20)) + bui.getsound("shieldUp").play() + self.on_bascenev1libup_cancel() + PresenceUpdate().start() except KeyError: try: - ticket = json.loads(res)['ticket'] - bui.screenmessage("Input your 2FA or Discord Backup code", - (0.21, 1.0, 0.20)) - bui.getsound('error').play() + ticket = json.loads(res)["ticket"] + bui.screenmessage( + "Input your 2FA or Discord Backup code", (0.21, 1.0, 0.20) + ) + bui.getsound("error").play() self.resp = ticket self.backup_2fa_code(tickt=ticket) self.code = True except KeyError: bui.screenmessage("Incorrect credentials", (1.00, 0.15, 0.15)) - bui.getsound('error').play() + bui.getsound("error").play() except: bui.screenmessage("Connect to the internet", (1.00, 0.15, 0.15)) - bui.getsound('error').play() + bui.getsound("error").play() conn.close() elif self.code == True: @@ -736,42 +846,14 @@ class Discordlogin(PopupWindow): else: self.email_widget.delete() self.password_widget.delete() - remove(self.path) - bui.getsound('shieldDown').play() + del babase.app.config["token"] + babase.app.config.commit() + bui.getsound("shieldDown").play() bui.screenmessage("Account successfully removed!!", (0.10, 0.10, 1.00)) self.on_bascenev1libup_cancel() PresenceUpdate().close() -run_once = False - - -def get_once_asset(): - global run_once - if run_once: - return - response = Request( - "https://discordapp.com/api/oauth2/applications/963434684669382696/assets", - headers={"User-Agent": "Mozilla/5.0"}, - ) - try: - with urlopen(response) as assets: - assets = json.loads(assets.read()) - asset = [] - asset_id = [] - for x in assets: - dem = x["name"] - don = x["id"] - asset_id.append(don) - asset.append(dem) - asset_id_dict = dict(zip(asset, asset_id)) - - with open(DIRPATH, "w") as imagesets: - jsonfile = json.dumps(asset_id_dict) - json.dump(asset_id_dict, imagesets, indent=4) - except: - pass - run_once = True def get_class(): @@ -790,11 +872,12 @@ class DiscordRP(babase.Plugin): if not ANDROID: _run_overrides() - get_once_asset() def on_app_running(self) -> None: if not ANDROID: - self.rpc_thread.start() + threading.Thread( + target=self.rpc_thread.start, daemon=True, name="start_rpc" + ).start() self.update_timer = bs.AppTimer( 1, bs.WeakCall(self.update_status), repeat=True @@ -806,19 +889,19 @@ class DiscordRP(babase.Plugin): ) def has_settings_ui(self): - return True + if ANDROID: + return True + else: + return False def show_settings_ui(self, button): - if not ANDROID: - bui.screenmessage("Nothing here achievement!!!", (0.26, 0.65, 0.94)) - bui.getsound('achievement').play() - if ANDROID: - Discordlogin() + Discordlogin() def on_app_shutdown(self) -> None: if not ANDROID and self.rpc_thread.is_discord_running(): self.rpc_thread.rpc.close() self.rpc_thread.should_close = True + def on_app_pause(self) -> None: self.rpc_thread.close() @@ -855,7 +938,7 @@ class DiscordRP(babase.Plugin): if name == this: self.rpc_thread.large_image_key = "lobby" self.rpc_thread.large_image_text = "Bombing up" - # self.rpc_thread.small_image_key = "lobbysmall" + self.rpc_thread.small_image_key = "lobbysmall" if name == "Ranking": self.rpc_thread.large_image_key = "ranking" self.rpc_thread.large_image_text = "Viewing Results" @@ -871,7 +954,15 @@ class DiscordRP(babase.Plugin): def update_status(self) -> None: roster = bs.get_game_roster() - connection_info = bs.get_connection_to_host_info_2() + try: + connection_info = ( + bs.get_connection_to_host_info() + if build_number < 21727 + else bs.get_connection_to_host_info_2() + ) + self.rpc_thread.connection_to_host_info = connection_info + except (RuntimeError, TypeError): + pass self.rpc_thread.large_image_key = "bombsquadicon" self.rpc_thread.large_image_text = "BombSquad" @@ -879,116 +970,154 @@ class DiscordRP(babase.Plugin): self.rpc_thread.small_image_text = ( f"{_babase.app.classic.platform.capitalize()}({APP_VERSION})" ) - if not ANDROID: - svinfo = str(connection_info) - if self._last_server_info != svinfo: - self._last_server_info = svinfo - self.rpc_thread.party_id = str(uuid.uuid4()) - self.rpc_thread._update_secret() - if connection_info: - servername = connection_info.name - self.rpc_thread.details = "Online" - self.rpc_thread.party_size = max( - 1, sum(len(client["players"]) for client in roster) - ) - self.rpc_thread.party_max = max(8, self.rpc_thread.party_size) - if len(servername) == 19 and "Private Party" in servername: - self.rpc_thread.state = "Private Party" - elif servername == "": # A local game joinable from the internet - try: - offlinename = json.loads(bs.get_game_roster()[0]["spec_string"])[ - "n" - ] - if len(offlinename) > 19: # Thanks Rikko - self.rpc_thread.state = offlinename[slice(19)] + "..." - else: - self.rpc_thread.state = offlinename - except IndexError: - pass - else: - if len(servername) > 19: - self.rpc_thread.state = servername[slice(19)] + ".." + try: + if not ANDROID: + svinfo = str(connection_info) + if self._last_server_info != svinfo: + self._last_server_info = svinfo + self.rpc_thread.party_id = str(uuid.uuid4()) + self.rpc_thread._update_secret() + + if connection_info: + hostname = socket.gethostname() + local_ip = socket.gethostbyname(hostname) + + if bs.get_connection_to_host_info_2().address == local_ip: + self.rpc_thread.details = "Local Server" else: - self.rpc_thread.state = servername[slice(19)] - - if not connection_info: - self.rpc_thread.details = "Local" # ! replace with something like ballistica github cause - self.rpc_thread.state = self._get_current_activity_name() - self.rpc_thread.party_size = max(1, len(roster)) - self.rpc_thread.party_max = max(1, bs.get_public_party_max_size()) - - if ( - bs.get_foreground_host_session() is not None - and self.rpc_thread.details == "Local" - ): - session = ( - bs.get_foreground_host_session() - .__class__.__name__.replace("MainMenuSession", "") - .replace("EndSession", "") - .replace("FreeForAllSession", ": FFA") # ! for session use small image key - .replace("DualTeamSession", ": Teams") - .replace("CoopSession", ": Coop") + self.rpc_thread.details = "Online" + + servername = connection_info.name + self.rpc_thread.party_size = max( + 1, sum(len(client["players"]) for client in roster) ) - #! self.rpc_thread.small_image_key = session.lower() - self.rpc_thread.details = f"{self.rpc_thread.details} {session}" - if ( - self.rpc_thread.state == "NoneType" - ): # sometimes the game just breaks which means its not really watching replay FIXME - self.rpc_thread.state = "Watching Replay" - self.rpc_thread.large_image_key = "replay" - self.rpc_thread.large_image_text = "Viewing Awesomeness" - #!self.rpc_thread.small_image_key = "replaysmall" - - act = bs.get_foreground_host_activity() - session = bs.get_foreground_host_session() - if act: - from bascenev1lib.game.elimination import EliminationGame - from bascenev1lib.game.thelaststand import TheLastStandGame - from bascenev1lib.game.meteorshower import MeteorShowerGame - - # noinspection PyUnresolvedReferences,PyProtectedMember - try: - self.rpc_thread.start_timestamp = act._discordrp_start_time # type: ignore - except AttributeError: - # This can be the case if plugin launched AFTER activity - # has been created; in that case let's assume it was - # created just now. - self.rpc_thread.start_timestamp = act._discordrp_start_time = time.mktime( # type: ignore - time.localtime() - ) - if isinstance(act, EliminationGame): - alive_count = len([p for p in act.players if p.lives > 0]) - self.rpc_thread.details += f" ({alive_count} players left)" - elif isinstance(act, TheLastStandGame): - # noinspection PyProtectedMember - points = act._score - self.rpc_thread.details += f" ({points} points)" - elif isinstance(act, MeteorShowerGame): - with act.context: - sec = bs.time() - act._timer.getstarttime() - secfmt = "" - if sec < 60: - secfmt = f"{sec:.2f}" + self.rpc_thread.party_max = max(8, self.rpc_thread.party_size) + if len(servername) == 19 and "Private Party" in servername: + self.rpc_thread.state = "Private Party" + elif servername == "": # A local game joinable from the internet + try: + offlinename = json.loads( + bs.get_game_roster()[0]["spec_string"] + )["n"] + if len(offlinename) > 19: # Thanks Rikko + self.rpc_thread.state = offlinename[slice(19)] + "..." + else: + self.rpc_thread.state = offlinename + except IndexError: + pass + else: + if len(servername) > 19: + self.rpc_thread.state = servername[slice(19)] + ".." else: - secfmt = f"{int(sec) // 60:02}:{sec:.2f}" - self.rpc_thread.details += f" ({secfmt})" + self.rpc_thread.state = servername[slice(19)] - # if isinstance(session, ba.DualTeamSession): - # scores = ':'.join([ - # str(t.customdata['score']) - # for t in session.sessionteams - # ]) - # self.rpc_thread.details += f' ({scores})' + if not connection_info: + self.rpc_thread.details = ( + "Local" # ! replace with something like ballistica github cause + ) + self.rpc_thread.state = self._get_current_activity_name() + self.rpc_thread.party_size = max(1, len(roster)) + self.rpc_thread.party_max = max(1, bs.get_public_party_max_size()) - mapname, short_map_name = self._get_current_map_name() - if mapname: - with open(DIRPATH, 'r') as asset_dict: - asset_keys = json.load(asset_dict).keys() - if short_map_name in asset_keys: - self.rpc_thread.large_image_text = mapname - self.rpc_thread.large_image_key = short_map_name - self.rpc_thread.small_image_key = 'bombsquadlogo2' - self.rpc_thread.small_image_text = 'BombSquad' + if ( + bs.get_foreground_host_session() is not None + and self.rpc_thread.details == "Local" + ): + session = ( + bs.get_foreground_host_session() + .__class__.__name__.replace("MainMenuSession", "") + .replace("EndSession", "") + .replace("FreeForAllSession", ": FFA") + .replace("DualTeamSession", ": Teams") + .replace("CoopSession", ": Coop") + ) + if len(session) > 1: + self.rpc_thread.small_image_key = session.replace( + ": ", "" + ).lower() + self.rpc_thread.small_image_text = session.replace(": ", "") + self.rpc_thread.details = self.rpc_thread.details + if ( + self.rpc_thread.state == "NoneType" + ): # sometimes the game just breaks which means its not really watching replay FIXME + self.rpc_thread.state = "Watching Replay" + self.rpc_thread.large_image_key = "replay" + self.rpc_thread.large_image_text = "Viewing Awesomeness" + self.rpc_thread.small_image_key = "replaysmall" + except UnboundLocalError: + pass + + act = bs.get_foreground_host_activity() + session = bs.get_foreground_host_session() + if act: + from bascenev1lib.game.elimination import EliminationGame + from bascenev1lib.game.thelaststand import TheLastStandGame + from bascenev1lib.game.meteorshower import MeteorShowerGame + from bascenev1lib.game.football import FootballCoopGame + from bascenev1lib.game.easteregghunt import EasterEggHuntGame + + # noinspection PyUnresolvedReferences,PyProtectedMember + try: + self.rpc_thread.start_timestamp = act._discordrp_start_time # type: ignore + except AttributeError: + # This can be the case if plugin launched AFTER activity + # has been created; in that case let's assume it was + # created just now. + self.rpc_thread.start_timestamp = act._discordrp_start_time = time.mktime( # type: ignore + time.localtime() + ) + if isinstance(act, EliminationGame): + alive_count = len([p for p in act.players if p.lives > 0]) + self.rpc_thread.details += f" ({alive_count} players left)" + elif isinstance(act, TheLastStandGame): + # noinspection PyProtectedMember + points = act._score + self.rpc_thread.details += f" ({points} points)" + elif isinstance(act, MeteorShowerGame): + with act.context: + sec = bs.time() - act._timer.getstarttime() + secfmt = "" + if sec < 60: + secfmt = f"{sec:.2f}" + else: + secfmt = f"{int(sec) // 60:02}:{sec:.2f}" + self.rpc_thread.details += f" ({secfmt})" + # elif isinstance(act, OnslaughtGame): + # score = act._score + # level = act._wavenum + # # self. + elif isinstance(act, FootballCoopGame): + # try: + # score = f"{act.teams[0].score} : {act.teams[1].score}" + # except IndexError: + score = f"{act.teams[0].score} : {act._bot_team.score}" + self.rpc_thread.details = score + # elif isinstance(act, RunaroundGame) + # score = act._score + # level = act._wavenum + # lives = act._lives + elif isinstance(act, EasterEggHuntGame): + eggs_collected = len(act._eggs) - 1 + self.rpc_thread.details = f"{eggs_collected} eggs collected" + # elif isinstance(act, TargetPracticeGame): + # #for FFA + # scoere = bs.get_foreground_host_activity().players[0].score + + # if isinstance(session, ba.DualTeamSession): + # scores = ':'.join([ + # str(t.customdata['score']) + # for t in session.sessionteams + # ]) + # self.rpc_thread.details += f' ({scores})' + + mapname, short_map_name = self._get_current_map_name() + if mapname: + asset_keys = MAPNAME_ID.keys() + if short_map_name in asset_keys: + self.rpc_thread.large_image_text = mapname + self.rpc_thread.large_image_key = short_map_name + # self.rpc_thread.small_image_key = 'bombsquadlogo2' + # self.rpc_thread.small_image_text = 'BombSquad' if _babase.get_idle_time() / (1000 * 60) % 60 >= 0.4: self.rpc_thread.details = f"AFK in {self.rpc_thread.details}" @@ -996,5 +1125,7 @@ class DiscordRP(babase.Plugin): self.rpc_thread.large_image_key = ( "https://media.tenor.com/uAqNn6fv7x4AAAAM/bombsquad-spaz.gif" ) - if ANDROID and Path(f"{_babase.app.env.python_directory_user}/__pycache__/token.txt").exists(): - self.rpc_thread.presence() + if babase.app.config.get("token"): + #! This function might cause some errors + # self.rpc_thread.presence() + pass diff --git a/plugins/utilities/natpmp_upnp.py b/plugins/utilities/natpmp_upnp.py index f8aa684..58fb2db 100644 --- a/plugins/utilities/natpmp_upnp.py +++ b/plugins/utilities/natpmp_upnp.py @@ -1,14 +1,19 @@ -# ba_meta require api 8 - +# ba_meta require api 9 +#! Try patching upnpclient to use defusedxml replacement for lxml for more device support import babase import bauiv1 as bui import bascenev1 as bs import shutil +import platform +import os import hashlib +import zipfile +import tarfile import threading import ast import time +from urllib.parse import urlparse, unquote from pathlib import Path from os import remove, getcwd from urllib.request import urlretrieve, urlopen @@ -36,63 +41,214 @@ def threaded(func): @threaded def get_modules() -> None: - install_path = Path(f"{getcwd()}/ba_data/python") # For the guys like me on windows - upnpy_path = Path(f"{install_path}/upnp.tar.gz") - nat_pmp_path = Path(f"{install_path}/natpmp.tar.gz") - upnpy_file_path = Path(f"{install_path}/upnpy") - nat_pmp_file_path = Path(f"{install_path}/natpmp") - nat_pmp_source_dir = Path(f"{install_path}/NAT-PMP-1.3.2/natpmp") - upnpy_source_dir = Path(f"{install_path}/UPnPy-1.1.8/upnpy") - if ( - not Path(f"{nat_pmp_file_path}/__init__.py").exists() - and not Path(f"{upnpy_file_path}/__init__.py").exists() - ): # YouKnowDev - nat_pmp_url = "https://files.pythonhosted.org/packages/dc/0c/28263fb4a623e6718a179bca1f360a6ae38f0f716a6cacdf47e15a5fa23e/NAT-PMP-1.3.2.tar.gz" - upnpy_url = "https://files.pythonhosted.org/packages/80/66/d4e721ff8766ea3e78730574669f6feeb71e438a8c2d7a62b2c3456a5c12/UPnPy-1.1.8.tar.gz" - try: - # fix issue where the file delete themselves + if babase.app.classic.platform == "mac": + install_path = bs.app.env.python_directory_app + else: + install_path = Path( + f"{getcwd()}/ba_data/python" + ) # For the guys like me on windows + packages = { + "upnp-client": { + "url": "https://files.pythonhosted.org/packages/dd/69/4d38d9fa757c328df93e7037eb8c1da8ca48e62828c23ba3c421e9335e30/upnpclient-1.0.3.tar.gz", + "md5": "f936c8de89705555f6bd736a66d3af5d", + "folder": "upnpclient", + }, + "python-dateutil": { + "url": "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", + "md5": "81cb6aad924ef40ebfd3d62eaebe47c6", + "folder": "dateutil", + }, + "six": { + "url": "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", + "md5": "a0387fe15662c71057b4fb2b7aa9056a", + "folder": "six.py", + }, + "requests": { + "url": "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", + "md5": "fa3ee5ac3f1b3f4368bd74ab530d3f0f", + "folder": "requests", + }, + "idna": { + "url": "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", + "md5": "28448b00665099117b6daa9887812cc4", + "folder": "idna", + }, + #! Api 9 already has urllib3 module + # "urllib3": { + # "url": "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", + # "md5": "872f7f43af1b48e7c116c7542ab39fab", + # "folder": "urllib3", + # }, + "ifaddr": { + "url": "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", + "md5": "b1cac02b5dc354d68dd6d853bc9565a7", + "folder": "ifaddr", + }, + "NAT-PMP": { + "url": "https://files.pythonhosted.org/packages/dc/0c/28263fb4a623e6718a179bca1f360a6ae38f0f716a6cacdf47e15a5fa23e/NAT-PMP-1.3.2.tar.gz", + "md5": "7e5faa22acb0935f75664e9c4941fda4", + "folder": "natpmp", + }, + } + + system = platform.platform() + + if "Windows" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/36/88/684d4e800f5aa28df2a991a6a622783fb73cf0e46235cfa690f9776f032e/lxml-5.3.0-cp312-cp312-win32.whl", + "md5": "a5579cb068a3fbfb5989fbeb4024c599", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", + "md5": "babec153025b1270d6a2fd76e2c3772f", + "folder": "charset_normalizer", + } + elif "Darwin" in system and "arm64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/eb/6d/d1f1c5e40c64bf62afd7a3f9b34ce18a586a1cccbf71e783cd0a6d8e8971/lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", + "md5": "0200ca09c13892c80b47cf4c713786ed", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", + "md5": "572c9f4f64469518d6a6b4c15710201a", + "folder": "charset_normalizer", + } + elif "Darwin" in system and "x86_64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/bd/83/26b1864921869784355459f374896dcf8b44d4af3b15d7697e9156cb2de9/lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", + "md5": "96b82c1e6d24472af28c48d9bb21605e", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", + "md5": "572c9f4f64469518d6a6b4c15710201a", + "folder": "charset_normalizer", + } + elif "glibc" in system and "x86_64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/0a/6e/94537acfb5b8f18235d13186d247bca478fea5e87d224644e0fe907df976/lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", + "md5": "d63bf3d33e46a3b0262176b1a815b4b0", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "md5": "1edb315f82fa657b8ee5d564117e057c", + "folder": "charset_normalizer", + } + elif "glibc" in system and "aarch64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/88/69/6972bfafa8cd3ddc8562b126dd607011e218e17be313a8b1b9cc5a0ee876/lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "md5": "663ccdccd076b26b5607901799c671be", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "md5": "d2e8c76665fb9fb013882d4052f46b95", + "folder": "charset_normalizer", + } + elif not "glibc" in system and "x86_64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/7d/ed/e6276c8d9668028213df01f598f385b05b55a4e1b4662ee12ef05dab35aa/lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", + "md5": "659bdaee4672e8409b277b570e3e3e39", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", + "md5": "7a60860d64616d5a0af22d034963ab11", + "folder": "charset_normalizer", + } + elif not "glibc" in system and "aarch64" in system: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/8d/e8/4b15df533fe8e8d53363b23a41df9be907330e1fa28c7ca36893fad338ee/lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", + "md5": "3ec71cd198cc28525f4c1d65d41a7689", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", + "md5": "ed3a63cc79137f316ee386cd7aaea7e6", + "folder": "charset_normalizer", + } + else: + packages["lxml"] = { + "url": "https://files.pythonhosted.org/packages/e0/d2/e9bff9fb359226c25cda3538f664f54f2804f4b37b0d7c944639e1a51f69/lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", + "md5": "ecfccadd587adb67ca54a24977e1a436", + "folder": "lxml", + } + packages["charset_normalizer"] = { + "url": "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", + "md5": "9bdbf872c3bdbcb7191d5cdf3176c38a", + "folder": "charset_normalizer", + } + + for package, details in packages.items(): + parsed_url = urlparse(details["url"]) + path = unquote(parsed_url.path) + filename = os.path.basename(path) + + if details["url"].endswith(".whl"): + file_format = "whl" + folder_name = "-".join(filename.split("-")[:2]) + elif details["url"].endswith(".tar.gz"): + file_format = "tar.gz" + folder_name = filename.rsplit(".", 2)[0] + package_path = os.path.join(install_path, f"{package}.{file_format}") + package_path = Path(f"{install_path}/{package}.{file_format}") + package_source_dir = Path(f"{install_path}/{details['folder']}") + + if not Path(f"{package_source_dir}/__init__.py").exists(): try: - shutil.rmtree(nat_pmp_file_path) - shutil.rmtree(upnpy_file_path) + shutil.rmtree(package_source_dir) except: pass - nat_pmp_filename, headers = urlretrieve(nat_pmp_url, filename=nat_pmp_path) - upnpy_filename, headers = urlretrieve(upnpy_url, filename=upnpy_path) - with open(nat_pmp_filename, "rb") as f: - content = f.read() - assert ( - hashlib.md5(content).hexdigest() - == "7e5faa22acb0935f75664e9c4941fda4" - ) - with open(upnpy_filename, "rb") as f: - content = f.read() - assert ( - hashlib.md5(content).hexdigest() - == "b33ad0b38e39af258e2c8f38813abf7b" - ) - shutil.unpack_archive(nat_pmp_filename, install_path) - shutil.unpack_archive(upnpy_filename, install_path) - remove(upnpy_path) - remove(nat_pmp_path) - shutil.copytree(nat_pmp_source_dir, nat_pmp_file_path) - shutil.copytree(upnpy_source_dir, upnpy_file_path) - shutil.rmtree(Path(f"{install_path}/NAT-PMP-1.3.2")) - shutil.rmtree(Path(f"{install_path}/UPnPy-1.1.8")) - except Exception as e: - if type(e) == shutil.Error: - shutil.rmtree(Path(f"{install_path}/NAT-PMP-1.3.2")) - shutil.rmtree(Path(f"{install_path}/UPnPy-1.1.8")) - else: - pass - # Patch to natpmp to work without netifaces - with open(f"{nat_pmp_file_path}/__init__.py", "r") as f: - lines = f.readlines() - # Define the new function as a string - new_function = ''' + package_filename, headers = urlretrieve( + details["url"], filename=package_path + ) + + with open(package_filename, "rb") as f: + content = f.read() + assert hashlib.md5(content).hexdigest() == details["md5"] + try: + shutil.unpack_archive(package_filename, install_path, format='gztar') + extracted_package_files = Path(f"{install_path}/{folder_name}") + for root, dirs, files in os.walk(extracted_package_files): + for dir in dirs: + subfolder = os.path.join(root, dir) + if subfolder.endswith(details["folder"]): + shutil.copytree( + subfolder, f"{install_path}/{details['folder']}" + ) + if details["folder"] == "six.py": + shutil.copy( + Path(f"{install_path}/{folder_name}/six.py"), + f"{install_path}/six.py", + ) + try: + shutil.rmtree(Path(f"{install_path}/{folder_name}")) + except FileNotFoundError: + pass + except shutil.ReadError as e: + with zipfile.ZipFile(package_filename, "r") as zip_ref: + zip_ref.extractall(install_path) + try: + shutil.rmtree(Path(f"{install_path}/lxml-5.3.0.dist-info")) #! Remember to update accordingly + except: + shutil.rmtree(Path(f"{install_path}/charset_normalizer-3.4.1.dist-info")) #! + remove(package_path) + else: + return + # Patch to natpmp to work without netifaces + with open(Path(f"{install_path}/natpmp/__init__.py"), "r") as f: + lines = f.readlines() + # Define the new function as a string + new_function = ''' # Plucked from https://github.com/tenable/upnp_info/blob/d20a1fda8ca4877d61b89fe7126077a3a5f0b322/upnp_info.py#L23 def get_gateway_addr(): - """Returns the gateway ip of the router if upnp service is available""" + """ + Returns the gateway ip of the router if upnp service is available + """ try: locations = set() location_regex = re.compile("location:[ ]*(.+)"+ chr(13) + chr(10), re.IGNORECASE) @@ -124,168 +280,149 @@ def get_gateway_addr(): return gateway_ip_address except: pass - + ''' + # Replace the function + lines[224:229] = new_function + lines[21] = "import socket\nimport re\nfrom urllib.parse import urlparse" - # Replace the function - lines[224:229] = new_function - lines[21] = "import socket\nimport re\nfrom urllib.parse import urlparse" + with open(Path(f"{install_path}/natpmp/__init__.py"), "w") as f: + f.writelines(lines) - with open(f"{nat_pmp_file_path}/__init__.py", "w") as f: - f.writelines(lines) - - add_port_mapping() + add_port_mapping() +def play_sound(sound): + with bs.get_foreground_host_activity().context: + bs.getsound(sound).play() + +accessible_online = None @threaded def confirm_port(): + global accessible_online time.sleep(5) with urlopen("https://legacy.ballistica.net/bsAccessCheck") as resp: resp = resp.read().decode() resp = ast.literal_eval(resp) - return resp["accessible"] + accessible_online = resp["accessible"] + # return resp["accessible"] @threaded def add_port_mapping(): + if accessible_online: + return # Try to add UDP port using NAT-PMP - import socket - import natpmp - from natpmp import NATPMPUnsupportedError, NATPMPNetworkError - try: - natpmp.map_port( - natpmp.NATPMP_PROTOCOL_UDP, - BS_PORT, - BS_PORT, - 14400, - gateway_ip=natpmp.get_gateway_addr(), - ) - if confirm_port(): - babase.screenmessage( - "You are now joinable from the internet", (0.2, 1, 0.2) - ) - except (NATPMPUnsupportedError, NATPMPNetworkError): - import upnpy - from upnpy.exceptions import SOAPError - from urllib.error import HTTPError - - upnp = upnpy.UPnP() - devices = upnp.discover() - - if devices == []: - babase.screenmessage( - "Please enable upnp service on your router", (1.00, 0.15, 0.15) - ) - # bui.getsound('shieldDown').play() -> RuntimeError : Sound creation failed - return - - local_ip = ( - ( - [ - ip - for ip in socket.gethostbyname_ex(socket.gethostname())[2] - if not ip.startswith("127.") - ] - or [ - [ - (s.connect(("8.8.8.8", 53)), s.getsockname()[0], s.close()) - for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)] - ][0][1] - ] - ) - + ["no IP found"] - )[0] - try: - for upnp_dev in devices: - for service in upnp_dev.services: - if service in WAN_SERVICE_NAMES: - service = upnp_dev[service] - try: - result = service.GetSpecificPortMappingEntry( - NewRemoteHost="", NewExternalPort=BS_PORT, NewProtocol="UDP" - ) - if result and not confirm_port(): - if babase.do_once(): - babase.screenmessage( - "Oops seems like your network doesn't support upnp", - (1.0, 0.15, 0.15), - ) - babase.pushcall( - bui.getsound("error").play(), from_other_thread=True - ) - return - except SOAPError: - if confirm_port(): - return - service.AddPortMapping( - NewRemoteHost="", - NewExternalPort=BS_PORT, - NewProtocol="UDP", - NewInternalPort=BS_PORT, - NewInternalClient=str(local_ip), - NewEnabled="1", - NewPortMappingDescription="Bombsquad", - NewLeaseDuration=14400, - ) - if confirm_port(): - babase.screenmessage( - "You are now joinable from the internet", (0.2, 1, 0.2) - ) - bui.getsound("shieldUp").play() - except (SOAPError, HTTPError, UnicodeDecodeError): - babase.screenmessage('You will need to manualy add the port at the router :(') - - -@threaded -def delete_port_mapping(): - import socket - import natpmp - from natpmp import NATPMPUnsupportedError, NATPMPNetworkError - - try: - natpmp.map_port( - natpmp.NATPMP_PROTOCOL_UDP, - BS_PORT, - BS_PORT, - 0, - gateway_ip=natpmp.get_gateway_addr(), - ) - except (NATPMPUnsupportedError, NATPMPNetworkError): - import upnpy - from upnpy.exceptions import SOAPError - - upnp = upnpy.UPnP() - devices = upnp.discover() - - if devices == []: - return + import socket + import natpmp + from natpmp import NATPMPUnsupportedError, NATPMPNetworkError try: - for upnp_dev in devices: - for service in upnp_dev.services: - if service in WAN_SERVICE_NAMES: - service = upnp_dev[service] - service.DeletePortMapping( - NewRemoteHost="", NewExternalPort=BS_PORT, NewProtocol="UDP") - except: - pass + natpmp.map_port( + natpmp.NATPMP_PROTOCOL_UDP, + BS_PORT, + BS_PORT, + 14400, + gateway_ip=natpmp.get_gateway_addr(), + ) + if accessible_online: + babase.screenmessage( + "You are now joinable from the internet", (0.2, 1, 0.2) + ) + babase.pushcall( + babase.Call(play_sound, "shieldUp"), from_other_thread=True + ) + except (NATPMPUnsupportedError, NATPMPNetworkError): + import upnpclient + from upnpclient.soap import SOAPError + from urllib.error import HTTPError + + devices = upnpclient.discover() + + if devices == []: + babase.screenmessage( + "Please enable upnp service on your router", (1.00, 0.15, 0.15) + ) + babase.pushcall( + babase.Call(play_sound, "shieldDown"), from_other_thread=True + ) + return + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # connect() for UDP doesn't send packets + s.connect(("10.0.0.0", 0)) + local_ip = s.getsockname()[0] + s.close() + except: + pass + try: + for upnp_dev in devices: + for service in upnp_dev.services: + if service.name in WAN_SERVICE_NAMES: + try: + result = service.GetSpecificPortMappingEntry( + NewRemoteHost="", + NewExternalPort=BS_PORT, + NewProtocol="UDP", + ) + print(result["NewEnabled"]) + if result["NewEnabled"] and not accessible_online: + if babase.do_once(): + babase.screenmessage( + "Oops seems like your network doesn't support upnp", + (1.0, 0.15, 0.15), + ) + babase.pushcall( + babase.Call(play_sound, "shieldDown"), + from_other_thread=True, + ) + return + except SOAPError: + if accessible_online: + return + service.AddPortMapping( + NewRemoteHost="0.0.0.0", + NewExternalPort=BS_PORT, + NewProtocol="UDP", + NewInternalPort=BS_PORT, + NewInternalClient=local_ip, + NewEnabled="1", + NewPortMappingDescription="Bombsquad", + NewLeaseDuration=14400, + ) + babase.pushcall( + babase.Call(play_sound, "shieldUp"), + from_other_thread=True, + ) + except (SOAPError, HTTPError, UnicodeDecodeError): + babase.screenmessage( + "You will need to manualy port forward at the router :(" + ) + babase.pushcall(babase.Call(play_sound, "error"),from_other_thread=True,) + except ModuleNotFoundError: + pass # ba_meta export babase.Plugin class Joinable(babase.Plugin): def on_app_running(self) -> None: - get_modules() - if confirm_port(): + # try: + confirm_port() + if accessible_online: return else: - add_port_mapping() - - def on_app_shutdown(self) -> None: - delete_port_mapping() - - def on_app_pause(self) -> None: - delete_port_mapping() - + try: + import upnpclient + add_port_mapping() + except ImportError: + try: + install_path = Path(f"{getcwd()}/ba_data/python") + shutil.rmtree(f"{install_path}/upnpy") + shutil.rmtree(f"{install_path}/natpmp") + except FileNotFoundError: + get_modules() + def on_app_resume(self) -> None: + confirm_port() add_port_mapping() diff --git a/plugins/utilities/wave_emote.py b/plugins/utilities/wave_emote.py new file mode 100644 index 0000000..63e52dc --- /dev/null +++ b/plugins/utilities/wave_emote.py @@ -0,0 +1,50 @@ +# ba_meta require api 9 +#! Fix bug when the previous message is "hello" it will not trigger the wave emote on new round or game +import time + +import babase +import bascenev1 as bs + +last_len_msg = 0 # Initialize the global variable outside the function + +def wave_emote(): + global last_len_msg # To modify the global variable + + # Check if the players are in game first + try: + act_players = bs.get_foreground_host_activity().players + if not act_players: + return + except AttributeError: + # Except the attribute error if the player is in a server + return + + # Incase chats are empty or in replay + try: + lastmsg = bs.get_chat_messages()[-1] + except: + return + + # Perform a check to see if the player is playing|spectating + for player in act_players: + try: + if player.actor.node: + continue + except: + return + + # Check if the message contains "hello" + if len(bs.get_chat_messages()) != last_len_msg: + if act_players and "hello" in lastmsg: + for player in act_players: + if player.getname() in lastmsg: + # Trigger the wave emote + player.actor.node.handlemessage("celebrate_r", 1000) + + last_len_msg = len(bs.get_chat_messages()) + print(last_len_msg, "last_len_msg") + +# ba_meta export plugin +class brostos(babase.Plugin): + timer = bs.AppTimer(0.5, wave_emote, repeat=True) +