From db8bfc0f44f8cd6153d84743da556251f4f4814f Mon Sep 17 00:00:00 2001 From: "dries.k" Date: Sat, 5 Aug 2023 16:36:12 +0200 Subject: [PATCH] remove websocket-client dependency in favor of QWebSocket --- gotify_tray/gotify/api.py | 74 ++++++++++------------ gotify_tray/gotify/listener.py | 99 +++++++++++++----------------- gotify_tray/gui/MainApplication.py | 44 +++++-------- requirements.txt | 1 - setup.py | 2 +- 5 files changed, 94 insertions(+), 126 deletions(-) diff --git a/gotify_tray/gotify/api.py b/gotify_tray/gotify/api.py index 64371ef..92953cd 100644 --- a/gotify_tray/gotify/api.py +++ b/gotify_tray/gotify/api.py @@ -1,8 +1,9 @@ import logging -from typing import Callable import requests +from PyQt6 import QtCore + from .listener import Listener from .models import ( GotifyApplicationModel, @@ -17,8 +18,9 @@ from .models import ( logger = logging.getLogger("gotify-tray") -class GotifySession(object): +class GotifySession(QtCore.QObject): def __init__(self, url: str, token: str): + super(GotifySession, self).__init__() self.session = requests.Session() self.update_auth(url.rstrip("/"), token) @@ -46,9 +48,6 @@ class GotifySession(object): class GotifyApplication(GotifySession): - def __init__(self, url: str, application_token: str): - super(GotifyApplication, self).__init__(url, application_token) - def push( self, title: str = "", message: str = "", priority: int = 0, extras: dict | None = None ) -> GotifyMessageModel | GotifyErrorModel: @@ -72,6 +71,23 @@ class GotifyApplication(GotifySession): class GotifyClient(GotifySession): + new_message = QtCore.pyqtSignal(GotifyMessageModel) + opened = QtCore.pyqtSignal() + closed = QtCore.pyqtSignal() + + def __init__(self, url: str, client_token: str): + self.listener = Listener(url, client_token) + + super(GotifyClient, self).__init__(url, client_token) + + self.listener.opened.connect(self.opened.emit) + self.listener.closed.connect(self.closed.emit) + self.listener.new_message.connect(self.new_message.emit) + + def update_auth(self, url: str | None = None, token: str | None = None): + super().update_auth(url, token) + self.listener.update_auth(url, token) + """ Application @@ -180,48 +196,26 @@ class GotifyClient(GotifySession): response = self._delete(f"/message/{message_id}") return None if response.ok else GotifyErrorModel(response) - def listen( - self, - opened_callback: (Callable[[], None]) | None = None, - closed_callback: Callable[[int, str], None] | None = None, - new_message_callback: Callable[[GotifyMessageModel], None] | None = None, - error_callback: Callable[[Exception], None] | None = None, - ): - def dummy(*args): - ... - - self.listener = Listener(self.url, self.token) - self.listener.opened.connect(lambda: self.opened_callback(opened_callback)) - self.listener.closed.connect(closed_callback or dummy) - self.listener.new_message.connect(new_message_callback or dummy) - self.listener.error.connect(error_callback or dummy) + def listen(self): self.listener.start() - def opened_callback(self, user_callback: Callable[[], None] | None = None): - self.reset_wait_time() - if user_callback: - user_callback() - def reconnect(self): - if not self.is_listening(): - self.listener.start() + self.listener.reconnect() - def stop_final(self): - self.listener.stop_final() + def quit(self): + """Close the listener and disconnect from the closed signal so it doesn't get reopened + """ + try: + self.listener.closed.disconnect() + except TypeError: + logger.error(f"listener.closed was already disconnected.") + self.listener.close() - def stop(self, reset_wait: bool = False): - if reset_wait: - self.reset_wait_time() - self.listener.stop() + def stop(self): + self.listener.close() def is_listening(self) -> bool: - return self.listener.running - - def increase_wait_time(self): - self.listener.increase_wait_time() - - def get_wait_time(self) -> int: - return self.listener.wait_time + return self.listener.is_connected() def reset_wait_time(self): self.listener.reset_wait_time() diff --git a/gotify_tray/gotify/listener.py b/gotify_tray/gotify/listener.py index f18d1a5..7cb10b6 100644 --- a/gotify_tray/gotify/listener.py +++ b/gotify_tray/gotify/listener.py @@ -1,42 +1,52 @@ import json import logging -import platform -import ssl -import websocket from PyQt6 import QtCore +from PyQt6 import QtNetwork, QtWebSockets -from .models import GotifyMessageModel, GotifyErrorModel +from .models import GotifyMessageModel logger = logging.getLogger("gotify-tray") -class Listener(QtCore.QThread): +class Listener(QtWebSockets.QWebSocket): new_message = QtCore.pyqtSignal(GotifyMessageModel) - error = QtCore.pyqtSignal(Exception) opened = QtCore.pyqtSignal() - closed = QtCore.pyqtSignal(int, str) + closed = QtCore.pyqtSignal() def __init__(self, url: str, client_token: str): super(Listener, self).__init__() - qurl = QtCore.QUrl(url.rstrip("/") + "/") - qurl.setScheme("wss" if qurl.scheme() == "https" else "ws") - qurl.setPath(qurl.path() + "stream") - qurl.setQuery(f"token={client_token}") + self.update_auth(url, client_token) - self.ws = websocket.WebSocketApp( - qurl.toString(), - on_message=self._on_message, - on_error=self._on_error, - on_open=self._on_open, - on_close=self._on_close, - ) + self.connected.connect(self._on_connect) + self.disconnected.connect(self._on_disconnect) + self.error.connect(self._on_error) + self.textMessageReceived.connect(self._on_message) - self.wait_time = 0 + self.reset_wait_time() - self.running = False + def update_auth(self, url: str, client_token: str): + self.qurl = QtCore.QUrl(url.rstrip("/") + "/") + self.qurl.setScheme("wss" if self.qurl.scheme() == "https" else "ws") + self.qurl.setPath(self.qurl.path() + "stream") + self.qurl.setQuery(f"token={client_token}") + + def start(self): + logger.debug("Opening connection.") + self.open(self.qurl) + + def stop(self): + logger.debug("Stopping listener.") + self.close() + + def reconnect(self): + self.increase_wait_time() + QtCore.QTimer.singleShot(self.wait_time * 1000, self.start) + + def is_connected(self) -> bool: + return self.state() == QtNetwork.QAbstractSocket.SocketState.ConnectedState def reset_wait_time(self): self.wait_time = 0 @@ -47,42 +57,17 @@ class Listener(QtCore.QThread): else: self.wait_time = min(self.wait_time * 2, 10 * 60) - def _on_message(self, ws: websocket.WebSocketApp, message: str): + def _on_connect(self): + logger.debug("Connection established.") + self.reset_wait_time() + self.opened.emit() + + def _on_disconnect(self): + logger.debug(f"Connection was closed: {self.closeCode()}.") + self.closed.emit() + + def _on_message(self, message: str): self.new_message.emit(GotifyMessageModel(json.loads(message))) - def _on_error(self, ws: websocket.WebSocketApp, error: Exception): - logger.error(f"websocket error: {error}") - self.error.emit(error) - - def _on_open(self, ws: websocket.WebSocketApp): - self.opened.emit() - self.reset_wait_time() - - def _on_close( - self, ws: websocket.WebSocketApp, close_status_code: int, close_msg: str - ): - self.closed.emit(close_status_code, close_msg) - - def stop_final(self): - def dummy(*args): - ... - - self.ws.on_close = dummy - self.ws.close() - self.running = False - - def stop(self): - logger.debug("Listener: stopping.") - self.ws.close() - self.running = False - - def run(self): - self.running = True - try: - if platform.system() == "Darwin": - self.ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) - else: - self.ws.run_forever() - finally: - logger.debug("Listener: stopped.") - self.running = False + def _on_error(self): + logger.error(f"Listener socker error: {self.errorString()}") diff --git a/gotify_tray/gui/MainApplication.py b/gotify_tray/gui/MainApplication.py index 4636bf4..9416937 100644 --- a/gotify_tray/gui/MainApplication.py +++ b/gotify_tray/gui/MainApplication.py @@ -81,18 +81,13 @@ class MainApplication(QtWidgets.QApplication): self.first_connect = True - self.gotify_client.listen( - opened_callback=self.listener_opened_callback, - closed_callback=self.listener_closed_callback, - new_message_callback=self.new_message_callback, - error_callback=self.listener_error_callback, - ) - self.watchdog = ServerConnectionWatchdogTask(self.gotify_client) self.link_callbacks() self.init_shortcuts() + self.gotify_client.listen() + if settings.value("watchdog/enabled", type=bool): self.watchdog.start() @@ -151,22 +146,17 @@ class MainApplication(QtWidgets.QApplication): self.get_missed_messages_task.success.connect(get_missed_messages_callback) self.get_missed_messages_task.start() - def listener_closed_callback(self, close_status_code: int, close_msg: str): - self.main_window.set_connecting() - self.tray.set_icon_error() - self.gotify_client.increase_wait_time() - QtCore.QTimer.singleShot(self.gotify_client.get_wait_time() * 1000, self.gotify_client.reconnect) - - def listener_error_callback(self, exception: Exception): + def listener_closed_callback(self): self.main_window.set_connecting() self.tray.set_icon_error() + self.gotify_client.reconnect() def reconnect_callback(self): + self.gotify_client.reset_wait_time() if not self.gotify_client.is_listening(): - self.gotify_client.listener.reset_wait_time() self.gotify_client.reconnect() else: - self.gotify_client.stop(reset_wait=True) + self.gotify_client.stop() def abort_get_messages_task(self): """ @@ -322,18 +312,12 @@ class MainApplication(QtWidgets.QApplication): settings_dialog.apply_settings() if settings_dialog.server_changed: - # Restart the listener - self.gotify_client.stop_final() + # Update the server parameters and trigger a listener restart self.gotify_client.update_auth( settings.value("Server/url", type=str), settings.value("Server/client_token", type=str), ) - self.gotify_client.listen( - new_message_callback=self.new_message_callback, - opened_callback=self.listener_opened_callback, - closed_callback=self.listener_closed_callback, - error_callback=self.listener_error_callback, - ) + self.gotify_client.stop() def tray_notification_clicked_callback(self): if settings.value("tray/notifications/click", type=bool): @@ -368,7 +352,11 @@ class MainApplication(QtWidgets.QApplication): self.messages_model.rowsInserted.connect(self.main_window.display_message_widgets) - self.watchdog.closed.connect(lambda: self.listener_closed_callback(0, 0)) + self.gotify_client.opened.connect(self.listener_opened_callback) + self.gotify_client.closed.connect(self.listener_closed_callback) + self.gotify_client.new_message.connect(self.new_message_callback) + + self.watchdog.closed.connect(self.listener_closed_callback) def init_shortcuts(self): self.shortcut_quit = QtGui.QShortcut( @@ -384,14 +372,16 @@ class MainApplication(QtWidgets.QApplication): self.lock_file.setStaleLockTime(0) return self.lock_file.tryLock() - def quit(self) -> None: + def quit(self): + logger.debug("Quit requested.") + self.main_window.store_state() self.tray.hide() self.lock_file.unlock() - self.gotify_client.stop_final() + self.gotify_client.quit() super(MainApplication, self).quit() sys.exit(0) diff --git a/requirements.txt b/requirements.txt index fd7ab96..3cde5bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ requests==2.31.0 -websocket-client==1.5.2 pyqt6==6.5.1 diff --git a/setup.py b/setup.py index 0aea885..5714bea 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ with open("version.txt", "r") as f: # What packages are required for this module to be executed? REQUIRED = [ - 'requests==2.31.0', 'pyqt6==6.5.1', 'websocket-client==1.5.2' + 'requests==2.31.0', 'pyqt6==6.5.1' ] # What packages are optional?