added clipcascade
This commit is contained in:
Executable
+206
@@ -0,0 +1,206 @@
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from .frame import Frame
|
||||
import websocket
|
||||
import logging
|
||||
|
||||
from core.constants import *
|
||||
|
||||
VERSIONS = "1.0,1.1"
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, url, headers={}, on_close_callback=None, sslopt=None):
|
||||
|
||||
self.url = url
|
||||
self._ws_sslopt = sslopt
|
||||
self.ws = websocket.WebSocketApp(self.url, headers)
|
||||
self.ws.on_open = self._on_open
|
||||
self.ws.on_message = self._on_message
|
||||
self.ws.on_error = self._on_error
|
||||
self.ws.on_close = self._on_close
|
||||
self.on_close_callback = on_close_callback
|
||||
|
||||
self.opened = False
|
||||
|
||||
self.connected = False
|
||||
|
||||
self.counter = 0
|
||||
self.subscriptions = {}
|
||||
|
||||
self._connectCallback = None
|
||||
self.errorCallback = None
|
||||
|
||||
def _connect(self, timeout=0):
|
||||
if self._ws_sslopt:
|
||||
thread = Thread(
|
||||
target=lambda: self.ws.run_forever(sslopt=self._ws_sslopt)
|
||||
)
|
||||
else:
|
||||
thread = Thread(target=self.ws.run_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
total_ms = 0
|
||||
while self.opened is False:
|
||||
time.sleep(0.25)
|
||||
total_ms += 250
|
||||
if 0 < timeout < total_ms:
|
||||
raise TimeoutError(f"Connection to {self.url} timed out")
|
||||
|
||||
def _on_open(self, ws_app, *args):
|
||||
self.opened = True
|
||||
|
||||
def _on_close(self, ws_app, *args):
|
||||
self.connected = False
|
||||
logging.debug("Whoops! Lost connection to " + self.ws.url)
|
||||
if self.on_close_callback is not None:
|
||||
self.on_close_callback()
|
||||
self._clean_up()
|
||||
|
||||
def _on_error(self, ws_app, error, *args):
|
||||
logging.debug(error)
|
||||
|
||||
def _on_message(self, ws_app, message, *args):
|
||||
if message == "\n": # If message is a newline, it's a heartbeat frame
|
||||
logging.debug("Received heartbeat frame")
|
||||
self.ws.send("\n") # Send a heartbeat back to the server
|
||||
logging.debug("Sent heartbeat frame")
|
||||
return
|
||||
|
||||
logging.debug("\n<<< " + str(message))
|
||||
frame = Frame.unmarshall_single(message)
|
||||
_results = []
|
||||
if frame.command == "CONNECTED":
|
||||
self.connected = True
|
||||
logging.debug("connected to server " + self.url)
|
||||
if self._connectCallback is not None:
|
||||
_results.append(self._connectCallback(frame))
|
||||
elif frame.command == "MESSAGE":
|
||||
|
||||
subscription = frame.headers["subscription"]
|
||||
|
||||
if subscription in self.subscriptions:
|
||||
onreceive = self.subscriptions[subscription]
|
||||
messageID = frame.headers["message-id"]
|
||||
|
||||
def ack(headers):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
return self.ack(messageID, subscription, headers)
|
||||
|
||||
def nack(headers):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
return self.nack(messageID, subscription, headers)
|
||||
|
||||
frame.ack = ack
|
||||
frame.nack = nack
|
||||
|
||||
_results.append(onreceive(frame))
|
||||
else:
|
||||
info = "Unhandled received MESSAGE: " + str(frame)
|
||||
logging.debug(info)
|
||||
_results.append(info)
|
||||
elif frame.command == "RECEIPT":
|
||||
pass
|
||||
elif frame.command == "ERROR":
|
||||
if self.errorCallback is not None:
|
||||
_results.append(self.errorCallback(frame))
|
||||
else:
|
||||
info = "Unhandled received MESSAGE: " + frame.command
|
||||
logging.debug(info)
|
||||
_results.append(info)
|
||||
|
||||
return _results
|
||||
|
||||
def _transmit(self, command, headers, body=None):
|
||||
out = Frame.marshall(command, headers, body)
|
||||
logging.debug("\n>>> " + out)
|
||||
self.ws.send(out)
|
||||
|
||||
def connect(
|
||||
self,
|
||||
login=None,
|
||||
passcode=None,
|
||||
headers=None,
|
||||
connectCallback=None,
|
||||
errorCallback=None,
|
||||
timeout=0,
|
||||
):
|
||||
|
||||
logging.debug("Opening web socket...")
|
||||
self._connect(timeout)
|
||||
|
||||
headers = headers if headers is not None else {}
|
||||
headers["host"] = self.url
|
||||
headers["accept-version"] = VERSIONS
|
||||
headers["heart-beat"] = "0,20000"
|
||||
|
||||
if login is not None:
|
||||
headers["login"] = login
|
||||
if passcode is not None:
|
||||
headers["passcode"] = passcode
|
||||
|
||||
self._connectCallback = connectCallback
|
||||
self.errorCallback = errorCallback
|
||||
|
||||
self._transmit("CONNECT", headers)
|
||||
|
||||
def disconnect(self, disconnectCallback=None, headers=None):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
# self._transmit("DISCONNECT", headers) # comment this
|
||||
self.ws.on_close = None
|
||||
self.ws.close()
|
||||
self._clean_up()
|
||||
|
||||
if disconnectCallback is not None:
|
||||
disconnectCallback()
|
||||
|
||||
def _clean_up(self):
|
||||
self.connected = False
|
||||
|
||||
def send(self, destination, headers=None, body=None):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
if body is None:
|
||||
body = ""
|
||||
headers["destination"] = destination
|
||||
return self._transmit("SEND", headers, body)
|
||||
|
||||
def subscribe(self, destination, callback=None, headers=None):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
if "id" not in headers:
|
||||
headers["id"] = "sub-" + str(self.counter)
|
||||
self.counter += 1
|
||||
headers["destination"] = destination
|
||||
self.subscriptions[headers["id"]] = callback
|
||||
self._transmit("SUBSCRIBE", headers)
|
||||
|
||||
def unsubscribe():
|
||||
self.unsubscribe(headers["id"])
|
||||
|
||||
return headers["id"], unsubscribe
|
||||
|
||||
def unsubscribe(self, id):
|
||||
del self.subscriptions[id]
|
||||
return self._transmit("UNSUBSCRIBE", {"id": id})
|
||||
|
||||
def ack(self, message_id, subscription, headers):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
headers["message-id"] = message_id
|
||||
headers["subscription"] = subscription
|
||||
return self._transmit("ACK", headers)
|
||||
|
||||
def nack(self, message_id, subscription, headers):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
headers["message-id"] = message_id
|
||||
headers["subscription"] = subscription
|
||||
return self._transmit("NACK", headers)
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
Byte = {"LF": "\x0A", "NULL": "\x00"}
|
||||
|
||||
|
||||
class Frame:
|
||||
|
||||
def __init__(self, command, headers, body):
|
||||
self.command = command
|
||||
self.headers = headers
|
||||
self.body = "" if body is None else body
|
||||
|
||||
def __str__(self):
|
||||
lines = [self.command]
|
||||
skipContentLength = "content-length" in self.headers
|
||||
if skipContentLength:
|
||||
del self.headers["content-length"]
|
||||
|
||||
for name in self.headers:
|
||||
value = self.headers[name]
|
||||
lines.append("" + name + ":" + value)
|
||||
|
||||
if self.body is not None and not skipContentLength:
|
||||
lines.append("content-length:" + str(len(self.body)))
|
||||
|
||||
lines.append(Byte["LF"] + self.body)
|
||||
return Byte["LF"].join(lines)
|
||||
|
||||
@staticmethod
|
||||
def unmarshall_single(data):
|
||||
lines = data.split(Byte["LF"])
|
||||
|
||||
command = lines[0].strip()
|
||||
headers = {}
|
||||
|
||||
# get all headers
|
||||
i = 1
|
||||
while lines[i] != "":
|
||||
# get key, value from raw header
|
||||
(key, value) = lines[i].split(":", 1)
|
||||
headers[key] = value
|
||||
i += 1
|
||||
|
||||
# set body to None if there is no body
|
||||
body = None if lines[i + 1] == Byte["NULL"] else lines[i + 1][:-1]
|
||||
|
||||
return Frame(command, headers, body)
|
||||
|
||||
@staticmethod
|
||||
def marshall(command, headers, body):
|
||||
return str(Frame(command, headers, body)) + Byte["NULL"]
|
||||
Executable
+161
@@ -0,0 +1,161 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
||||
from interfaces.ws_interface import WSInterface
|
||||
from stomp_ws.client import Client
|
||||
from core.config import Config
|
||||
from utils.cipher_manager import CipherManager
|
||||
from clipboard.clipboard_manager import ClipboardManager
|
||||
from utils.notification_manager import NotificationManager
|
||||
from utils.request_manager import RequestManager
|
||||
from utils.ssl_helper import websocket_sslopt_for_config
|
||||
from core.constants import *
|
||||
|
||||
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
||||
from cli.tray import TaskbarPanel
|
||||
else:
|
||||
from gui.tray import TaskbarPanel
|
||||
|
||||
|
||||
class STOMPManager(WSInterface):
|
||||
def __init__(self, config: Config, is_login_phase=True):
|
||||
self.config = config
|
||||
self.clipboard_manager = ClipboardManager(self.config)
|
||||
self.cipher_manager = CipherManager(self.config)
|
||||
self.notification_manager = NotificationManager(self.config)
|
||||
self.sys_tray: TaskbarPanel = None
|
||||
self.first_conn_lost = True
|
||||
self.is_login_phase = is_login_phase
|
||||
self.client = None
|
||||
self.is_connected = False
|
||||
self.disconnected = False
|
||||
self.is_auto_reconnecting = False
|
||||
|
||||
def set_tray_ref(self, sys_tray: TaskbarPanel):
|
||||
"""
|
||||
Sets the system tray reference.
|
||||
"""
|
||||
self.sys_tray = sys_tray
|
||||
self.clipboard_manager.set_tray_ref(sys_tray)
|
||||
|
||||
def get_total_timeout(self):
|
||||
"""
|
||||
Returns the total timeout value in milliseconds."""
|
||||
return (RECONNECT_WS_TIMER * 1000) + WEBSOCKET_TIMEOUT
|
||||
|
||||
def get_stats(self):
|
||||
return None
|
||||
|
||||
def connect(self) -> tuple[bool, str]:
|
||||
try:
|
||||
if self.is_connected:
|
||||
return True, ""
|
||||
self.client = Client(
|
||||
self.config.data["websocket_url"],
|
||||
headers={
|
||||
"Cookie": RequestManager.format_cookie(
|
||||
self.config.data["cookie"]
|
||||
)
|
||||
},
|
||||
on_close_callback=self._on_close,
|
||||
sslopt=websocket_sslopt_for_config(self.config),
|
||||
)
|
||||
self.client.connect(
|
||||
timeout=WEBSOCKET_TIMEOUT,
|
||||
connectCallback=lambda _: self.client.subscribe( # receive event
|
||||
destination=SUBSCRIPTION_DESTINATION,
|
||||
callback=self._receive,
|
||||
),
|
||||
)
|
||||
if self.disconnected:
|
||||
self.disconnect()
|
||||
return False, "Websocket disconnected"
|
||||
|
||||
# logging.info("Websocket connected")
|
||||
self.is_connected = True
|
||||
self.is_auto_reconnecting = False
|
||||
if not self.first_conn_lost:
|
||||
self.first_conn_lost = True
|
||||
self.notification_manager.notify(
|
||||
title=f"{APP_NAME}: WebSocket Connection Restored 🔗",
|
||||
message="Connection re-established",
|
||||
)
|
||||
|
||||
# send event
|
||||
self.clipboard_manager.on_copy(self.send)
|
||||
return True, "Websocket connected"
|
||||
except Exception as e:
|
||||
msg = f"Failed to connect websocket: {e}"
|
||||
logging.error(msg)
|
||||
return False, msg
|
||||
|
||||
def _on_close(self):
|
||||
self.is_connected = False
|
||||
# Auto Reconnect
|
||||
if not self.is_login_phase and not self.disconnected:
|
||||
self.is_auto_reconnecting = True
|
||||
if self.first_conn_lost:
|
||||
self.notification_manager.notify(
|
||||
title=f"{APP_NAME}: WebSocket Connection Lost ⛓️💥",
|
||||
message="Check your internet connection. Retrying...",
|
||||
)
|
||||
self.first_conn_lost = False
|
||||
time.sleep(RECONNECT_WS_TIMER) # seconds
|
||||
self.connect()
|
||||
|
||||
def send(self, payload: str, payload_type: str = "text"):
|
||||
try:
|
||||
if self.is_connected:
|
||||
if self.clipboard_manager.has_clipboard_changed(payload):
|
||||
if self.config.data["cipher_enabled"]:
|
||||
payload = CipherManager.encode_to_json_string(
|
||||
**self.cipher_manager.encrypt(payload)
|
||||
)
|
||||
body = json.dumps({"payload": payload, "type": payload_type})
|
||||
self.client.send(destination=SEND_DESTINATION, body=body)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to send data: {e}")
|
||||
|
||||
def _receive(self, frame: any) -> str:
|
||||
try:
|
||||
if self.is_connected:
|
||||
body = json.loads(frame.body)
|
||||
payload = body["payload"]
|
||||
payload_type = body.get("type", "text")
|
||||
if self.config.data["cipher_enabled"]:
|
||||
payload = self.cipher_manager.decrypt(
|
||||
**CipherManager.decode_from_json_string(payload)
|
||||
)
|
||||
|
||||
if self.clipboard_manager.has_clipboard_changed(payload):
|
||||
self.clipboard_manager.base64_to_clipboard(
|
||||
base64_string=payload, type_=payload_type
|
||||
)
|
||||
except json.decoder.JSONDecodeError:
|
||||
logging.error(
|
||||
"If cipher is enabled, please make sure it is enabled on all devices"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to receive data: {e}")
|
||||
|
||||
def manual_reconnect(self):
|
||||
if not self.is_auto_reconnecting:
|
||||
self.disconnected = False
|
||||
self.connect()
|
||||
|
||||
def disconnect(self):
|
||||
try:
|
||||
self.clipboard_manager.previous_clipboard_hash = 0
|
||||
self.disconnected = True
|
||||
self.first_conn_lost = True
|
||||
try:
|
||||
self.client.disconnect()
|
||||
self.is_connected = False
|
||||
logging.info("Websocket disconnected")
|
||||
except Exception as e:
|
||||
pass # silent catch
|
||||
self.clipboard_manager.stop()
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to disconnect websocket: {e}")
|
||||
Reference in New Issue
Block a user