310 lines
12 KiB
Python
Executable File
310 lines
12 KiB
Python
Executable File
import logging
|
|
import sys
|
|
|
|
|
|
from core.constants import *
|
|
|
|
from core.config import Config
|
|
from utils.request_manager import RequestManager
|
|
from utils.cipher_manager import CipherManager
|
|
from stomp_ws.stomp_manager import STOMPManager
|
|
from p2p.p2p_manager import P2PManager
|
|
|
|
if PLATFORM == WINDOWS:
|
|
import ctypes
|
|
elif PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
import fcntl
|
|
|
|
|
|
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
import pyfiglet
|
|
from cli.login import LoginForm
|
|
from cli.info import CustomDialog
|
|
from cli.tray import TaskbarPanel
|
|
from cli.message_box import MessageBox
|
|
from cli.echo import Echo
|
|
else:
|
|
from gui.login import LoginForm
|
|
from gui.info import CustomDialog
|
|
from gui.tray import TaskbarPanel
|
|
from gui.message_box import MessageBox
|
|
|
|
|
|
class Application:
|
|
def __init__(
|
|
self,
|
|
log_file_path=LOG_FILE_NAME,
|
|
data_file_path=DATA_FILE_NAME,
|
|
mutex_identifier=MUTEX_NAME,
|
|
):
|
|
try:
|
|
self.log_file_path = os.path.join(
|
|
get_program_files_directory(), log_file_path
|
|
)
|
|
self.data_file_path = os.path.join(
|
|
get_program_files_directory(), data_file_path
|
|
)
|
|
self.mutex_identifier = mutex_identifier
|
|
|
|
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
self.lock_file = None # File(lock) object
|
|
self.mutex_identifier = os.path.join(
|
|
get_program_files_directory(), self.mutex_identifier
|
|
)
|
|
|
|
self.config = Config(
|
|
file_name=self.data_file_path
|
|
) # Maintain a single configuration instance for the entire application lifecycle.
|
|
|
|
self.request_manager = RequestManager(self.config)
|
|
self.stomp_manager = STOMPManager(self.config)
|
|
self.p2p_manager = P2PManager(self.config)
|
|
self.cipher_manager = CipherManager(self.config)
|
|
except Exception as e:
|
|
CustomDialog(
|
|
f"An error occurred during application initialization: {e}",
|
|
msg_type="error",
|
|
).mainloop()
|
|
|
|
def setup_logging(self):
|
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
|
logging.basicConfig(
|
|
level=LOG_LEVEL,
|
|
format=LOG_FORMAT,
|
|
filename=self.log_file_path,
|
|
filemode="w",
|
|
)
|
|
|
|
def ensure_single_instance(self):
|
|
if PLATFORM == WINDOWS:
|
|
ctypes.windll.kernel32.CreateMutexW(None, False, self.mutex_identifier)
|
|
if ctypes.windll.kernel32.GetLastError() == 183: # ERROR_ALREADY_EXISTS
|
|
CustomDialog(
|
|
"Another instance of ClipCascade is already running.",
|
|
msg_type="warning",
|
|
).mainloop()
|
|
sys.exit(0)
|
|
elif PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
if PLATFORM == MACOS:
|
|
app_dir = get_program_files_directory()
|
|
if not os.path.exists(app_dir):
|
|
try:
|
|
os.makedirs(app_dir)
|
|
except Exception as e:
|
|
CustomDialog(
|
|
f"An error occurred while creating the directory '{app_dir}'. Error: {e}",
|
|
msg_type="error",
|
|
).mainloop()
|
|
sys.exit(1)
|
|
|
|
# Create the lock file
|
|
try:
|
|
self.create_lock_file()
|
|
except IOError:
|
|
run_anyway = MessageBox().askquestion(
|
|
"ClipCascade",
|
|
"Another instance of ClipCascade is already running. Do you want to run anyway?",
|
|
)
|
|
if run_anyway == "yes":
|
|
os.remove(self.mutex_identifier)
|
|
self.create_lock_file()
|
|
else:
|
|
self.lock_file = None
|
|
sys.exit(0)
|
|
|
|
def create_lock_file(self, path=None):
|
|
if path is None:
|
|
path = self.mutex_identifier
|
|
|
|
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
self.lock_file = open(path, "w")
|
|
fcntl.flock(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
|
|
def authenticate_and_connect(self):
|
|
# Attempt to connect with existing cookie
|
|
if self.config.data.get("cookie"):
|
|
ws_conn_successful, msg = self._get_ws_manager().connect()
|
|
if ws_conn_successful:
|
|
self._get_ws_manager().is_login_phase = False
|
|
return
|
|
|
|
# enable login form
|
|
used_saved_credentials = False
|
|
display_login_success_dialog = False
|
|
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
Echo("═" * 14 + "\n║ LOGIN FORM ║\n" + "═" * 14)
|
|
while True:
|
|
if (
|
|
self.config.data.get("cookie") is not None
|
|
and self.config.data["save_password"]
|
|
and self.config.data["cipher_enabled"] == False
|
|
and not used_saved_credentials
|
|
):
|
|
# Attempt to connect with password when using saved credentials
|
|
used_saved_credentials = True
|
|
else:
|
|
display_login_success_dialog = True
|
|
self.config.data["password"] = "" # Clear the password
|
|
login_form = LoginForm(
|
|
self.config,
|
|
on_quit_callback=(
|
|
None
|
|
if (PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI)
|
|
else lambda: sys.exit(0)
|
|
),
|
|
)
|
|
login_form.mainloop() # wait until login form is closed
|
|
raw_password = self.config.data[
|
|
"password"
|
|
] # Store the raw password temporarily for hashing
|
|
self.config.data["password"] = (
|
|
CipherManager.string_to_sha3_512_lowercase_hex(raw_password)
|
|
) # Hash the password
|
|
|
|
login_successful, msg_login, self.config.data["cookie"] = (
|
|
self.request_manager.login()
|
|
)
|
|
if login_successful:
|
|
self.config.data["csrf_token"] = self.request_manager.get_csrf_token()
|
|
self.config.data["server_mode"] = self.request_manager.get_server_mode()
|
|
if self.config.data["server_mode"] == "P2P":
|
|
self.config.data["stun_url"] = self.request_manager.get_stun_url()
|
|
self.config.data["maxsize"] = -1
|
|
self.config.data["websocket_url"] = Config.convert_to_websocket_url(
|
|
self.config.data["server_url"], WEBSOCKET_ENDPOINT_P2P
|
|
)
|
|
else:
|
|
self.config.data["stun_url"] = ""
|
|
self.config.data["maxsize"] = self.request_manager.maxsize()
|
|
self.config.data["websocket_url"] = Config.convert_to_websocket_url(
|
|
self.config.data["server_url"], WEBSOCKET_ENDPOINT
|
|
)
|
|
ws_conn_successful, msg = self._get_ws_manager().connect()
|
|
if ws_conn_successful:
|
|
self._get_ws_manager().is_login_phase = False
|
|
if self.config.data["cipher_enabled"]:
|
|
self.config.data["hashed_password"] = (
|
|
self.cipher_manager.hash_password(raw_password)
|
|
)
|
|
if not self.config.data["save_password"]:
|
|
self.config.data["password"] = ""
|
|
if display_login_success_dialog:
|
|
CustomDialog(
|
|
"Success! ClipCascade will now run in the task bar/menu bar.",
|
|
msg_type="success",
|
|
timeout=5000,
|
|
).mainloop()
|
|
break
|
|
else:
|
|
CustomDialog(
|
|
"Login successful but websocket connection failed. \nPlease check websocket-url\n"
|
|
+ msg,
|
|
msg_type="error",
|
|
).mainloop()
|
|
else:
|
|
CustomDialog("Login Failed\n" + msg_login, msg_type="error").mainloop()
|
|
|
|
raw_password = None # Clear the raw password
|
|
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
Echo("-" * 53)
|
|
|
|
def _get_ws_manager(self):
|
|
if self.config.data["server_mode"] == "P2P":
|
|
return self.p2p_manager
|
|
else:
|
|
return self.stomp_manager
|
|
|
|
def get_version_update_status(self) -> list:
|
|
"""
|
|
Checks for a new version of the application by comparing the current version
|
|
with the one available in a remote JSON file.
|
|
|
|
Returns:
|
|
list: [bool, str, str, str] - [Is new version available, latest version, current version, release URL]
|
|
"""
|
|
try:
|
|
response = RequestManager.get(VERSION_URL)
|
|
response_data = response.json()
|
|
if PLATFORM == WINDOWS:
|
|
key = "windows"
|
|
elif PLATFORM == MACOS:
|
|
key = "macos"
|
|
elif PLATFORM.startswith(LINUX):
|
|
if not LINUX_USE_CLI_UI:
|
|
key = "linux_gui"
|
|
else:
|
|
key = "linux_non_gui"
|
|
|
|
if response_data[key] != APP_VERSION:
|
|
return [True, response_data[key], APP_VERSION, RELEASE_URL]
|
|
except Exception as e:
|
|
logging.error(f"Error checking for new version: {e}")
|
|
return [False, "", APP_VERSION, RELEASE_URL]
|
|
|
|
def get_donation_url(self) -> str:
|
|
try:
|
|
metadata = self.request_manager.get_metadata()
|
|
if metadata is not None:
|
|
return metadata.get("funding", None)
|
|
except Exception as e:
|
|
logging.error(f"Error fetching metadata: {e}")
|
|
return None
|
|
|
|
def logoff_and_exit(self):
|
|
try:
|
|
self._get_ws_manager().disconnect()
|
|
self.request_manager.logout()
|
|
self.config.data["hashed_password"] = None
|
|
self.config.data["cookie"] = None
|
|
self.config.data["maxsize"] = None
|
|
self.config.data["password"] = ""
|
|
self.config.data["csrf_token"] = ""
|
|
self.config.save()
|
|
except Exception as e:
|
|
raise Exception(f"Error during logging off: {e}")
|
|
|
|
def banner(self):
|
|
if PLATFORM.startswith(LINUX) and LINUX_USE_CLI_UI:
|
|
Echo(pyfiglet.figlet_format(APP_NAME))
|
|
Echo("*" * 53)
|
|
Echo("Real-Time Clipboard Syncing".center(53))
|
|
Echo(GITHUB_URL.center(53))
|
|
Echo("*" * 53)
|
|
|
|
def run(self):
|
|
try:
|
|
self.banner()
|
|
self.setup_logging()
|
|
self.ensure_single_instance()
|
|
self.config.load()
|
|
self.authenticate_and_connect()
|
|
self.config.save()
|
|
update_available = self.get_version_update_status()
|
|
donation_url = self.get_donation_url()
|
|
|
|
sys_tray = TaskbarPanel(
|
|
on_connect_callback=self._get_ws_manager().manual_reconnect,
|
|
on_disconnect_callback=self._get_ws_manager().disconnect,
|
|
on_logoff_callback=self.logoff_and_exit,
|
|
new_version_available=update_available,
|
|
github_url=GITHUB_URL,
|
|
donation_url=donation_url,
|
|
ws_interface=self._get_ws_manager(),
|
|
config=self.config,
|
|
)
|
|
self._get_ws_manager().set_tray_ref(sys_tray)
|
|
sys_tray.run()
|
|
except Exception as e:
|
|
msg = f"An unexpected error has occurred: {e}"
|
|
logging.error(msg)
|
|
CustomDialog(
|
|
msg + "\nCheck logs in project directory", msg_type="error"
|
|
).mainloop()
|
|
finally:
|
|
self._get_ws_manager().disconnect()
|
|
if PLATFORM == MACOS or PLATFORM.startswith(LINUX):
|
|
if self.lock_file is not None:
|
|
fcntl.flock(self.lock_file, fcntl.LOCK_UN)
|
|
self.lock_file.close()
|
|
os.remove(self.mutex_identifier)
|