From 76263bb55223132e0105cb695a8be8d1b632df85 Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 10 Mar 2024 15:19:04 +0100 Subject: [PATCH 01/12] Introducing websockets - EARLY BETA, DO NOT USE THEM! Check the README for more information. --- README.md | 7 ++ src/PolsuAPI/api.py | 114 +++++++++++++++++++++++- src/PolsuAPI/client.py | 12 +++ src/PolsuAPI/objects/player.py | 35 ++++++++ src/components/logger.py | 2 - src/components/logs.py | 4 +- src/components/player.py | 153 +++++++++++++++++++++++++++------ src/overlay.py | 13 +-- src/plugins/plugin.py | 28 +++--- src/utils/skin.py | 20 +++-- 10 files changed, 326 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 18f7744..bea3bfc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ +> ![!WARNING] +> DO NOT USE THIS BRANCH! +> WEBSOCKETS ARE IN EARLY BETA AND REQUIRE A SPECIAL AUTHENTICATION ON THE API +> LAUNCHING THIS VERSION MIGHT RESULT IN A BAN FROM THE API +> +> PLEASE USE THE `main` BRANCH INSTEAD! +
Polsu diff --git a/src/PolsuAPI/api.py b/src/PolsuAPI/api.py index 0512e38..75e7b64 100644 --- a/src/PolsuAPI/api.py +++ b/src/PolsuAPI/api.py @@ -42,7 +42,7 @@ import traceback -from aiohttp import ClientSession, ContentTypeError +from aiohttp import ClientSession, ContentTypeError, WSMsgType from json import load @@ -144,6 +144,110 @@ async def login(self) -> User: self.logger.error(f"An error occurred while logging in!\n\nTraceback: {traceback.format_exc()} | {e}") return None + + async def WebSocketConnection(self, setWebSocket, callback, closed) -> None: + """ + Create a WebSocket connection + + :param setWebSocket: A function to set the WebSocket + :param callback: A function to call when a player is received + :param closed: A function to call when the WebSocket is closed + """ + self.logger.info(f"[WS] Creating a WebSocket connection...") + + try: + # Create a WebSocket connection + async with ClientSession() as session: + async with session.ws_connect( + url=f"{self.api}/internal/overlay/websocket", + headers=self.polsuHeaders, + timeout=1800, + receive_timeout=1800, + heartbeat=1800, + autoping=True, + autoclose=False + ) as ws: + if not ws.closed: + self.logger.info(f"[WS] Starting websocket connection...") + self.logger.debug(f"[WS] Sending handshake...") + + # Send a handshake + await ws.send_json( + { + "protocol": "PolsuWebSocketProtocol - 1.0", + "handshake": self.key + } + ) + + self.logger.debug(f"[WS] Handshake sent!") + + if not ws.closed: + # Wait for the handshake response + async for msg in ws: + try: + data = msg.json() + if data.get('success', False) and data.get('data', {}) == "Connection established.": + self.logger.info(f"[WS] New WebSocket connection established!") + break + else: + self.logger.debug(f"[WS] Couldn't create a WebSocket connection! {msg}") + break + except: + self.logger.debug(f"[WS] Couldn't create a WebSocket connection! {msg}") + break + + if not ws.closed: + # Set the WebSocket so the overlay can send messages + await setWebSocket(ws) + + self.logger.info(f"[WS] WebSocket connection established!") + + # Wait for messages + expired = False + + async for msg in ws: + if msg.type in (WSMsgType.CLOSED, WSMsgType.ERROR): + return + else: + try: + data = msg.json() + + if DEV_MODE: + self.logger.debug(f"[WS] Received data: {data}") + + if data.get("data", {}) != {}: + if data.get("success", False): + player = Player(data.get("data")) + player.manual = data.get("data", {}).get("player", {}).get("manual", False) + player.websocket = True + await callback(player) + else: + if isinstance(data.get("data", {}), str): + if data.get("data", {}) == "Expired websocket!": + self.logger.warning(f"[WS] WebSocket connection expired!") + expired = True + else: + self.logger.error(f"[WS] An error occurred while receiving data: {data.get('data', {})}") + + break + else: + f = open(f"{resource_path('src/PolsuAPI')}/schemas/nicked.json", mode="r", encoding="utf-8") + player = Player(load(f)) + player.username = data.get("data", {}).get("player", {}).get("username") + player.rank = f"§c{data.get('data', {}).get('player', {}).get('username')}" + player.manual = data.get("data", {}).get("player", {}).get("manual", False) + player.nicked = True + player.websocket = True + await callback(player) + except: + pass + except Exception as e: + self.logger.error(f"An error occurred while creating a WebSocket connection!\n\nTraceback: {traceback.format_exc()} | {e}") + + closed(expired) + + self.logger.info(f"[WS] WebSocket connection closed!") + async def get_stats(self, player: str) -> Player: """ @@ -322,15 +426,19 @@ async def load_skin(self, player: Player) -> None: if DEV_MODE: self.logger.info(f"GET skins.mcstats.com/face/{player.uuid}") + headers = { + "User-Agent": __header__["User-Agent"] + } + async with ClientSession() as session: - async with session.get(f"https://skins.mcstats.com/face/{player.uuid}", headers=__header__) as response: + async with session.get(f"https://skins.mcstats.com/face/{player.uuid}", headers=headers) as response: if response.status == 200: return await response.read() else: self.logger.error(f"An error occurred while getting the skin of {player.uuid} ({response.status})!") raise APIError except ContentTypeError: - raise APIError + return None except APIError: return None except Exception as e: diff --git a/src/PolsuAPI/client.py b/src/PolsuAPI/client.py index 05c9c3c..9d425b0 100644 --- a/src/PolsuAPI/client.py +++ b/src/PolsuAPI/client.py @@ -169,3 +169,15 @@ async def loadSkin(self, player: Pl) -> None: return await self.client.load_skin(player) except asyncio.TimeoutError: raise APIError + + + async def WebSocket(self, setWebSocket, callback, closed) -> None: + """ + Get a Player stats with a WebSocket connection + + :param setWebSocket: A setWebSocket function + :param callback: A callback function + :param closed: A closed function + :return: An instance of Pl, representing the Player stats + """ + await self.client.WebSocketConnection(setWebSocket, callback, closed) diff --git a/src/PolsuAPI/objects/player.py b/src/PolsuAPI/objects/player.py index 1c59eb7..5fbdb31 100644 --- a/src/PolsuAPI/objects/player.py +++ b/src/PolsuAPI/objects/player.py @@ -515,6 +515,9 @@ def __init__(self, data: dict) -> None: self._ping = Ping(data.get('ping')) self._local = None + self._manual = False + self._websocket = False + @property def data(self) -> dict: @@ -643,6 +646,38 @@ def local(self, value: LocalBlacklisted) -> None: """ self._local = value + + @property + def manual(self) -> bool: + """ + Whether the Player is manually requested or not + """ + return self._manual + + + @manual.setter + def manual(self, value: bool) -> None: + """ + Set whether the Player is manually requested or not + """ + self._manual = value + + + @property + def websocket(self) -> bool: + """ + Whether the Player was requested through the WebSocket or not + """ + return self._websocket + + + @websocket.setter + def websocket(self, value: bool) -> None: + """ + Set whether the Player was requested through the WebSocket or not + """ + self._websocket = value + def __repr__(self) -> str: return f"" diff --git a/src/components/logger.py b/src/components/logger.py index ee8ba51..84f3f90 100644 --- a/src/components/logger.py +++ b/src/components/logger.py @@ -64,8 +64,6 @@ def __init__(self): handler.setFormatter(formatter) self.logger.addHandler(handler) - self.logger.info('Logger Initialised\n\n') - def info(self, message: str) -> None: """ diff --git a/src/components/logs.py b/src/components/logs.py index bd27ac6..7fcc7d9 100644 --- a/src/components/logs.py +++ b/src/components/logs.py @@ -163,7 +163,7 @@ def who(self) -> None: self.autoWho = True active = get_active_window_title() - if any(client in active for client in CLIENT_NAMES): + if active and any(client in active for client in CLIENT_NAMES): keyboard.press_and_release('t') sleep(0.2) keyboard.write('/who') @@ -279,7 +279,7 @@ def readLogs(self) -> None: # If it's the first line after a player connects to Hypixel, the delivery command is executed elif self.connecting and "[CHAT] " in line: active = get_active_window_title() - if any(client in active for client in CLIENT_NAMES): + if active and any(client in active for client in CLIENT_NAMES): sleep(0.5) keyboard.press_and_release('t') sleep(0.3) diff --git a/src/components/player.py b/src/components/player.py index 5ad1bc7..9e69ed6 100644 --- a/src/components/player.py +++ b/src/components/player.py @@ -37,7 +37,6 @@ from ..PolsuAPI.objects.player import Player as Pl from ..utils.colours import Colours - import asyncio import traceback @@ -70,6 +69,11 @@ def __init__(self, win) -> None: self.loading = [] + self.websocket = WebSocket(self.client) + self.websocket.playerObject.connect(self.update) + self.websocket.start() + + def getPlayer(self, players: list, manual: bool = False) -> None: """ Get a player from the API @@ -118,40 +122,65 @@ def getPlayer(self, players: list, manual: bool = False) -> None: if len(new) == 1: self.win.logger.info(f"Requesting: {new[0]}.") - try: - self.threads[cleaned] = Worker(self.client, new[0], manual) - self.threads[cleaned].playerObject.connect(self.update) - self.threads[cleaned].start() + if self.websocket.websocket: + asyncio.run( + self.websocket.query( + [ + { + "player": new[0], + "manual": manual + } + ] + ) + ) + else: + try: + self.threads[cleaned] = Worker(self.client, new[0], manual) + self.threads[cleaned].playerObject.connect(self.update) + self.threads[cleaned].start() - self.win.plugins.broadcast("on_player_load", new[0]) - except: - self.win.logger.error(f"Error while loading a player ({new[0]}).\n\nTraceback: {traceback.format_exc()}") + self.win.plugins.broadcast("on_player_load", new[0]) + except: + self.win.logger.error(f"Error while loading a player ({new[0]}).\n\nTraceback: {traceback.format_exc()}") else: - if len(new) > 40: - nb_slice = 10 - elif len(new) > 20: - nb_slice = 6 + if self.websocket.websocket: + asyncio.run( + self.websocket.query( + [ + { + "player": p, + "manual": manual + } + for p in new + ] + ) + ) else: - nb_slice = 3 + if len(new) > 40: + nb_slice = 10 + elif len(new) > 20: + nb_slice = 6 + else: + nb_slice = 3 - slices = [new[i : i+nb_slice] for i in range(0, len(new), nb_slice)] + slices = [new[i : i+nb_slice] for i in range(0, len(new), nb_slice)] - for s in slices: - self.win.logger.info(f"Requesting: {s}.") + for s in slices: + self.win.logger.info(f"Requesting: {s}.") - uuid = str(uuid4()) - while uuid in self.threads: uuid = str(uuid4()) + while uuid in self.threads: + uuid = str(uuid4()) - try: - self.threads[uuid] = Worker(self.client, s, manual) - self.threads[uuid].playerObject.connect(self.update) - self.threads[uuid].start() + try: + self.threads[uuid] = Worker(self.client, s, manual) + self.threads[uuid].playerObject.connect(self.update) + self.threads[uuid].start() - for p in s: - self.win.plugins.broadcast("on_player_load", p) - except: - self.win.logger.error(f"Error while loading a player slice ({s}).\n\nTraceback: {traceback.format_exc()}") + for p in s: + self.win.plugins.broadcast("on_player_load", p) + except: + self.win.logger.error(f"Error while loading a player slice ({s}).\n\nTraceback: {traceback.format_exc()}") def loadPlayer(self, player: str, uuid: str) -> None: @@ -316,7 +345,7 @@ def __init__(self, client, query: Union[list, str], manual: bool = False) -> Non self.client = client self.query = query self.manual = manual - + def run(self) -> None: """ @@ -346,3 +375,73 @@ def run(self) -> None: self.playerObject.emit(None) except: self.playerObject.emit(False) + + +class WebSocket(QThread): + """ + The worker class, used to get players from the API + """ + playerObject = pyqtSignal(object) + + def __init__(self, client) -> None: + """ + Initialise the class + + :param client: The Polsu client + :param query: The query to use + :param manual: If the player is manually added + """ + super(QThread, self).__init__() + self.client = client + self.websocket = None + + + def run(self) -> None: + """ + Run the thread + """ + asyncio.run(self.client.player.WebSocket(self.setWebSocket, self.update, self.closed)) + + + async def setWebSocket(self, ws) -> None: + """ + Get a player from the websocket + + :param ws: The websocket + """ + self.websocket = ws + + + async def query(self, query: list) -> None: + """ + Query a player + + :param query: The query to use + """ + if self.websocket: + await self.websocket.send_json( + { + "query": query + } + ) + + + async def update(self, player: Pl) -> None: + """ + Insert a player into the table + + :param player: The player to update + """ + self.playerObject.emit(player) + + + def closed(self, expired: bool) -> None: + """ + Called when the websocket is closed + + :param expired: If the websocket expired + """ + self.websocket = None + + if expired: + self.start() diff --git a/src/overlay.py b/src/overlay.py index 16d2e53..011063e 100644 --- a/src/overlay.py +++ b/src/overlay.py @@ -32,7 +32,7 @@ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ """ from .PolsuAPI import User -from src import Menu, Notif, Settings, Logs, Player, loadThemes, openSettings, __version__, DEV_MODE +from src import Menu, Notif, Settings, Logs, Player, loadThemes, openSettings, __version__, DEV_MODE, PLUGINS_DEV_MODE from .components.theme import ThemeStyle from .components.logger import Logger from .components.rpc import openRPC, startRPC @@ -241,10 +241,13 @@ def __init__(self, logger: Logger) -> None: PluginWindow(self.ask), PluginPlayer(self.player), ) - self.plugins.load_plugins(self.pluginsConfig) - self.logger.info(f"There are {len(self.plugins.getPlugins())} plugins loaded.") - self.logger.debug(f"Plugins: {', '.join([plugin.__name__ for plugin in self.plugins.getPlugins()])}") - self.logger.debug("Plugins loaded!") + if PLUGINS_DEV_MODE: + self.plugins.load_plugins(self.pluginsConfig) + self.logger.info(f"There are {len(self.plugins.getPlugins())} plugins loaded.") + self.logger.debug(f"Plugins: {', '.join([plugin.__name__ for plugin in self.plugins.getPlugins()])}") + self.logger.debug("Plugins loaded!") + else: + self.logger.warning("You have disabled the plugins development mode! No plugins loaded.") def loginEnded(self, user: User) -> None: diff --git a/src/plugins/plugin.py b/src/plugins/plugin.py index 3c2bb7e..5620921 100644 --- a/src/plugins/plugin.py +++ b/src/plugins/plugin.py @@ -96,35 +96,35 @@ def __init__(self) -> None: Initialise the plugin """ pass - + def on_load(self) -> None: """ Called when the plugin is loaded """ raise NotImplementedError("on_load() is not implemented!") - + def on_unload(self) -> None: """ Called when the plugin is unloaded """ raise NotImplementedError("on_unload() is not implemented!") - + def on_login(self, user: User) -> None: """ Called when the user logs in """ raise NotImplementedError("on_login() is not implemented!") - + def on_logout(self, user: User) -> None: """ Called when the user logs out """ raise NotImplementedError("on_logout() is not implemented!") - + def on_search(self, player: str) -> None: """ @@ -138,7 +138,7 @@ def on_player_load(self, player: str) -> None: Called when the player is loaded """ raise NotImplementedError("on_player_load() is not implemented!") - + def on_player_insert(self, player: Player) -> None: """ @@ -152,56 +152,56 @@ def on_player_message(self, message: str) -> None: Called when the player sends a message """ raise NotImplementedError("on_player_message() is not implemented!") - + def on_message(self, message: str) -> None: """ Called when a message is sent """ raise NotImplementedError("on_message() is not implemented!") - + def on_join(self, player: str) -> None: """ Called when a player joins the game """ raise NotImplementedError("on_join() is not implemented!") - + def on_leave(self, player: str) -> None: """ Called when a player leaves the game """ raise NotImplementedError("on_leave() is not implemented!") - + def on_final_kill(self, player: str) -> None: """ Called when a player gets a final kill """ raise NotImplementedError("on_final_kill() is not implemented!") - + def on_final_death(self, player: str) -> None: """ Called when a player gets final killed """ raise NotImplementedError("on_final_death() is not implemented!") - + def on_who(self) -> None: """ Called when a player uses /who """ raise NotImplementedError("on_who() is not implemented!") - + def on_list(self) -> None: """ Called when a player uses /list """ raise NotImplementedError("on_list() is not implemented!") - + def on_game_start(self) -> None: """ diff --git a/src/utils/skin.py b/src/utils/skin.py index 9aa493a..f027bd3 100644 --- a/src/utils/skin.py +++ b/src/utils/skin.py @@ -74,8 +74,8 @@ def loadSkin(self, player: Player, count: int) -> None: :param count: The position of the player in the table """ try: - if player.username in self.cache: - self.setSkin(self.cache[player.username], player, count) + if player.uuid in self.cache: + self.setSkin(self.cache[player.uuid], player, count) else: button = QPushButton(self.table) button.setIcon(self.default) @@ -91,9 +91,13 @@ def loadSkin(self, player: Player, count: int) -> None: self.table.setCellWidget(row, 0, button) self.table.setItem(row, 0, TableSortingItem(count)) - self.threads[player.username] = Worker(player, self.win.player.client, self.default, count) - self.threads[player.username].update.connect(self.setSkin) - self.threads[player.username].start() + # FIXME: This is a temporary fix for the skin loading issue with players loaded via the websocket + if player.websocket: + return + + self.threads[player.uuid] = Worker(player, self.win.player.client, self.default, count) + self.threads[player.uuid].update.connect(self.setSkin) + self.threads[player.uuid].start() except: self.win.logger.critical(f"An error occurred while loading the skin of {player.username}!\n\nTraceback: {traceback.format_exc()}") @@ -114,7 +118,7 @@ def setSkin(self, icon: QIcon, player: Player, count: int, cache: bool = True) - """ try: if cache: - self.cache[player.username] = icon + self.cache[player.uuid] = icon button = QPushButton(self.table) button.setIcon(icon) @@ -153,6 +157,7 @@ def __init__(self, player, client: Polsu, default: QIcon, count: int) -> None: :param player: The player to load the skin :param client: The client to request the API :param default: The default icon + :param count: The position of the player in the table """ super(QThread, self).__init__() self.player = player @@ -160,8 +165,6 @@ def __init__(self, player, client: Polsu, default: QIcon, count: int) -> None: self.default = default self.count = count - self.icon = QIcon() - def run(self) -> None: """ @@ -180,5 +183,4 @@ def run(self) -> None: except: icon = self.default - self.icon = icon self.update.emit(icon, self.player, self.count) From ce411fbf11c0f470ba4e092bcaa79ed34c66027b Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 10 Mar 2024 15:20:29 +0100 Subject: [PATCH 02/12] README update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bea3bfc..830c509 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -> ![!WARNING] +> [!WARNING] > DO NOT USE THIS BRANCH! > WEBSOCKETS ARE IN EARLY BETA AND REQUIRE A SPECIAL AUTHENTICATION ON THE API > LAUNCHING THIS VERSION MIGHT RESULT IN A BAN FROM THE API From 9bc6be8ec2808af3506808031d89a59744664a02 Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 10 Mar 2024 15:39:14 +0100 Subject: [PATCH 03/12] Added the new environment variable to the .env.example file --- .env.example | 6 +++++- README.md | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 83b804c..85c5dcb 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,5 @@ -DEV_MODE=False \ No newline at end of file +# Enable or disable the development mode +DEV_MODE=False + +# Enable or disable plugins in development mode +PLUGINS_DEV_MODE=True diff --git a/README.md b/README.md index 830c509..04554dd 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ A Hypixel Bedwars Overlay in Python, 100% free and open source! Wakatime
- - + + # 📖 Table of Contents - [📝 About](#-about) From f5652a172a48d20fd056218dab2284c5b33d25d9 Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 10 Mar 2024 17:16:26 +0100 Subject: [PATCH 04/12] Hopefully fixed skin loading with websockets, seems to work --- src/PolsuAPI/api.py | 18 +++++++++++------- src/components/player.py | 34 ++++++++++++++++++++++++++++------ src/utils/log.py | 2 +- src/utils/quickbuy.py | 18 +++++++++++++++++- src/utils/skin.py | 22 ++++++++++++++++++++-- 5 files changed, 77 insertions(+), 17 deletions(-) diff --git a/src/PolsuAPI/api.py b/src/PolsuAPI/api.py index 75e7b64..ab2ca01 100644 --- a/src/PolsuAPI/api.py +++ b/src/PolsuAPI/api.py @@ -155,6 +155,9 @@ async def WebSocketConnection(self, setWebSocket, callback, closed) -> None: """ self.logger.info(f"[WS] Creating a WebSocket connection...") + # Wait for messages + expired = False + try: # Create a WebSocket connection async with ClientSession() as session: @@ -202,9 +205,6 @@ async def WebSocketConnection(self, setWebSocket, callback, closed) -> None: self.logger.info(f"[WS] WebSocket connection established!") - # Wait for messages - expired = False - async for msg in ws: if msg.type in (WSMsgType.CLOSED, WSMsgType.ERROR): return @@ -223,11 +223,15 @@ async def WebSocketConnection(self, setWebSocket, callback, closed) -> None: await callback(player) else: if isinstance(data.get("data", {}), str): - if data.get("data", {}) == "Expired websocket!": + if data.get("data", {}) == "Expired websocket!" or data.get("data", {}) == "Packet limit reached!": self.logger.warning(f"[WS] WebSocket connection expired!") expired = True + elif data.get("data", {}) == "Malformed JSON" or data.get("data", {}) == "Missing query" or data.get("data", {}) == "Invalid query": + self.logger.error(f"[WS] An error occurred while sending some data: {data.get('data', {})}") + elif data.get("data", {}) == "Too many players": + self.logger.error(f"[WS] Oops we reached the maximum amount of players!") else: - self.logger.error(f"[WS] An error occurred while receiving data: {data.get('data', {})}") + self.logger.error(f"[WS] An error occurred with the websocket connection: {data.get('data', {})}") break else: @@ -239,8 +243,8 @@ async def WebSocketConnection(self, setWebSocket, callback, closed) -> None: player.nicked = True player.websocket = True await callback(player) - except: - pass + except Exception as e: + self.logger.error(f"An error occurred while receiving a WebSocket message!\n\nTraceback: {traceback.format_exc()} | {e}") except Exception as e: self.logger.error(f"An error occurred while creating a WebSocket connection!\n\nTraceback: {traceback.format_exc()} | {e}") diff --git a/src/components/player.py b/src/components/player.py index 9e69ed6..92ec994 100644 --- a/src/components/player.py +++ b/src/components/player.py @@ -31,7 +31,7 @@ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ """ -from src import CACHE, DEV_MODE +from src import CACHE, DEV_MODE, USE_WEBSOCKET from ..PolsuAPI import Polsu from ..PolsuAPI.exception import APIError, InvalidAPIKeyError from ..PolsuAPI.objects.player import Player as Pl @@ -69,9 +69,12 @@ def __init__(self, win) -> None: self.loading = [] - self.websocket = WebSocket(self.client) - self.websocket.playerObject.connect(self.update) - self.websocket.start() + if USE_WEBSOCKET: + self.websocket = WebSocket(self.client) + self.websocket.playerObject.connect(self.update) + self.websocket.start() + else: + self.websocket = None def getPlayer(self, players: list, manual: bool = False) -> None: @@ -122,7 +125,7 @@ def getPlayer(self, players: list, manual: bool = False) -> None: if len(new) == 1: self.win.logger.info(f"Requesting: {new[0]}.") - if self.websocket.websocket: + if self.websocket and self.websocket.websocket: asyncio.run( self.websocket.query( [ @@ -143,7 +146,7 @@ def getPlayer(self, players: list, manual: bool = False) -> None: except: self.win.logger.error(f"Error while loading a player ({new[0]}).\n\nTraceback: {traceback.format_exc()}") else: - if self.websocket.websocket: + if self.websocket and self.websocket.websocket: asyncio.run( self.websocket.query( [ @@ -224,6 +227,22 @@ def setRPCPlayer(self, player: Pl) -> None: self.update(player) + def deleteWorker(self, player: str) -> None: + """ + Delete the worker + + :param player: The player to delete + """ + cleaned = player.lower() + + if cleaned in self.threads: + try: + self.threads[cleaned].terminate() + self.threads.pop(cleaned) + except: + self.win.logger.error(f"An error occurred while deleting the worker of {cleaned}!\n\nTraceback: {traceback.format_exc()}") + + def update(self, player: Pl, cache: bool = True) -> None: """ Insert a player into the table @@ -298,6 +317,9 @@ def update(self, player: Pl, cache: bool = True) -> None: self.win.settings.update("APIKey", "") + self.deleteWorker(player.cleaned) + + def getCache(self, player: str) -> Union[Pl, None]: """ Get a player from the cache diff --git a/src/utils/log.py b/src/utils/log.py index eb4f9c0..8fa09bc 100644 --- a/src/utils/log.py +++ b/src/utils/log.py @@ -55,7 +55,7 @@ def __init__(self, key: str, logger) -> None: """ super(QThread, self).__init__() self.client = Polsu(key, logger) - + def run(self): """ diff --git a/src/utils/quickbuy.py b/src/utils/quickbuy.py index 205d1fc..028af09 100644 --- a/src/utils/quickbuy.py +++ b/src/utils/quickbuy.py @@ -96,7 +96,21 @@ def run(self, player) -> None: self.win.logger.error(f"An error occurred while loading the quickbuy image!\n\nTraceback: {traceback.format_exc()}") - def setPixmap(self, player, pixmap: QPixmap, cache: bool = True) -> None: + def deleteWorker(self, player: Player) -> None: + """ + Delete the Worker + + :param player: The player + """ + if player.username in self.threads: + try: + self.threads[player.username].terminate() + self.threads.pop(player.username) + except: + self.win.logger.error(f"An error occurred while deleting the worker of {player.username}!\n\nTraceback: {traceback.format_exc()}") + + + def setPixmap(self, player: Player, pixmap: QPixmap, cache: bool = True) -> None: """ Callback function to set the pixmap of the quickbuy image @@ -121,6 +135,8 @@ def setPixmap(self, player, pixmap: QPixmap, cache: bool = True) -> None: self.win.quickbuyWindow.resize(pixmap.size().width(), pixmap.size().height()) self.win.quickbuyWindow.setFixedSize(pixmap.size().width(), pixmap.size().height()) self.win.quickbuyWindow.show() + + self.deleteWorker(player) else: self.win.notif.send( title="Error...", diff --git a/src/utils/skin.py b/src/utils/skin.py index f027bd3..a59892a 100644 --- a/src/utils/skin.py +++ b/src/utils/skin.py @@ -92,8 +92,8 @@ def loadSkin(self, player: Player, count: int) -> None: self.table.setItem(row, 0, TableSortingItem(count)) # FIXME: This is a temporary fix for the skin loading issue with players loaded via the websocket - if player.websocket: - return + #if player.websocket: + # return self.threads[player.uuid] = Worker(player, self.win.player.client, self.default, count) self.threads[player.uuid].update.connect(self.setSkin) @@ -105,6 +105,20 @@ def loadSkin(self, player: Player, count: int) -> None: def rgbaToHex(self, rgba): rgba = tuple(int(x) for x in rgba) return "#{:02X}{:02X}{:02X}{:02X}".format(*rgba) + + + def deleteWorker(self, uuid: str) -> None: + """ + Delete the worker + + :param uuid: The UUID of the player + """ + if uuid in self.threads: + try: + self.threads[uuid].terminate() + self.threads.pop(uuid) + except: + self.win.logger.error(f"An error occurred while deleting the worker of {uuid}!\n\nTraceback: {traceback.format_exc()}") def setSkin(self, icon: QIcon, player: Player, count: int, cache: bool = True) -> None: @@ -140,6 +154,10 @@ def setSkin(self, icon: QIcon, player: Player, count: int, cache: bool = True) - color.setAlpha(50) item = self.table.item(row, 0) item.setBackground(color) + + self.deleteWorker(player.uuid) + + self.win.logger.info(f"Skin of {player.username} has been set.") except: self.win.logger.critical(f"An error occurred while setting the skin of {player.username}!\n\nTraceback: {traceback.format_exc()}") From fd0c370c4815bdb46069622ffbab187622868165 Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 10 Mar 2024 17:16:38 +0100 Subject: [PATCH 05/12] New .env variable --- .env.example | 3 +++ src/__init__.py | 4 +++- version.rc | 6 +++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 85c5dcb..c97e1cb 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,6 @@ DEV_MODE=False # Enable or disable plugins in development mode PLUGINS_DEV_MODE=True + +# Rather or not to use WebSockets +USE_WEBSOCKET=True diff --git a/src/__init__.py b/src/__init__.py index 927ad2d..9392102 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -41,7 +41,7 @@ __title__ = "PolsuOverlay" __author__ = "Polsulpicien" __license__ = "GPL-3.0 License" -__version__ = "2.0.7" +__version__ = "2.0.8" __description__ = "Polsu's Overlay" __module__ = getcwd() @@ -67,6 +67,8 @@ load_dotenv('.env') DEV_MODE = True if environ.get("DEV_MODE", "False") == "True" else False +PLUGINS_DEV_MODE = True if environ.get("PLUGINS_DEV_MODE", "False") == "True" else False +USE_WEBSOCKET = True if environ.get("USE_WEBSOCKET", "False") == "True" else False # This will only change the local cache, not the API cache diff --git a/version.rc b/version.rc index 036d137..f0c78d5 100644 --- a/version.rc +++ b/version.rc @@ -2,8 +2,8 @@ VSVersionInfo( ffi=FixedFileInfo( - filevers=(2, 0, 7, 1), - prodvers=(2, 0, 7, 1), + filevers=(2, 0, 8, 1), + prodvers=(2, 0, 8, 1), mask=0x3f, flags=0x0, OS=0x4, @@ -18,7 +18,7 @@ VSVersionInfo( u'040904B0', [StringStruct(u'CompanyName', u'Polsu Development'), StringStruct(u'FileDescription', u'Polsu Overlay'), - StringStruct(u'FileVersion', u'2.0.7.1'), + StringStruct(u'FileVersion', u'2.0.8.1'), StringStruct(u'InternalName', u'Polsu Overlay'), StringStruct(u'LegalCopyright', u'Copyright © 2022 - 2024 Polsu Development'), StringStruct(u'OriginalFilename', u'Polsu Overlay.exe'), From 34f05034796fae951b4ca52f53cf06645e006f1f Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 10 Mar 2024 17:22:45 +0100 Subject: [PATCH 06/12] Allowing plugins to run /who --- src/components/logs.py | 12 +++++++----- src/plugins/logs.py | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/logs.py b/src/components/logs.py index 7fcc7d9..f989eb3 100644 --- a/src/components/logs.py +++ b/src/components/logs.py @@ -151,19 +151,21 @@ def leftGame(self) -> None: self.hideOverlayTimer = 0 - def who(self) -> None: + def who(self, force: False) -> None: """ Runs /who + + :param force: A boolean representing if the /who should be forced """ - if not self.autoWho: + if force or not self.autoWho: self.leftGame() self.reset() - if self.win.configWho: + if force or self.win.configWho: self.autoWho = True active = get_active_window_title() - if active and any(client in active for client in CLIENT_NAMES): + if force or active and any(client in active for client in CLIENT_NAMES): keyboard.press_and_release('t') sleep(0.2) keyboard.write('/who') @@ -171,7 +173,7 @@ def who(self) -> None: keyboard.press_and_release('enter') else: self.autoWho = False - + self.waitingForGame = True diff --git a/src/plugins/logs.py b/src/plugins/logs.py index 4cb138c..a489448 100644 --- a/src/plugins/logs.py +++ b/src/plugins/logs.py @@ -42,4 +42,5 @@ def __init__(self, logs: Logs) -> None: self.isWaitingForGame = logs.isWaitingForGame self.isInAParty = logs.isInAParty self.getPartyMembers = logs.getPartyMembers + self.who = logs.who \ No newline at end of file From 8b7b0f3ff9f793949b3bbdb7c337436f8e9ced56 Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 10 Mar 2024 17:27:28 +0100 Subject: [PATCH 07/12] Fixed websockets in prod --- src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__init__.py b/src/__init__.py index 9392102..a206c4f 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -68,7 +68,7 @@ DEV_MODE = True if environ.get("DEV_MODE", "False") == "True" else False PLUGINS_DEV_MODE = True if environ.get("PLUGINS_DEV_MODE", "False") == "True" else False -USE_WEBSOCKET = True if environ.get("USE_WEBSOCKET", "False") == "True" else False +USE_WEBSOCKET = True if environ.get("USE_WEBSOCKET", "False") == "True" else True # This will only change the local cache, not the API cache From e4796a6bce2076b2c0d5d91b7a8ebaf0a7834408 Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 10 Mar 2024 17:34:53 +0100 Subject: [PATCH 08/12] Oops broke the overlay --- src/components/logs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/logs.py b/src/components/logs.py index f989eb3..20c4f7b 100644 --- a/src/components/logs.py +++ b/src/components/logs.py @@ -151,7 +151,7 @@ def leftGame(self) -> None: self.hideOverlayTimer = 0 - def who(self, force: False) -> None: + def who(self, force: bool = False) -> None: """ Runs /who From c609719d928590cf29988138321b1a48acd927e6 Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 17 Mar 2024 08:59:32 +0100 Subject: [PATCH 09/12] Improved logs reading, using position instead of raw lines --- src/components/logs.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/components/logs.py b/src/components/logs.py index 20c4f7b..0f274ea 100644 --- a/src/components/logs.py +++ b/src/components/logs.py @@ -63,9 +63,9 @@ def __init__(self, win) -> None: """ self.win = win - self.oldString = "" + self.lastReadPosition = 0 self.timeIconIndex = 1 - self.error_sent = False + self.errorSent = False self.waitingForGame = False self.isInGame = False @@ -181,7 +181,8 @@ def task(self) -> None: """ The main task which reads the log file """ - if self.oldString == "": + # Check if the log file has been read before or not + if self.lastReadPosition == 0: self.readLogFile() else: try: @@ -190,24 +191,33 @@ def task(self) -> None: self.win.logger.error(f"Error while reading logs.\n\nTraceback: {traceback.format_exc()}") - def readLogFile(self) -> str: + def readLogFile(self) -> list[str]: """ Function which returns the new lines of the log file :return: A string containing the new lines of the log file """ try: - with open(self.win.configLogPath, "r+") as logFile: - contents = logFile.read() + new_lines = [] + with open(self.win.configLogPath, "r") as logFile: + logFile.seek(self.lastReadPosition) + + if self.lastReadPosition > 0: + for line in logFile: + lastNewlineIdx = line.rfind('\n') + cleaned = line[:lastNewlineIdx] + line[lastNewlineIdx + 1:] + new_lines.append(self.rawLine(cleaned)) + else: + for line in logFile: + new_lines.append(line) - new = contents[len(self.oldString):] - self.oldString = contents + self.lastReadPosition = logFile.tell() - self.error_sent = False + self.errorSent = False - return new + return new_lines except FileNotFoundError: - if not self.error_sent: + if not self.errorSent: self.win.notif.send( title="Warning!", message="The log file you are currently using isn't valid.\nGo to: Settings -> Client, and choose a valid client.", @@ -215,10 +225,10 @@ def readLogFile(self) -> str: ) # To avoid multiple notifications - self.error_sent = True - return "" + self.errorSent = True + return [] except: - return "" + return [] def rawLine(self, string: str) -> str: @@ -245,8 +255,7 @@ def readLogs(self) -> None: Function which detected players in the new lines added in the log file Automatically add them to the queue, to get their stats and display them on the overlay """ - line: str = self.readLogFile() - lines = self.rawLine(line).splitlines() + lines: list[str] = self.readLogFile() for l in lines: line = l.replace(" [System] ", "") From 65fbf0c74a9897a664a79ba61febe117be280836 Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 17 Mar 2024 08:59:53 +0100 Subject: [PATCH 10/12] Improved websockets & fixed https requests fallback --- src/PolsuAPI/api.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/PolsuAPI/api.py b/src/PolsuAPI/api.py index ab2ca01..837b5ea 100644 --- a/src/PolsuAPI/api.py +++ b/src/PolsuAPI/api.py @@ -223,26 +223,36 @@ async def WebSocketConnection(self, setWebSocket, callback, closed) -> None: await callback(player) else: if isinstance(data.get("data", {}), str): - if data.get("data", {}) == "Expired websocket!" or data.get("data", {}) == "Packet limit reached!": + if data.get("data", {}) == "Expired websocket!": self.logger.warning(f"[WS] WebSocket connection expired!") expired = True + + raise Exception("Expired websocket!") + elif data.get("data", {}) == "Packet limit reached!": + self.logger.error(f"[WS] Packet limit reached!") + expired = True + + raise Exception("Packet limit reached!") elif data.get("data", {}) == "Malformed JSON" or data.get("data", {}) == "Missing query" or data.get("data", {}) == "Invalid query": self.logger.error(f"[WS] An error occurred while sending some data: {data.get('data', {})}") elif data.get("data", {}) == "Too many players": self.logger.error(f"[WS] Oops we reached the maximum amount of players!") else: self.logger.error(f"[WS] An error occurred with the websocket connection: {data.get('data', {})}") - - break else: - f = open(f"{resource_path('src/PolsuAPI')}/schemas/nicked.json", mode="r", encoding="utf-8") - player = Player(load(f)) - player.username = data.get("data", {}).get("player", {}).get("username") - player.rank = f"§c{data.get('data', {}).get('player', {}).get('username')}" - player.manual = data.get("data", {}).get("player", {}).get("manual", False) - player.nicked = True - player.websocket = True - await callback(player) + if data.get("cause", None).startswith("Rate Limited"): + self.logger.error(f"[WS] Rate limited! {data.get('cause', None)}") + + raise Exception("Rate limited!") + else: + f = open(f"{resource_path('src/PolsuAPI')}/schemas/nicked.json", mode="r", encoding="utf-8") + player = Player(load(f)) + player.username = data.get("data", {}).get("player", {}).get("username") + player.rank = f"§c{data.get('data', {}).get('player', {}).get('username')}" + player.manual = data.get("data", {}).get("player", {}).get("manual", False) + player.nicked = True + player.websocket = True + await callback(player) except Exception as e: self.logger.error(f"An error occurred while receiving a WebSocket message!\n\nTraceback: {traceback.format_exc()} | {e}") except Exception as e: From a5d5bb7e0e9b97d3ec8ac3ae16cfeff5b7f11448 Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 17 Mar 2024 09:00:33 +0100 Subject: [PATCH 11/12] Minor changes, not showing full key in logs anymore --- main.py | 2 +- src/components/player.py | 5 ++++- src/overlay.py | 10 ++++++++-- src/plugins/window.py | 5 ++++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 2bf85d9..ab3c915 100644 --- a/main.py +++ b/main.py @@ -52,7 +52,7 @@ if getattr(sys, 'frozen', False): - import pyi_splash + import pyi_splash # type: ignore def run(window: Updater, logger: Logger) -> None: diff --git a/src/components/player.py b/src/components/player.py index 92ec994..3e1e33d 100644 --- a/src/components/player.py +++ b/src/components/player.py @@ -317,7 +317,8 @@ def update(self, player: Pl, cache: bool = True) -> None: self.win.settings.update("APIKey", "") - self.deleteWorker(player.cleaned) + if not isinstance(player, bool): + self.deleteWorker(player.cleaned) def getCache(self, player: str) -> Union[Pl, None]: @@ -465,5 +466,7 @@ def closed(self, expired: bool) -> None: """ self.websocket = None + print("Expired!", expired) if expired: + self.client.logger.debug("The websocket expired! Creating a new one...") self.start() diff --git a/src/overlay.py b/src/overlay.py index 011063e..b9f4587 100644 --- a/src/overlay.py +++ b/src/overlay.py @@ -128,6 +128,7 @@ def __init__(self, logger: Logger) -> None: self.logger.debug("Loading the Settings...") self.settings = Settings(self) conf = self.settings.loadConfig() + conf["APIKey"] = f"{conf.get('APIKey', '')[0:4]}XXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" self.logger.debug(f"Settings: {conf}") @@ -184,7 +185,7 @@ def __init__(self, logger: Logger) -> None: # Check Logs Task self.logger.debug("Loading the Check Logs Task...") checkLogsTask = QTimer(self) - checkLogsTask.setInterval(700) #1000 -> 1 sec | 0.7 sec + checkLogsTask.setInterval(100) #1000 -> 1 sec | 0.7 sec checkLogsTask.timeout.connect(self.logs.task) checkLogsTask.start() self.logger.debug(f"Check Logs Task active: {'yes' if checkLogsTask.isActive() else 'no'}") @@ -238,7 +239,12 @@ def __init__(self, logger: Logger) -> None: PluginLogs(self.logs), PluginAPI(self.player.client), PluginSettings(self.settings), - PluginWindow(self.ask), + PluginWindow( + self.ask, + self.mini, + self.maxi, + self.window + ), PluginPlayer(self.player), ) if PLUGINS_DEV_MODE: diff --git a/src/plugins/window.py b/src/plugins/window.py index 76912bb..a77945a 100644 --- a/src/plugins/window.py +++ b/src/plugins/window.py @@ -32,5 +32,8 @@ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ """ class PluginWindow: - def __init__(self, ask) -> None: + def __init__(self, ask, minimize, maximize, window) -> None: self.ask = ask + self.minimize = minimize + self.maximize = maximize + self.window = window From ed79e6b38f7969eb72bd8ea2ee11c71ecd7dda9f Mon Sep 17 00:00:00 2001 From: Polsulpicien <70817001+Polsulpicien@users.noreply.github.com> Date: Sun, 17 Mar 2024 09:05:09 +0100 Subject: [PATCH 12/12] Made the dev mode terminal warning bigger --- dev.py | 11 ++++++++++- src/components/player.py | 1 - 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/dev.py b/dev.py index 726d944..0ba1ade 100644 --- a/dev.py +++ b/dev.py @@ -87,6 +87,15 @@ else: logger.critical("You are running the development version of the overlay! However, you are not in development mode.") - print("You are running the development version of the overlay! However, you are not in development mode.") + print("\n") + print("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓") + print("┃ ┃") + print("┃ WARNING ! ┃") + print("┃ ┃") + print("┃ You are running the development version of the overlay! However, you are not in development mode. ┃") + print("┃ Please set the DEV_MODE variable to True in the environment file! Or run the main.py file. ┃") + print("┃ ┃") + print("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛") + print("\n") sys.exit(1) diff --git a/src/components/player.py b/src/components/player.py index 3e1e33d..9d7064f 100644 --- a/src/components/player.py +++ b/src/components/player.py @@ -466,7 +466,6 @@ def closed(self, expired: bool) -> None: """ self.websocket = None - print("Expired!", expired) if expired: self.client.logger.debug("The websocket expired! Creating a new one...") self.start()