From f682858051f2f8c4f99ac27142400a099f7dc3ca Mon Sep 17 00:00:00 2001 From: punix Date: Wed, 4 Jun 2025 18:49:17 +0200 Subject: [PATCH] large update --- Changelog | 11 +- common_tools.py | 883 -------------------------------- install | 233 --------- lx-icons/128/download.png | Bin 0 -> 3715 bytes lx-icons/128/download_error.png | Bin 0 -> 2408 bytes lx-icons/128/log.png | Bin 0 -> 4566 bytes lx-icons/256/download.png | Bin 0 -> 7533 bytes lx-icons/256/download_error.png | Bin 0 -> 4662 bytes lx-icons/256/log.png | Bin 0 -> 10443 bytes lx-icons/32/download.png | Bin 0 -> 984 bytes lx-icons/32/download_error.png | Bin 0 -> 657 bytes lx-icons/32/log.png | Bin 0 -> 906 bytes lx-icons/32/wg_vpn.png | Bin 5909 -> 0 bytes lx-icons/48/download.png | Bin 0 -> 1443 bytes lx-icons/48/download_error.png | Bin 0 -> 906 bytes lx-icons/48/log.png | Bin 0 -> 1655 bytes lx-icons/64/download.png | Bin 0 -> 1799 bytes lx-icons/64/download_error.png | Bin 0 -> 1187 bytes lx-icons/64/log.png | Bin 0 -> 2015 bytes match_found.py | 3 +- settings | 9 - ssl_decrypt.py | 2 +- ssl_encrypt.py | 2 +- start_wg.py | 8 +- tunnel.py | 230 +++++++++ wg_start.service | 11 - wirepy.py | 37 +- wp_app_config.py | 17 +- 28 files changed, 274 insertions(+), 1172 deletions(-) delete mode 100755 common_tools.py delete mode 100755 install create mode 100644 lx-icons/128/download.png create mode 100644 lx-icons/128/download_error.png create mode 100644 lx-icons/128/log.png create mode 100644 lx-icons/256/download.png create mode 100644 lx-icons/256/download_error.png create mode 100644 lx-icons/256/log.png create mode 100644 lx-icons/32/download.png create mode 100644 lx-icons/32/download_error.png create mode 100644 lx-icons/32/log.png delete mode 100644 lx-icons/32/wg_vpn.png create mode 100644 lx-icons/48/download.png create mode 100644 lx-icons/48/download_error.png create mode 100644 lx-icons/48/log.png create mode 100644 lx-icons/64/download.png create mode 100644 lx-icons/64/download_error.png create mode 100644 lx-icons/64/log.png delete mode 100644 settings create mode 100644 tunnel.py delete mode 100644 wg_start.service mode change 100644 => 100755 wp_app_config.py diff --git a/Changelog b/Changelog index 8c67e92..b3414ab 100644 --- a/Changelog +++ b/Changelog @@ -7,23 +7,26 @@ My standard System: Linux Mint 22 Cinnamon - If Wire-Py already runs, prevent further start - for loops with lists replaced by List Comprehensions - ### Added -13-04-0725 +03-06-2025 + + - + ### Added +13-04-20255 - Installer update for Open Suse Tumbleweed and Leap - add symbolic link wirepy.py ### Added -09-04-0725 +09-04-2025 - Installer now with query and remove - Icons merged ### Added -07-04-0725 +07-04-2025 - Installers will support other systems again - Installer is now finished clean with wrong password diff --git a/common_tools.py b/common_tools.py deleted file mode 100755 index f9686fa..0000000 --- a/common_tools.py +++ /dev/null @@ -1,883 +0,0 @@ -""" Classes Method and Functions for lx Apps """ - -import getpass -import shutil -import signal -import base64 -import secrets -import subprocess -from subprocess import CompletedProcess, run -import re -import sys -import tkinter as tk -from typing import Optional, Dict, Any, NoReturn -import zipfile -from datetime import datetime -from pathlib import Path -from tkinter import ttk, Toplevel -from wp_app_config import AppConfig, Msg, logging -import requests - -# Translate -_ = AppConfig.setup_translations() - - -class CryptoUtil: - """ - This class is for the creation of the folders and files - required by Wire-Py, as well as for decryption - the tunnel from the user's home directory - """ - - @staticmethod - def decrypt(user) -> None: - """ - Starts SSL dencrypt - """ - if len([file.stem for file in AppConfig.CONFIG_DIR.glob("*.dat")]) == 0: - pass - else: - process: CompletedProcess[str] = run( - ["pkexec", "/usr/local/bin/ssl_decrypt.py", "--user", user], - capture_output=True, - text=True, - check=False, - ) - - # Output from Openssl Error - if process.stderr: - logging.error(process.stderr, exc_info=True) - - if process.returncode == 0: - logging.info("Files successfully decrypted...", exc_info=True) - else: - logging.error( - f"Error process decrypt: Code {process.returncode}", exc_info=True - ) - - @staticmethod - def encrypt(user) -> None: - """ - Starts SSL encryption - """ - process: CompletedProcess[str] = run( - ["pkexec", "/usr/local/bin/ssl_encrypt.py", "--user", user], - capture_output=True, - text=True, - check=False, - ) - - # Output from Openssl Error - if process.stderr: - logging.error(process.stderr, exc_info=True) - - if process.returncode == 0: - logging.info("Files successfully encrypted...", exc_info=True) - else: - logging.error( - f"Error process encrypt: Code {process.returncode}", exc_info=True - ) - - @staticmethod - def find_key(key: str = "") -> bool: - """ - Checks if the private key already exists in the system using an external script. - Returns True only if the full key is found exactly (no partial match). - """ - process: CompletedProcess[str] = run( - ["pkexec", "/usr/local/bin/match_found.py", key], - capture_output=True, - text=True, - check=False, - ) - if "True" in process.stdout: - return True - elif "False" in process.stdout: - return False - logging.error( - f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}", - exc_info=True, - ) - return False - - @staticmethod - def is_valid_base64(key: str) -> bool: - """ - Validates if the input is a valid Base64 string (WireGuard private key format). - Returns True only for non-empty strings that match the expected length. - """ - # Check for empty string - if not key or key.strip() == "": - return False - - # Regex pattern to validate Base64: [A-Za-z0-9+/]+={0,2} - base64_pattern = r"^[A-Za-z0-9+/]+={0,2}$" - if not re.match(base64_pattern, key): - return False - - try: - # Decode and check length (WireGuard private keys are 32 bytes long) - decoded = base64.b64decode(key) - if len(decoded) != 32: # 32 bytes = 256 bits - return False - except Exception as e: - logging.error(f"Error on decode Base64: {e}", exc_info=True) - return False - - return True - - -class LxTools: - """ - Class LinuxTools methods that can also be used for other apps - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - @staticmethod - def center_window_cross_platform(window, width, height): - """ - Centers a window on the primary monitor in a way that works on both X11 and Wayland - - Args: - window: The tkinter window to center - width: Window width - height: Window height - """ - # Calculate the position before showing the window - - # First attempt: Try to use GDK if available (works on both X11 and Wayland) - try: - import gi - - gi.require_version("Gdk", "3.0") - from gi.repository import Gdk - - display = Gdk.Display.get_default() - monitor = display.get_primary_monitor() or display.get_monitor(0) - geometry = monitor.get_geometry() - scale_factor = monitor.get_scale_factor() - - # Calculate center position on the primary monitor - x = geometry.x + (geometry.width - width // scale_factor) // 2 - y = geometry.y + (geometry.height - height // scale_factor) // 2 - - # Set window geometry - window.geometry(f"{width}x{height}+{x}+{y}") - return - except (ImportError, AttributeError): - pass - - # Second attempt: Try xrandr for X11 - try: - import subprocess - - output = subprocess.check_output( - ["xrandr", "--query"], universal_newlines=True - ) - - # Parse the output to find the primary monitor - primary_info = None - for line in output.splitlines(): - if "primary" in line: - parts = line.split() - for part in parts: - if "x" in part and "+" in part: - primary_info = part - break - break - - if primary_info: - # Parse the geometry: WIDTH x HEIGHT+X+Y - geometry = primary_info.split("+") - dimensions = geometry[0].split("x") - primary_width = int(dimensions[0]) - primary_height = int(dimensions[1]) - primary_x = int(geometry[1]) - primary_y = int(geometry[2]) - - # Calculate center position on the primary monitor - x = primary_x + (primary_width - width) // 2 - y = primary_y + (primary_height - height) // 2 - - # Set window geometry - window.geometry(f"{width}x{height}+{x}+{y}") - return - except (ImportError, IndexError, ValueError): - pass - - # Final fallback: Use standard Tkinter method - screen_width = window.winfo_screenwidth() - screen_height = window.winfo_screenheight() - - # Try to make an educated guess for multi-monitor setups - # If screen width is much larger than height, assume multiple monitors side by side - if ( - screen_width > screen_height * 1.8 - ): # Heuristic for detecting multiple monitors - # Assume the primary monitor is on the left half - screen_width = screen_width // 2 - - x = (screen_width - width) // 2 - y = (screen_height - height) // 2 - window.geometry(f"{width}x{height}+{x}+{y}") - - @staticmethod - def clean_files(tmp_dir: Path = AppConfig.TEMP_DIR, file: Path = None) -> None: - """ - Deletes temporary files and directories for cleanup when exiting the application. - - This method safely removes an optional directory defined by `AppConfig.TEMP_DIR` - and a single file to free up resources at the end of the program's execution. - All operations are performed securely, and errors such as `FileNotFoundError` - are ignored if the target files or directories do not exist. - :param tmp_dir: (Path, optional): Path to the temporary directory that should be deleted. - If `None`, the value of `AppConfig.TEMP_DIR` is used. - :param file: (Path, optional): Path to the file that should be deleted. - If `None`, no additional file will be deleted. - - Returns: - None: The method does not return any value. - """ - - if AppConfig.TEMP_DIR is not None: - shutil.rmtree(AppConfig.TEMP_DIR) - try: - if file is not None: - Path.unlink(file) - - except FileNotFoundError: - pass - - @staticmethod - def msg_window( - image_path: Path, - image_path2: Path, - w_title: str, - w_txt: str, - txt2: Optional[str] = None, - com: Optional[str] = None, - ) -> None: - """ - Creates message windows - - :param image_path2: - :param image_path: - AppConfig.IMAGE_PATHS["icon_info"] = Image for TK window which is displayed to the left of the text - AppConfig.IMAGE_PATHS["icon_vpn"] = Image for Task Icon - :argument w_title = Windows Title - :argument w_txt = Text for Tk Window - :argument txt2 = Text for Button two - :argument com = function for Button command - """ - msg: tk.Toplevel = tk.Toplevel() - msg.resizable(width=False, height=False) - msg.title(w_title) - msg.configure(pady=15, padx=15) - - # load first image for a window - try: - msg.img = tk.PhotoImage(file=image_path) - msg.i_window = tk.Label(msg, image=msg.img) - except Exception as e: - logging.error(f"Error on load Window Image: {e}", exc_info=True) - msg.i_window = tk.Label(msg, text="Image not found") - - label: tk.Label = tk.Label(msg, text=w_txt) - label.grid(column=1, row=0) - - if txt2 is not None and com is not None: - label.config(font=("Ubuntu", 11), padx=15, justify="left") - msg.i_window.grid(column=0, row=0, sticky="nw") - button2: ttk.Button = ttk.Button( - msg, text=f"{txt2}", command=com, padding=4 - ) - button2.grid(column=0, row=1, sticky="e", columnspan=2) - button: ttk.Button = ttk.Button( - msg, text="OK", command=msg.destroy, padding=4 - ) - button.grid(column=0, row=1, sticky="w", columnspan=2) - else: - label.config(font=("Ubuntu", 11), padx=15) - msg.i_window.grid(column=0, row=0) - button: ttk.Button = ttk.Button( - msg, text="OK", command=msg.destroy, padding=4 - ) - button.grid(column=0, columnspan=2, row=1) - - try: - icon = tk.PhotoImage(file=image_path2) - msg.iconphoto(True, icon) - except Exception as e: - logging.error(f"Error loading the window icon: {e}", exc_info=True) - - msg.columnconfigure(0, weight=1) - msg.rowconfigure(0, weight=1) - msg.winfo_toplevel() - - @staticmethod - def sigi(file_path: Optional[Path] = None, file: Optional[Path] = None) -> None: - """ - Function for cleanup after a program interruption - - :param file: Optional - File to be deleted - :param file_path: Optional - Directory to be deleted - """ - - def signal_handler(signum: int, frame: Any) -> NoReturn: - """ - Determines clear text names for signal numbers and handles signals - - Args: - signum: The signal number - frame: The current stack frame - - Returns: - NoReturn since the function either exits the program or continues execution - """ - - signals_to_names_dict: Dict[int, str] = dict( - (getattr(signal, n), n) - for n in dir(signal) - if n.startswith("SIG") and "_" not in n - ) - - signal_name: str = signals_to_names_dict.get( - signum, f"Unnamed signal: {signum}" - ) - - # End program for certain signals, report to others only reception - if signum in (signal.SIGINT, signal.SIGTERM): - exit_code: int = 1 - logging.error( - f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.", - exc_info=True, - ) - LxTools.clean_files(file_path, file) - logging.info("Breakdown by user...") - sys.exit(exit_code) - else: - logging.info(f"Signal {signum} received and ignored.") - LxTools.clean_files(file_path, file) - logging.error("Process unexpectedly ended...") - - # Register signal handlers for various signals - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGHUP, signal_handler) - - -class Tunnel: - """ - Class of Methods for Wire-Py - """ - - @staticmethod - def parse_files_to_dictionary( - directory: Path = None, filepath: str = None, content: str = None - ) -> tuple[dict, str] | dict | None: - data = {} - - if filepath is not None: - filepath = Path(filepath) - try: - content = filepath.read_text() - - # parse the content - address_line = next( - line for line in content.splitlines() if line.startswith("Address") - ) - dns_line = next( - line for line in content.splitlines() if line.startswith("DNS") - ) - endpoint_line = next( - line for line in content.splitlines() if line.startswith("Endpoint") - ) - private_key_line = next( - line - for line in content.splitlines() - if line.startswith("PrivateKey") - ) - - content = secrets.token_bytes(len(content)) - - # extract the values - address = address_line.split("=")[1].strip() - dns = dns_line.split("=")[1].strip() - endpoint = endpoint_line.split("=")[1].strip() - private_key = private_key_line.split("=")[1].strip() - - # Shorten the tunnel name to the maximum allowed length if it exceeds 12 characters. - original_stem = filepath.stem - truncated_stem = ( - original_stem[-12:] if len(original_stem) > 12 else original_stem - ) - - # save in the dictionary - data[truncated_stem] = { - "Address": address, - "DNS": dns, - "Endpoint": endpoint, - "PrivateKey": private_key, - } - - content = secrets.token_bytes(len(content)) - - except StopIteration: - pass - - elif directory is not None: - - if not directory.exists() or not directory.is_dir(): - logging.error( - "Temp directory does not exist or is not a directory.", - exc_info=True, - ) - return None - - # Get a list of all files in the directory - files = [file for file in AppConfig.TEMP_DIR.iterdir() if file.is_file()] - - # Search for the string in the files - for file in files: - try: - content = file.read_text() - # parse the content - address_line = next( - line - for line in content.splitlines() - if line.startswith("Address") - ) - dns_line = next( - line for line in content.splitlines() if line.startswith("DNS") - ) - endpoint_line = next( - line - for line in content.splitlines() - if line.startswith("Endpoint") - ) - - # extract values - address = address_line.split("=")[1].strip() - dns = dns_line.split("=")[1].strip() - endpoint = endpoint_line.split("=")[1].strip() - - # save values to dictionary - data[file.stem] = { - "Address": address, - "DNS": dns, - "Endpoint": endpoint, - } - - except Exception: - # Ignore errors and continue to the next file - continue - if content is not None: - content = secrets.token_bytes(len(content)) - if filepath is not None: - return data, truncated_stem - else: - return data - - @staticmethod - def get_active() -> str: - """ - Shows the Active Tunnel - """ - active = None - try: - process: CompletedProcess[str] = run( - ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show", "--active"], - capture_output=True, - text=True, - check=False, - ) - - active = next( - line.split(":")[0].strip() - for line in process.stdout.splitlines() - if line.endswith("wireguard") - ) - - if process.stderr and "error" in process.stderr.lower(): - logging.error(f"Error output on nmcli: {process.stderr}") - - except StopIteration: - active = None - except Exception as e: - logging.error(f"Error on nmcli: {e}") - active = None - - return active if active is not None else "" - - @staticmethod - def export() -> bool | None: - """ - This will export the tunnels. - A zipfile with the current date and time is created - in the user's home directory with the correct right - """ - now_time: datetime = datetime.now() - now_datetime: str = now_time.strftime("wg-exp-%m-%d-%Y-%H:%M") - - try: - AppConfig.ensure_directories() - CryptoUtil.decrypt(getpass.getuser()) - if len([file.name for file in AppConfig.TEMP_DIR.glob("*.conf")]) == 0: - - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_info"], - AppConfig.IMAGE_PATHS["icon_msg"], - Msg.STR["sel_tl"], - Msg.STR["tl_first"], - ) - return False - else: - wg_tar: str = f"{AppConfig.BASE_DIR}/{now_datetime}" - try: - shutil.make_archive(wg_tar, "zip", AppConfig.TEMP_DIR) - with zipfile.ZipFile(f"{wg_tar}.zip", "r") as zf: - if zf.namelist(): - - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_info"], - AppConfig.IMAGE_PATHS["icon_vpn"], - Msg.STR["exp_succ"], - Msg.STR["exp_in_home"], - ) - else: - logging.error( - "There was a mistake at creating the Zip file. File is empty." - ) - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], - Msg.STR["exp_err"], - Msg.STR["exp_zip"], - ) - return False - return True - except PermissionError: - logging.error( - f"Permission denied when creating archive in {wg_tar}" - ) - return False - - except zipfile.BadZipFile as e: - logging.error(f"Invalid ZIP file: {e}") - return False - except TypeError: - pass - except Exception as e: - logging.error(f"Export failed: {str(e)}") - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], - Msg.STR["exp_err"], - Msg.STR["exp_try"], - ) - return False - - finally: - LxTools.clean_files(AppConfig.TEMP_DIR) - AppConfig.ensure_directories() - - -# ConfigManager with caching -class ConfigManager: - """ - Universal class for managing configuration files with caching support. - - This class provides a general solution to load, save, and manage configuration - files across different projects. It uses a caching system to optimize access efficiency. - The `init()` method initializes the configuration file path, while `load()` and `save()` - synchronize data between the file and internal memory structures. - - Key Features: - - Caching to minimize I/O operations. - - Default values for missing or corrupted configuration files. - - Reusability across different projects and use cases. - - The class is designed for central application configuration management, working closely - with `ThemeManager` to dynamically manage themes or other settings. - """ - - _config = None - _config_file = None - - @classmethod - def init(cls, config_file): - """Initial the Configmanager with the given config file""" - cls._config_file = config_file - cls._config = None # Reset the cache - - @classmethod - def load(cls): - """Load the config file and return the config as dict""" - if not cls._config: - try: - lines = Path(cls._config_file).read_text(encoding="utf-8").splitlines() - cls._config = { - "updates": lines[1].strip(), - "theme": lines[3].strip(), - "tooltips": lines[5].strip() - == "True", # is converted here to boolean!!! - "autostart": lines[7].strip() if len(lines) > 7 else "off", - } - except (IndexError, FileNotFoundError): - # DeDefault values in case of error - cls._config = { - "updates": "on", - "theme": "light", - "tooltips": "True", # Default Value as string! - "autostart": "off", - } - return cls._config - - @classmethod - def save(cls): - """Save the config to the config file""" - if cls._config: - lines = [ - "# Configuration\n", - f"{cls._config['updates']}\n", - "# Theme\n", - f"{cls._config['theme']}\n", - "# Tooltips\n", - f"{str(cls._config['tooltips'])}\n", - "# Autostart\n", - f"{cls._config['autostart']}\n", - ] - Path(cls._config_file).write_text("".join(lines), encoding="utf-8") - - @classmethod - def set(cls, key, value): - """Sets a configuration value and saves the change""" - cls.load() - cls._config[key] = value - cls.save() - - @classmethod - def get(cls, key, default=None): - """Returns a configuration value""" - config = cls.load() - return config.get(key, default) - - -class ThemeManager: - """ - Class for central theme management and UI customization. - - This static class allows dynamic adjustment of the application's appearance. - The method `change_theme()` updates the current theme and saves - the selection in the configuration file via `ConfigManager`. - It ensures a consistent visual design across the entire project. - - Key Features: - - Central control over themes. - - Automatic saving of theme settings to the configuration file. - - Tight integration with `ConfigManager` for persistent storage of preferences. - - The class is designed to apply themes consistently throughout the application, - ensuring that changes are traceable and uniform across all parts of the project. - """ - - @staticmethod - def change_theme(root, theme_in_use, theme_name=None): - """Change application theme centrally""" - root.tk.call("set_theme", theme_in_use) - if theme_in_use == theme_name: - ConfigManager.set("theme", theme_in_use) - - -class GiteaUpdate: - """ - Calling download requests the download URL of the running script, - the taskbar image for the “Download OK” window, the taskbar image for the - “Download error” window, and the variable res - """ - - @staticmethod - def api_down(update_api_url: str, version: str, update_setting: str = None) -> str: - """ - Checks for updates via API - - Args: - update_api_url: Update API URL - version: Current version - update_setting: Update setting from ConfigManager (on/off) - - Returns: - New version or status message - """ - # If updates are disabled, return immediately - if update_setting != "on": - return "False" - - try: - response: requests.Response = requests.get(update_api_url, timeout=10) - response.raise_for_status() # Raise exception for HTTP errors - - response_data = response.json() - if not response_data: - return "No Updates" - - latest_version = response_data[0].get("tag_name") - if not latest_version: - return "Invalid API Response" - - # Compare versions (strip 'v. ' prefix if present) - current_version = version[3:] if version.startswith("v. ") else version - - if current_version != latest_version: - return latest_version - else: - return "No Updates" - - except requests.exceptions.RequestException: - return "No Internet Connection!" - except (ValueError, KeyError, IndexError): - return "Invalid API Response" - - @staticmethod - def download( - urld: str, - res: str, - image_path: Path = None, - image_path2: Path = None, - image_path3: Path = None, - image_path4: Path = None, - ) -> None: - """ - Downloads new version of wirepy - - :param urld: Download URL - :param res: Result filename - :param image_path: AppConfig.IMAGE_PATHS["icon_info"]: Image for TK window which is displayed to the left of the text - :param image_path2: AppConfig.IMAGE_PATHS["icon_vpn"]: Image for Task Icon - :param image_path3: AppConfig.IMAGE_PATHS["icon_error"]: Image for TK window which is displayed to the left of the text - :param image_path4: AppConfig.IMAGE_PATHS["icon_msg"]: Image for Task Icon - """ - try: - to_down: str = f"wget -qP {Path.home()} {" "} {urld}" - result: int = subprocess.call(to_down, shell=True) - if result == 0: - shutil.chown(f"{Path.home()}/{res}.zip", 1000, 1000) - - wt: str = _("Download Successful") - msg_t: str = _("Your zip file is in home directory") - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_info"], - AppConfig.IMAGE_PATHS["icon_vpn"], - wt, - msg_t, - ) - - else: - - wt: str = _("Download error") - msg_t: str = _("Download failed! Please try again") - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], - wt, - msg_t, - ) - - except subprocess.CalledProcessError: - - wt: str = _("Download error") - msg_t: str = _("Download failed! No internet connection!") - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], - wt, - msg_t, - ) - - -class Tooltip: - """Class for Tooltip - from common_tools.py import Tooltip - example: Tooltip(label, "Show tooltip on label") - example: Tooltip(button, "Show tooltip on button") - example: Tooltip(widget, "Text", state_var=tk.BooleanVar()) - example: Tooltip(widget, "Text", state_var=tk.BooleanVar(), x_offset=20, y_offset=10) - - info: label and button are parent widgets. - NOTE: When using with state_var, pass the tk.BooleanVar object directly, - NOT its value. For example: use state_var=my_bool_var, NOT state_var=my_bool_var.get() - """ - - def __init__( - self, - widget: Any, - text: str, - state_var: Optional[tk.BooleanVar] = None, - x_offset: int = 65, - y_offset: int = 40, - ) -> None: - """Tooltip Class""" - self.widget: Any = widget - self.text: str = text - self.tooltip_window: Optional[Toplevel] = None - self.state_var = state_var - self.x_offset = x_offset - self.y_offset = y_offset - - # Initial binding based on the current state - self.update_bindings() - - # Add trace to the state_var if provided - if self.state_var is not None: - self.state_var.trace_add("write", self.update_bindings) - - def update_bindings(self, *args) -> None: - """Updates the bindings based on the current state""" - # Remove existing bindings first - self.widget.unbind("") - self.widget.unbind("") - - # Add new bindings if tooltips are enabled - if self.state_var is None or self.state_var.get(): - self.widget.bind("", self.show_tooltip) - self.widget.bind("", self.hide_tooltip) - - def show_tooltip(self, event: Optional[Any] = None) -> None: - """Shows the tooltip""" - if self.tooltip_window or not self.text: - return - - x: int - y: int - cx: int - cy: int - - x, y, cx, cy = self.widget.bbox("insert") - x += self.widget.winfo_rootx() + self.x_offset - y += self.widget.winfo_rooty() + self.y_offset - - self.tooltip_window = tw = tk.Toplevel(self.widget) - tw.wm_overrideredirect(True) - tw.wm_geometry(f"+{x}+{y}") - - label: tk.Label = tk.Label( - tw, - text=self.text, - background="lightgreen", - foreground="black", - relief="solid", - borderwidth=1, - padx=5, - pady=5, - ) - label.grid() - - self.tooltip_window.after(2200, lambda: tw.destroy()) - - def hide_tooltip(self, event: Optional[Any] = None) -> None: - """Hides the tooltip""" - if self.tooltip_window: - self.tooltip_window.destroy() - self.tooltip_window = None diff --git a/install b/install deleted file mode 100755 index cb4818f..0000000 --- a/install +++ /dev/null @@ -1,233 +0,0 @@ -#!/bin/bash -NORMAL='\033[0m' -GREEN='\033[1;32m' -RED='\033[31;1;42m' -BLUE='\033[30;1;34m' - -install_file_with(){ - clear - mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \ - mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \ - systemctl --user enable wg_start.service >/dev/null 2>&1 - sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/ - if [ $? -ne 0 ] - then - systemctl --user disable wg_start.service - rm -r ~/.config/wire_py && rm -r ~/.config/systemd - exit 0 - else - sudo apt install python3-tk && \ - sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \ - sudo cp -fv match_found.py /usr/local/bin/ && \ - sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \ - sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \ - sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \ - sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy - sudo mkdir -p /usr/local/etc/ssl - if [ ! -f /usr/local/etc/ssl/pwgk.pem ] - then - sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 - fi - fi - } - -install_arch_d(){ - clear - mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \ - mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \ - systemctl --user enable wg_start.service >/dev/null 2>&1 - sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/ - if [ $? -ne 0 ] - then - systemctl --user disable wg_start.service - rm -r ~/.config/wire_py && rm -r ~/.config/systemd - exit 0 - else - sudo pacman -S --noconfirm tk python3 python-requests && \ - sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \ - sudo cp -fv match_found.py /usr/local/bin/ && \ - sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \ - sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \ - sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \ - sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy - sudo mkdir -p /usr/local/etc/ssl - if [ ! -f /usr/local/etc/ssl/pwgk.pem ] - then - sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 - fi - - fi - } - -install(){ - if grep -i 'debian' /etc/os-release > /dev/null 2>&1 - then - groups > /tmp/isgroup - if grep 'sudo' /tmp/isgroup - then - install_file_with - else - echo -e "$BLUE"The installer found that they are not in the group sudo."" - echo -e "with "$RED"su -"$BLUE" "they can enter the root shell in which they then"" - echo -e "enter "$GREEN""usermod -aG sudo $USER.""$BLUE"" - echo -e ""after logging in from the system, they can then run Wire-Py install again." $NORMAL" - read -n 1 -s -r -p $"Press Enter to exit" - clear - exit 0 - - fi - - elif grep -i 'mint\|ubuntu\|pop|' /etc/os-release > /dev/null 2>&1 - then - install_file_with - - elif grep -i 'arch' /etc/os-release > /dev/null 2>&1 - then - groups > /tmp/isgroup - clear - if grep 'wheel' /tmp/isgroup - then - install_arch_d - else - echo "The installer found that they are not in the group sudo." - echo "The sudoers file must be edited with" - echo -e "$RED""su -""$NORMAL" - echo -e "$GREEN"""EDITOR=nano visudo"""$NORMAL" - echo "Find the line:" - echo "## Uncomment to allow members of group wheel to execute any command" - echo "remove '#' on # %wheel ALL=(ALL) ALL and save the file" - echo -e "then enter "$GREEN"gpasswd -a $USER wheel.""$NORMAL" - echo "after logging in from the system, they can then run Wire-Py install again." - read -n 1 -s -r -p $"Press Enter to exit" - clear - exit 0 - - fi - - elif grep -i '|manjaro\|garuda\|endeavour|' /etc/os-release > /dev/null 2>&1 - then - install_arch_d - - elif grep -i 'fedora' /etc/os-release > /dev/null 2>&1 - then - clear - mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \ - mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \ - systemctl --user enable wg_start.service >/dev/null 2>&1 - sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/ - if [ $? -ne 0 ] - then - systemctl --user disable wg_start.service - rm -r ~/.config/wire_py && rm -r ~/.config/systemd - exit 0 - else - sudo dnf install python3-tkinter -y - sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \ - sudo cp -fv match_found.py /usr/local/bin/ && \ - sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \ - sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \ - sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \ - sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy - sudo mkdir -p /usr/local/etc/ssl - if [ ! -f /usr/local/etc/ssl/pwgk.pem ] - then - sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 - fi - - fi - elif grep -i 'suse' /etc/os-release > /dev/null 2>&1 - then - clear - mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \ - mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \ - systemctl --user enable wg_start.service >/dev/null 2>&1 - sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/ - if [ $? -ne 0 ] - then - systemctl --user disable wg_start.service - rm -r ~/.config/wire_py && rm -r ~/.config/systemd - exit 0 - else - sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \ - sudo cp -fv match_found.py /usr/local/bin/ && \ - sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \ - sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \ - sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \ - sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy - sudo mkdir -p /usr/local/etc/ssl - if [ ! -f /usr/local/etc/ssl/pwgk.pem ] - then - sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 - fi - if grep -i 'Tumbleweed' /etc/os-release > /dev/null 2>&1 - then - sudo zypper install python313-tk - else - sudo zypper install python36-tk - fi - - fi - - else - clear - echo $"Your System could not be determined." - echo - read -n 1 -s -r -p $"Press Enter to exit" - clear - exit 0 - - fi - #clear - read -n 1 -s -r -p $"Press Enter to exit" - clear - - } - -remove(){ - sudo rm -f /usr/local/bin/wirepy /usr/local/bin/wirepy.py /usr/local/bin/start_wg.py \ - /usr/local/bin/wp_app_config.py common_tools.py /usr/local/bin/ssl_encrypt.py \ - /usr/local/bin/ssl_decrypt.py /usr/local/bin/match_found.py - if [ $? -ne 0 ] - then - exit 0 - else - systemctl --user disable wg_start.service - rm -r ~/.config/wire_py && rm -r ~/.config/systemd - sudo rm /usr/share/applications/Wire-Py.desktop - sudo rm /usr/share/locale/de/LC_MESSAGES/languages/de/wirepy.mo - sudo rm -r /usr/local/etc/ssl - which syncpy >/dev/null - if [ $? -ne 0 ] - then - sudo rm -r /usr/share/icons/lx-icons && sudo rm -r /usr/share/TK-Themes - - fi - - echo - read -p "Press Enter to exit..." - - fi - - } - -which wirepy >/dev/null -if [ $? -eq 0 ] - then - echo "Do you want to update/reinstall or uninstall wirepy?" - echo - echo "Update/reinstall: press y, uninstall press r" - echo - read -n 1 -s -r -p "Cancel with any other key..." result - case $result in - [y]* ) clear; install; exit;; - [Y]* ) clear; install; exit;; - [j]* ) clear; install; exit;; - [J]* ) clear; install; exit;; - [r]* ) clear; remove; exit;; - [R]* ) clear; remove; exit;; - esac - clear -else - install - -fi \ No newline at end of file diff --git a/lx-icons/128/download.png b/lx-icons/128/download.png new file mode 100644 index 0000000000000000000000000000000000000000..15893504ea82e2d0ba21c9570de599bffbd8a2ff GIT binary patch literal 3715 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 literal 0 HcmV?d00001 diff --git a/lx-icons/128/log.png b/lx-icons/128/log.png new file mode 100644 index 0000000000000000000000000000000000000000..06de63c271cbc80db70f077159acb32ade5a07fd GIT binary patch literal 4566 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 EFFs4@kpKVy literal 0 HcmV?d00001 diff --git a/lx-icons/256/download.png b/lx-icons/256/download.png new file mode 100644 index 0000000000000000000000000000000000000000..614bf401453725a333a3e0c272b5c93023aab1d3 GIT binary patch literal 7533 zcmZX32UJtvviC_yAcPi?5&Cz!Uq$o|LDSw($6s0On zdJDZNg7hjN5Q<0-X*DGdNP-T(P=ONF{XdtYBi3!wacvg-?8QfKJhu_j&sASv*-f&B8+ zA5$mkymfC}qx%KrW#PZ*63uD}06b;7TAD_FK4cqGd!cQonhpTUXg~-LPiGoCg zoxmWj*SR^(#58yZYk8TP3&}Zes&+r~oBGCvvwhEAvBX&5zFOm}^G=KPrGcCNSvw7z zikpFWhNk`uWg)<2qVmSZRILIW)9JKM?Z3EA$eTQp4YTWrKhFyA2LHL$+@aUmD>iO8 zl>Z>zM0QXlD+ssfsW>(>Q=;%_((zyg_R2!uZCc{XmoIh_9N)O*UEZDN4qVBJ`dAW7 zddR_d2JJOKWl&^M``Dzy`rwA|q({`e0D7_IQ}FTQ8vCAj|sz~W>t?eg3&)dIVAYa$B0yi7+`^wjg9z?u}=kWVodMe zC*!*BNpN`iJ*0mh>@x8pfdYxW=T?3MVUivD7=s`!V;1PI<|ZRA{$&xi+4?cO{+ z23zU{CL}5?%|-EXOifKq&R)fyADo&h;te^N>o(pD2neVyN=;OHb3a{ersxSFaokR6 zLhYiawA-`_96DSC3{$*7`SNn|(`+;|?LQ^vntk42J$3 zjOk?bCvSI{?CM})hva4T#(R%_d@jxxp#+1xeSGRIgjr)krs0?t9ew>C%V{VEh9)2| znD*%}^$;xx{`v1v0ez2&Bz@*Wj z2;e3&jsFsd5Hq;(<_yBopwig)xs2Zp-uZWEHhfd4M^8)q@nfTkgeD)0pg#ucPX(^D zJijUm?4rQm1DJ57Y0=Vj8n7HXg!$(Yzm2@*!g1uHKQHa=2%gIXFv44fGe%vxO}4kssXNRktRLa9xbM7_>@& zSO9muB|Bz;U6bZsmM>lK~|4zJ;f1nl;4v8y0cc z-MsCWn~mWq@ejN$DhGY#W9c(skh{SB3-MMp>nGK}f)(yeBHEA<*LFCEoE@IORacLv zGEUpiWR~-mPnvXb>M9(suy{u%Y%{LYa+Acz6BS=K8)St-8B%+raB#%j*|6Ei(D;h^ z{SgZ9NS*DCJ2Y_@n;(p3pDrI7qqv1$>`-CxJ%k_xvIxvF)}z0NPu)S~Y8+fqfuW{ra@ZA5h*ezFc&GvV=IcPGnlssNT&II?zhA&+G=1@51OKaz&wOc$v5~+6M)kVQ>E0uK)bH z;2|6R-}}wV#e&-Wxvf3JeedyMdEr$cfQbMx2O`U_R+k*Bk$V(QYYGKt^Jk_A+{HCv z2AK#*0DBW^?Z#|h*=o0!Drx)u*%qFE#l!yv4jE@xD`$PD0yxb}%4OG82U;Vn4g=Ra zLgKkFAXwd+fKYMhz%F~^jE{xN>28POeb`khz^LMz7QQ@cFK9a2{LUjo02@jXLno%H zY_7Sr5+AP@;B5aN!-RTzCWb<39ivtlA2 zd4w-1-lvctlqxZITSRaR421Qk+%(@Qeptv#WKtIXl>bC9O@6S+Cud&FDX^y?fxc#6P-cd@`Qh>#2~Pm@@)Up#Va)0I>T(#`60udujw-qm>P{aIFTkRisVZ}sxMz+F?dYnZ=q zhgkq24=y(BJZexH2&0QYum*wLeRzG<8|b^F6pVb%v(Tb*nIQn6T`qv^iZ>GBQ9%+A z|2E2h@TX_UQA6gcj#(c5hY|lG(c{S~n*5f#GsSE3wtv6-C~M&_b_bjZb5@~kj*j^^ zK)V;JBHMOU7#$+7-2aPHv&|@iG=!Q3{Esiq-6->8fW6|iWsKeSeF}$ZTt7`mgj4TN z{To$klp=^CYY~WRttA3z*hi{UJU68OOf(DA;|rHb48Jw-u`iK*XKZxrLymIqP_aO7k(Z3Z8ZH>9}nEDLcFG;dfJk^uDj2U9VjDGC$<_Oe+M4 zq4Efl>++XQPUyA!_tg)D7gj)cJN~XPHD!>eMf*`^!Q3Lxq6KG) zy`~8(PFM67MXtLEN$&*E(+dPQZ{4N=6Ct-D)NDsc0Gri|^5M65wc|#9=sBkHCYnxp zdX%>2W5l`^^Vl{|QXb-&&T|(%6xSz)``Ot^;YM5>Yy?C6Hj`!3fsLakLKNV{5BtHT zHbecj(Henf$`>2RmYK-bIpc)crUWaUeh(D?_qh3Nkrw97%~p+dzp;hMzN z1DK*LLufvXn1|ijG-cKoRp*6eb0Q|+HpddH72>zcwpSckkML zX07l;mbVV4+Zj#hr&~DH6UtGITMWP8g<-(Ay@B9@oydRa=mx(I#ZvR?loa}iC8#tV z;{I0=*(+NMqeFiFwp|BLSPfIc6J@#E!tQag;h$cJ4)U3~^^j@d+z%%CapTWuFS&?R ztI32X`b$@@No1FDA#w>QC~9~kluNNgQ^#1R>mW~PPDygrGo}%ry8l21!ndg*c1Ea>_xCBV25M0vzaC0V;cX#*ABm`0XoN70Rbk@#;k~~2ad_S*6JKj z`kl4bJK4_2hz>KtK(`LYwIKqW8X(k5+sWP3W@;j^m*5ery4du?hm`?~+MkX&XT_`N zLKB_YoF^?k4!lVOi3NZdLC$`a5gKN1D$4JGY~AnALxk&Mj=s?0nyw@5;vEJD#{iP^ zJsAe259mk;D`gtLJ##LijIkxwr%jvui5|5U1!S%zpI+qploJ;wPmAM5nrwl=FX&*q z&8UQ_sNh9XI_~IvjKx4^|_RL&*;t;JiQk_S`T?KELN^G~?ernmJ z#cEvC0B0H^emOeT6RYzgNof>Z)@F_ah*A+yK-?KbYj`Z775`(72gQXAssTBdxGi?$ zs6H;;(9E*Q7lr_n7{-G@Tl+e6&d?_nWoUZH?+I@W~fR2??q>zH7 zBzv2ve)F#RyLt}R3p~1-bFemEEbz8PioTM4@?)c7_c^cZnO=dh7foN2^-*~eHxAxN zA95WS_>QDCG_q@o&TKYppFh|rUY7B6gt+#MXXl`E@$4Db;o4FO z0)bP0C({h0Rwsz7A)|cu#~zI`r??q2N-&R%lkHft{z-Rpc(cCP$pdu3XGU9?MtC+5 zqBiTwu+_g`q`mrG?ox*=Kz98EWb?zN@%62eA7e&EhBy}tXC&HADxm}0`E4hb<5$<} z9>`V7AJl^Fw}s$R=iaSisTn8_qu%}RwJC31HDx0$c{6+I7R?(~Q(iQ^dpD75y;#Z= z7)2@x(PG256h?!n_CW8L3X5mX|%;3fDMD#3D)`7jMxC&Sy7>~y{pD=(H#3P z!=EIB26k7K#Hfal$`vh&ykg6YD=8TIk`0ZCyz)kdd#IYkeImw>xH7byDAHtin*|iM zG@?COx?%dG1yd+RD)9)+Phx}2ia;ADfnm+SCr%5e7~;%1FKMd9MX{f~*s?EY>)xkfTa`-Grv3>IUXp`w4O9Wyr5nxkRX> zMGdmdj%{0u@r?33cVnJj-_?k5_8726w@c_(1_bW@_t!tGok~8eWGe^h&)_sIVbP!IRNpj zBhlr0Q#qSNPu58uyy4Ig=rr*HVPI4-ZTfbNF$Lr|Xj^g+e%Y^d9iujz&hbWyb~y+y z+kPUA-?_{2tC>;KVhpOh)N$OE58i{&AL-JLE_-L`#sx{X_lP<;H*O!B1^C7BpHXo%Th$*zaX2Y~%LR{%DuySo8sVA##yHc0LD}V@^ zR?fv%XV=8@10aAH^`MgPq(kZi_MnCOGlKCn2=Qxs^#b`RWoF%v`J`S4ZGUpNll{YF ztV~}0mgz%3leh(SrTD$eq*87dzhXW(vZE;$JQRG(kFRgkd}z?yBeaaZP&P$pHPnD# zHBIxO!&#gNG24avu|)U+O{;xR$m*MsLr2%d4EG0{nMQ}Fg7_-9x>V+bXXbIyK$Tmq zOaq!Qrksd-<79u93USFQfRk>hJUf1$`%#|{>r>TK{0a86(M*Yj#rT9Yd7M6%IrgTS zaZjX22I_oxfE0efoQ-9+BWUT<=8k&I z@Q1f~n@_Obg$za=)MV~~M2j|eEHCyuSvcoY`$>%q-Fu3DatpTx;7`}NjM?V-zz)zg z?eLuDF2JA9?UR9$Qg>nV-MvN#uj2$X;0 zOb>|Wor1wxk1W3vD(%0CKr@s*IqO~;zAu@1hkyD`cXS45En%|#qXC!po8GBY2JuAM zJIww(k6hrt;uBBX6zleJc1!3J3eW`%FthnfcWyXZO*2P#-TY!XP4k*bSU%3GG&kEF z!lgFGDC74O33C6+JWW0YVr@g-RW!6By%;s%OZpD{@x8;sXK#19uofo$EC(ad3276cAM8WPf`x zHl3ih*l&W9_WYqu&J;|%_|GGwEv@{RV+vc74Djj_NZN)K01}mbHx?~{OUf@xMrfO= zKtakr(mqQ!FEUb5Z{OU{?h$ttE*-m#t)3 zm@=3e!uoA+hSlJh(1-Yd@o+6;X#4>0^o<3s%G4NLEyf|0r%EBrCwe*6ymo>d~laN{6S4lC~ zMkNVT)&?0FGRlYe+Y6^}Qg5mB{xzd^x|CxPg}15eHlzua`zf@G=0}+=-40+kvI8Qf z6#zk64&JB3d-Fdt2>k{12Y)Y`FxpGyw%*Y>Tiz#GTj@=UFRUEn8Xb37fk(9@`Fj7I z2LUv8BiDceL1>RMeTG}0$J|lRQzSb=?|RXufig0{A1RIeX6h#4vUg7E*`d$9xX_)Y zzWK_DN8f}%0KT|bG5};J`5VNHPq<#C3rjOqy!Tx=r+eYFwkKBI41qVSJ^5K_c_hT~ z?AKJSuNhCd9kZ78s&Ec9k~x&?ih8z1jw_ff-foz@LIq~a?g*I&1tUjJ2NOX(FGF%% zMau&BABj_Gzd@{9Bbg%O%9CP4 z-M1u170v8XP5V?su<}*VC6PmX%h557Wq10vaL$GKQv;cetnNM1#LtUWlk+JQlAJ9c z504yAv0Z!}Kr;(Ms$8}Z$?>Y>EK%8*v5WP8=0T}(4K%g-P*BkE}goR<}~mV54wERLi@ytP)WcrERm`C1W-hKzVCQ%@&UWLfW;CB;Kqi=dn<2SB>{(EW{2aBBD1PXXzUW_sshxC!NhYMac_OB)jh0N z$zfb@@uzCzO9LD5sA+3o&9`%uPo`_G-p2)8jY*dtOzbH>EE*64)<%kCP1imyTv-QS zUn2azfuWJ&Y5!4H_Ej65F&dKJ=;ytk7BLqgv_NSXIWmCjhy1KK$>9JxJY0%=9*@*N zUV?O*qx0DG-m{41-MpZGuC`A;Q9_<*URcp^?cqt1%#vL*$+u-=Z=YJ^ zqM@PTRGXt#!#JA9L6>x*Tl6^|_%fWv7`3O~IjCh^>uvmZvFsUT%9k{5UzU{CTZUk3 z&n64CIFf69H!B}gXZuF_GK!E|cArfCU7hPxrLik+%f_w{e=PN-Uo$pNjcvdGr7pK> zNpz3fw#o+6pYVnW`o+y*d+WLDMCG;w2Gifv5PZy{wR~T#gCYkIKlQEWPv4w19OO~V|F@y|S zETJK?6{A$tU@S8Rvpu)(^Vjow{&@cRJ@>xuJ@P&AWe(_H%#-z!= zwF(wCs7p4HsCWIGe%3Zi`T_p7YPwGO=Ad16du$~R9@sX+fx)}8@@&3yi8EYk-fj!C zwiQFO^sMY`1)LuiV4ZM-Jv}{EUVSEZjjmzJ6;2v*?&qf#W97tspJosCtWg+NXV2O& z(XwaHp4~-byW#u-K8?P)B_TCD4%gZqs2t}PF!UL!TSqN-s~pl23uIc9rt26P5ty|x zD%J)1nHE1%7rr!3FCGD%sQoz)AJVasISPW>c6mL2Wqg$c?3;qnMByDGd*b@GcnM~e z>!Fc#=SK2yARl?V_o`A0EoOYodg+YZJ%^taBKsa`Tq)XL%Ze)zi!R6&F%&#&QRl&< zeX7O4tXD`#$WL=Cb)bJYtj(zZ1gxGVM35EYRQ~cB!y=04tLHwK+TAe~E|Qpc{(P1f z6HXSgdZU-KA-sf$8OE{+h3AM$o8)jUCxmkzJXntKjpmQo(T#|$+CagyPk6Lr;s(qY ziT+};u@wPqIAB|agIW+@@%AI*M~g3>k+oJFz>*h<7p>Wbjueg_7lmB^?MItY+uBdDHQHHbU(fjXwPJp${*yseW3JGfurwth9TdC*&?2C)>UmGT#EpcuIrbIAQwc!{-1jg!M88~tV3si6vcIVAF@`P-UBLu*AMH>lINXg_R9KgD&# zXc@c=nfZ{`3P!5j2D`ctX8Tf-r$ehkE^h_5Lvj|S0UQ9KKT#A702&Gr03-oGK!Nlh z7XKeA(eMM|;(>Y3$esLMN!oDGFIF&fB`V6D!eD=&mPaCo7Z;!J>AWKLNPiOy7M&u) zpPPGDR#sNm*f>_3I(W@3IXU?kdBlD5*4UTv00(AokBvEBsvRph#P2BD*q~Sch$x7p z1w0@zjn5bZrQpYcp&^G7_BRYLYkbLA!SP+J={kftP^y1Me(IoR zi37=nVxWkc+4H09wO+@N?562{@jyArngEzurUfqv{l!Xj2e{4KQiLVFUwMT`z~@iy z85Tl-w^H`<0d2ZvuQwyUV?1wOE7!rcv@-AVjp90-D@J^b{OFJY6U2I#o5 z_Z~n$yR6kdt2mMv11T8)kXE=8+)ncxNZk!22X#GXo=}~ElDAH@ehMEBa$s-6nJOSU z2O_`!ftF~#%*cx1U$FeYLFa$L@;^HgRi)h?+*~0DX3{=hL8>Q1==;`({oK&{a3*5e z(7I#Sv6TJPmGAmiEsJsJ+jvPyfHq$e{py{RMsEtkLH} z8Q@6u_4N*<^O@$#zhU>Fn`xmr*Hx_}CyNZQfN2?|R~5xL$4wvSM@rwfIB-ra6aQ5u z19M=9*ntd8L_xWO+9&1;DRAc zDH|;vot+X!SQp$i#$-zwk->=~(4bfo-5hJ{yjcnKzqmWkp)5?cRI(>;Xnj=wVsOeY zG?dvdpg3tP$Zat|6(8N~xi$LRNa>ku;~QTpDk{1Prx`ateL4qGLe|XdpuCNRc27+u zU#ct&*ys^KJ&yy4HMIFTx2!Yy?`vR_B)d;nHZdbvaFZK*P7Mc?i11lZ;Rn~L! zu{#$I#_NT$wLG>>7>%SR#?EgacI3Gn85keGX`C!6BG$4n7Ja zv}&J@i;I)nv&Z3h{&}^`r?SMr>>^bwo*1ah7l9WlbPjZ9s9gK{I=(+m=V9MzyzO&0 zT0l~TEa6D*Gvb8)vRMSgUj4dTV#C^?y&@&$_uN?RBRZ@hfR^%otLZq4H;jeCC|^Iw z7#fDfN4<3RSv>aJ@yrh3)4Wr%S17%^1cIc?!aa4ruJqeDYN@WRKFY$>3>sG+1kk=gwZ z^XJ2W1SHpXOnIn^(J-uQxgNJ7NKPd3j%-MQQ5|@!lFycy3>E9&y}3G&tt}Lc4~ih@ z@3*l>FsFx5{b??-eSLij>%tzcu7+6y-eF%yv*!}2x*F%{S#*LWaU#}8(7A39y1Yg=2VY)A*y{yM zCYjpqVY-EEm`)3MNFpF)dwF?j&O;!~p4P`tC+p$W;9+zQiHvn}>VN&(n2;qk$H%!M z=F9(VCZYy5VbU+4j;nLI!Hur7Zh?5e44Y<(VBoPCe)&75r9UpU;~V3a2ucVjxG4ti zF^e}oNi8tioUhG%|HtWqj*bNA#lbGcq*a4BrBx`Sv2yhhJ|ClEfPFZ zkwzaOez5DvDQl1x*A-J3!J{8iXfCd<%%`88S{}@x(3|QX&CY-D8!&pN_Dj)qghv<= zMyDeU!xuSAOH1+;M!D*(wb5z+hBCW~U6|JUOc%^4gT%n_@Rw2h`;fDomdF!=05*dr zh%0f%Y_4$l-D$F~Ce5n62cjFb?$oXp-dt~Quk|he8U4C}JC|UX<2P2Hh0KqWQQ95c z@{+P#^pV^lGHxVgZob2iI>@4roGR!42wvyw(IQvqEEWrcIffCzRHp0V_CM9??)VxX z@{D{X#lw)Hs*G8WoYdNxSZJ}Kan)$*iLMx#&guEF9Ou&D&l}v>*vKonOn&W=U3Z{6 z(SFGVH~8WGdyDvK*?jSsKRo}cre;YUBaq)IG(pL*S@cFW&#hEzXlfFiRi%+<@cXWq zezYckOa0WPg;7^mmziPy8ho`be|)#lYN8g!sbQG!Mtb=mlw)Pw+>(1sIpD&DuMO81 zzN}+E+WCF=AQ^R7_%R!T@HJM2C*}%*^ha5F|8Ihdm1qc<%X%syz8TQbUCyRH>9j_e!mtbngv)@>4ibIK;ub-S$e{) zTbi93O=RSAXzJzS&eC&3`8UUzyiCZ4-Gk-UvdWqxcUyPOP*9RXuk3H^brMB8o@&E# z2K?zTYOU&M6~n{Lt)-yPyBc?+mYSNbTSurOs#g(+r`C?MfO)AM|Lt42A86VMAG^9b zI9}{|CL?N8U6eh`Vel5;NC@oZ*R$(d#>#`fv!}6KDJiLgnHK9TaIfS{_95-fXuT2P zunA>p*JI)LER8v{96_m#YOneGGuTQQV#Q8Sqp*H`7|+}6l@vg-4187WvbX;^Mm5xS zQR(5$RjBv$ufg*51Pbw7Ut&~^(aI%~YOUb?!4#hHP$2iZ67r#MXqF%fGuNf#(GPj4x>w~@ty^<<) z`EF-wP*L2RT+l>82Il;?rMVR1^$X;Q9Y9vgz3!RF6vDX8-`^kU^Dh-Zb_v`KXssqR z#Yv9GtMDKOH0Aa62r8By{H*vP9ac!>uT%8IWD?)Gark61S<8Q-iFN4|O&~=e+AlS) ztgJk8IdqJjI{lAwt^|s^n2OWezI%LK_%gNN+`){2I8h0Rpj2wd<^s;2Kacd`7#GU; zF;*lFF_+%Grd5p}TKf@vY$tqx1@o=8G`6?5Z*Iw;{$$gLf*{hGERH}tygbNeJWlu= z6%rac)G%B|BKJ!7_pt!c&rdTn!$$r`X=!Qd$~u>uLP0q(6FKWqUL?!Zl$4EI+oGrL zWF18#iXQF_8dpY$ygHQj-Iv2tPfwSlCBCkXN1LK(!|L_#h@%e-hY|`Q z-?WC=p3IzP7QfS-W3zxeLYd0%i zWv5(hDunWZV}pZ(KO+jirzqd6BWQ2$)NGQ3H!D!lr2B)|E6t4%vbdzdw$DgLpiuWc}5# z)rl5Qv78ma)4Iob30{I|t0`ae`Smybp04HKz&3Wi^Awo`z1>66q?D}WSQBF#G z?sWy!Z2r74-PH7S;dD+f@7(;nacbGEwMWI54jqbJe7Op$D7XEQGage52-};yym0T8 z(bz}Rl;q^nLj=lH-IO7dzRyY?L(#dpoMiSTHZ2d!c@?GZUSEXvu2)FkofJuX?ZkZIpg!e|&vG!SlsQuU{i0 z%^yC9xVgK>`F7nZ3u6t~p!UUEA#Ge8v~zTgvkZ{Qjlw6Q2&;6NNPM(Da`O;<_{Tb1 z@9h~`85z!Qc72=cs=@L1djH`s+SWJ0lOnhI6^~c?{(ELzT+q;vd7AV?!pDyv^YZiKbz=;8X(xR%{JOYRY1J)1 zxuRTZ{t-GjIK(6+g_6<t%E==mp zW1aa{v~+Zyn;ROkZ*6bCfDLmJ8`7#54)7oC4c2GW>tDEVp=oc=&xxMrYu?e(q{|{)p-;|rlY%bUXQA@*=}J(WMuf~=H_HZMbX&(iA5{7CtVIVimBdf5sa=J zI90roQJ9Kbt;brCk&}CV@_xJ?pBa<%iHzN#aDasx6>d_ zSU)XOYp+dG4Aacx!_7e{^ifUlm_zy0=AgBvfdQ3x-1##_?Ck7q9UZEUj=U=q_0eTz zm(5(R5ZIh@y`BH(KqzVNG%0CwMh1)D(ix$Yv^3giX4#feRb^%6>E9hm`A-t3$Tv1N z!a_nqR)nZ^^d>PP)Q_?5;$FN00!>NQkHeonC3w}#WBFrZB6?-Tx#ehiSah`3pDu{a zfI|?KF&`Tf6L_iqfChIz**x}Cc$jj2#{R`m#DbPXyCh%If7j2=Ign9M=+qSt{<$nG zdxgA$ERhYb@nOJ7KEgz_mQi?F(ev>wzT_^4YbGY`gqV%3tuOW>hN?vd1V%CWLIl~DHRWT2J-jMDa`Jw+hLJA#n?Y3^%My@>P1&SMRqxS9Vj%jEi+HcFuQp( zvb|l~sr2k?qx`kKKPA#@^nZjgL=zoy6qk&DuB*^QP>q#1{>0XDNBI>ZPTnq0X}tITIs# z9n~H_6t(N_>{Qd$CA+}E!G%%r^6(J0!Y`A1{``4NeEiJv%k4BjM6SDUmbJ`Y%~0*e zjq^|vt2Xme6t{2RhAK1QPmuE4;Kf)4HwHpYIH^%iYG6&I12#7n`}9O>Xr9taq>_t^ zi+9FZUAsp6p}c(Fr86Re%t747!GRD^JnOd-sWU!0p$g@rpy zgQJ+y@gb0sQBvmo_g`We4;NRJE=M;;?!$)hp66Xb!L4P+NJ4FGZRbPsJfrZqI2wmy8ylOz0>W5$QiSEw z>W?2r4wjdQTHUm+Tv5Gt?Pc&-#`WYm6z&n8*gt>%q};DNIyxGvM`Ppui@ztv*#5UM zN$BR6zPI}MW!ea_3#5*P#NH)A!LG&iiCaNIL9WX~ms^}gQaCH=&Tt}4GIk}))ydh} z7t}N~g#J3r`qI+UD&=338DdpTE4;5IyF>gr>p68hXoQPg@p8+ zI<;A`W!7=PQiW5yKDkceVeEf8iV5XlXTzeRG*ndZ>`F>XS{D|0#bqGZ1fgMNWo11n zdz+W1?Y!bfghr#=OD7>60k4g6aK|@&!@b;Wf>kG9{TS=YH+}< zTX(|)y^sb@m>gvm4Vrr54cuMajw^zpEEmJRDyr^nm(DVH0KcoDn zSPo74{e}kQb9MDy=@^1QbC>P<;$n(3XUT?0OJ85p$LCY<*nNqa zt-m8;luGbG9ze|xz3BY>e3o5Siy~7Iflr@5zsi?ZfOx8NKK7}el*=!>uO^TYY3iU1^L+hVyivF%v~ynu|I@41t8xlfaFqu73u zmJdgK`=;{-A95MMPvGL7{^_Whn7Ftv0z}WV!~P7G7!R<~Ah+b{nV3#6@7uRH%W7*9 zH#fH)r;3Qfxvo^7gPqGa$>2kR)auj^9z5t=XKi(p@?FpE>!0Yb?MM`qa+`jdOT4+c z*?Wo|-bs_%^$hr&gPE1RJtqL08SNpDBxOu$>I=ae|49w8Dn5?*`P0;bf)hR)7aL3c zKRCOxGKnDw4-b!PR=s3ZL;Rvyg)Rin^rDQcELRTpTSo^qJ^d$stf+_x&En!>L~U&? zGX%=RPzwpGa6j%+($b4IloS*i%F2z;A-xRpuf^T{LP$vX&WVZyDKpj8)6>zw-mS3U zzKok6u+R9aT>?phd1C}T1(OqL`*?<53eJ}|I-o5Yz z05D?`LbtP0y-HuL_g2-y;@2xdtRT050NL*DZbWTmB^`HsxDXROjAU^Kygf}K?!0Ii7#URp0_5%V zuU<_cYr1|fC^(qwMd&LCiwS8EBzDmL#Q3<-`NR3u)%qhz!R{;P*aOxkQ73^70{(}i zTN0_v%eJ<*ZCzbAD}UEFW03atcHRGM3~AjqmrRapJh21t%kAH^db^wVty|fHkG>}- z(>FoW+4}rhhTH228;Wf-x4JrYc%KphYNAUp?l--O=Y#Sk6akWT%2ZvR2M}1BeRt(4J znUvIe--LykSsjR`AQ9}z9onJJ%Aumi0+rXw=%VO^s*u3WkGw0T<^mpBfH2L(L4nVn z5z0N<>1ixB%6Hvav(0qH{#8>~XBQNVEUWzZQFtO6a$`;8mY&e&c80uvjR>SL$R4}a zsLNh(CdV1V2WtA)uV3{3dU{dz9_eNiZF14?-wP}cm3?hg5El<=Y0)^&j&m2DU$D4w zBW0GAsWt24M~|Dv=u1gEJ3ibj2vSf`aDu>8{}-)9XHU>-eQ7~a8de|v<%>$oW9(7D zn--KZUTMDX&KkS(=%>aZ01|Kq9<9sr(Vk|K^NC5R7N#dVMiFzZJ9pmR-(m4BXI|d2;dw;_dzzDJiaakVEHD4pW}?iFXdQzI_;Fa7)9c8|BYe-vX$_R+=N*Vc zQxgII#fx0B`tI%@^wE_MM*hkJA{PDdmO-y0S6k; zS4+mdXm>SDO%6W3@G?m;F&pU0#y4He2IC2A;@#i~fOKEp3JSu5Ks^XsU07X>hVmAL zG}W?g1f;5ql= zl!=K+r<%E?C6kD#sN~aq7an$YWs%e}tCCizpfNIdU^K`o*g;0w+c$4i3J09utunH( zM5U!MwT$=m;m5bDN#c_<%ss@8jEr!_qtj$OpSQo6`2z^Sr0^)Y496Y+l|&O)(KT$kV3#mx8bqkKM2=QQ7HoH*K&>>n7A{`8Wm6~{U#;&u5GzGY2v zwz@${+kL;jV{gPu+o@Y&QRH=?qLZg8Oxo(!>5qqpXJBgDQsd3hH#R27aA;;~`fPG? zl6*HERHqwMg_xL_)X-J#GdM@1v$J#8zv=AxyFnfU1c5VB;1xXj*Gy-1m}hKc6qArp zF~was3+@hpCOs?bou{RrJ`K(1VAHzrhw3N3qz7`xlUTzIH3)CQw_T!0oxE48%cOsG z^4!Pk)|mFOWtndcc$vmsu-8|Z_+(q%FoX90x}7Q*L4J8xTa;gP8JxkJB(>8%mW&x! zIS`{V+i-ip`ez>xQmOE9Sxxhyi<8rro}QSQka=GwY4;{O zeHxeB5RW*M;77jZGwr*?(N+P5wBW$J0FvO4qQ11VB=9qD%Hp0MnNwVpD?Ps^0d`~JDhMv6BC5_dzhYM-XC5o4z9u){~v037m+t=Xew zl7SjI%J=UJl#{Ofs<@Y&oV=FL=yRpfpJ{?7Wkw*jVA6TB%*o$h?ArW7o6|!p{03=MBmGe*8$5b2BaU{5l(6 zZZ6RCXEP$Xxuxa8#fvXS(YG+Cku`S)-GWDe$>QY>D=%;kKyh(Y84nMS%PRt20+K)h zV69T_v&43iDY|jem`7cQs|yQ&^vqOLO5Sp1Ri^^yeL)6ZzG6$sBB!LO8LHon4~_ZW zVDaUPjI!Pk5PcB_nP@t%_x+J|E`#gvO};!aGdWnArVo4gn&5}!DwB;4Ux_5 zx`2J&J?QkMIR?DoTm?g}w1hZ6c(B?2TdAsl0?<egNO|7^fct*cWgIa=<{kdKc% z%E4_mP?o#_1Ae%jLx-k@M$h>8%7ASn$kMnK?9|uLlh>}uDH$2X%?eKfHM6=Q-j=bq z57#KX5_CdTLn&hS^<$7v9XRvqCYbpSB}I&4qM{s&M}ItImWRtjv$4c9h__+kK-@@h zt+ar-(5APubFsba8D&VA6e19ODq31$!H3W&9i5!kKl>s;4}SgnMsU$^$t8%uCIxiB zzU}~9;lSxyt8QXra5M9cWBv7ey6nGi7fXbB(uPJ1}ryT{-U>$NWs4T8Nt zG0Iod)+WJhh|!Z-nTWStUs_#Xch@<*8iNQB`47AXn%pv0TvW9A$`?_las{Way?+8K z^W$zP8_HY=ObvGy+{S&jmQ&0gX+XA}SXmy_Bs77)X=rIVZ-{I0Pdm#mtgIv+2!J=& z$xjGd6Ue*l!va3PE=BFslx@j7M$V)?e`qn_VJTK&A9`DvC4u%s=EiEEjo9`LLq@;LL_GIPd(~Z#Jk7aOP zxPa%sy>ff`=FOY*jEokTZae)$!Lx_nOZ|K&chlq<@i9;e3k!o2oFQ&ak$YS!jNSOT zZ{KnV2y`|oK(ho}Ts)JAo|(BBe18UsTU%zHMe&hRdr$g}exBl9-3~J<1!`SPd1f{UFbogrB zepK1s(PXq#gX|WT3Afox6_)KJ@0pWe9-}6^Plv66@A@9(~h8ENM_6 zFnx$Fa9{52Oo;AP=*{*(aOD%^yE;1~a`*M~>mjP*uZW<#gx1&B2M2g~&=wUH71l91 z3e)#MRt`OMcR#7;bDZ4V-%QCrmYx&5!X_nYrlO&_RoD*}-{j@x zwOKAS5nuTK3|Nre9?G~6sAU;UPBsR2HjO}MDY!=d*?W}*EGGJW`0#Pbm;M|oBve;d zmmYc`k=j~FND>qX13kIN2Sbf|$4BI;2F5fYU}wM2N2i{RjgR-2R{Q&}Ssa1u`!g6s z2+l%{80#|$#(5TFV_icpoKiSkE^k578C8K*Va*`JgR4i4JEkf#|vVK0(ZRu)_K_U&8IFRI{F+>3x{*_)U0 z6c7#@qpx)Fyi7A(KV<$ z;K` ze@|i?`FEu9iYYlG6O*O;02>$$Jv|M%NB>cZw;l4>6o?)n2r)kHy8v294H=|1P?{^n zjAF8~7gLW*`50+kzFLVp{&RoL`yA}~y|@BGu!fFZx)Y>$5gr+tK-OLBHbV$=r?I{B zC;puFk*)q=kx~A;ED{>TMXlJWXL9oOh)J+z-q9Oe=WBQ|k6K?}PcBdfFU$EPUAs~x zaX?EmJu{OZi@S~c@?}GJ`boV3HbC&mJ>y(D~auc3b@B_cOjA42c!$tihxNx-l?@ccrCzgZ=_*@%)xRYA@TR9<80{(IC zn>R)?z&h%pEC*i0tojkAOES(MdqqbFif$?$uk=SyQYtz4z8soS!dt)_4yGv44Qp$q z!MK)D7|urQ2*6f{q{(_aYcn|CWeIEr{{{TTMoZ#+0(J<7`PKUp=b=Z>8zBPUX`jjk z=7A5A9Ds5M&aCCk#Z9d5iwNzNgb=OWsWJoa)$PwuL8o>uDki1~pcPee(KAvnj{|>e z;FIT{bByQDr>C#qdCL+weBa8jYrji|5(Wj!A_bRV3#=P^At?D-?TSymEP_+mnP+=vgHY} zd(RE7H(byFcqs34#uFZyM~Y=+F_>;eYxm(lT?+pj9%$78f`;R~^GUeo?fJ`Op9w(@ z6cPP)(lsU$5|R%X1ZOM9>!h*$TwBC?6nA$M;mMy`tDimP6ZP&LFZ5ecFLg% zm+~OQd&(N6TfL6wsNNudjXz;_%LccebLU<(r@zH+SX#H(!R6Dc2hy0zM39dHD1R1_TFpu^12iW`6IJx(y; z*ku2b5HZXd3=Mk^R$(dwGYhuu%r#yp;dk%ex&Gd>D|RuH^(mm?IXtUpN`SnE5v5uK zIqm6FF!B|im;#AoNk~lHWCZ@Z&rGD`T2lQ%^FY2nUDRPZNrN$hZ5=kSDI@Ml3l!Tb zP~OUB|NZ+nB;Xn`7+c_#0N- zsSyl}OHECUjfW>R5gT^auFQOG@onaG#h{hyg9k;jF1r}ZOo!TQJT9Nf*@y!B9t{8# ze)3qPfr^8)e*!)z#)cSfjJdI32qW{`-UsM|8CI!> zb7XaUOBcOGJ>h#kfA34J@-w85T(H?DJV)9HZmgu|ciE^J_*&6_E)#kq=y0y;J&l~+ zteNP@$f|v>q;ytfG(e3Aks_L$HL6q6(K&s~&dzPFlVN9fH|yOyV!3V**!K%~vWD-s z%+7M2p;eiJOE#Ks`T-rGvFhPG8wls~=Wm|c@X5to z3Pq55BH>d?xii11nVAJmuU!j+bDVZhe)Wn5B+Hct+O0ZK`BilcrRRb?X&MN`KhOM| zOLci(QBg6yy!?J7eFDD6WgW1YK!5I>#sjTZ_vrB7esdSulWS00&^V?wSpzr~0`~fE z&$F_!(kUM8*j<*9dG#wQDnV{Dc<|wlJ5ylB(}sULW2=wnuMxn&sW}sSFyC+LAy7H< zXMQik?6t)tbx)7Yij@D>dns{ouHjwuR(Yo3`Q9=nvx%do?O#DO@U1t|w+7w+L>3p` z)Y!n(V;XdgdTQ64@YMFv{%T*BZm4XiZ!zWK{N6R;N5p3X8>A(FZzD&a7@FA>6|I5( zWb;1HW+R>ki--gFwKr#HnKaKm#qXObGYtdU*c7&yLV%pmZewz{5Lv5HO5 F{{f%F2q^#n literal 0 HcmV?d00001 diff --git a/lx-icons/32/download.png b/lx-icons/32/download.png new file mode 100644 index 0000000000000000000000000000000000000000..e209fbd27900007daeae53cf22b23f503d486d9c GIT binary patch literal 984 zcmV;}11J26P)y{D4^000SaNLh0L04^f{04^f|c%?sf00007 zbV*G`2k8YK4Qtvw3tw_KUrx^X&j0b<@4&whm6er-M4wqzRb}9R0AFEPH*V^vtwxg(fPg=M$znn> zNNBo7C>X|MPKw*1zK=d!&W4@9J@F-%HD%Vssn%o*0B_&E$D_Vu^~|+YR9_@Dl1h$a zY}^WHYHY^kY#0I{MO)w)Jc`ma$H<$Q*XI;OEx`T$B_$QbIP07&&s#z6h`huW@RLXk z2Kv%3B3g3zpb;EgS&FKuY$#ig5;4bB1KlFxq*=kT|^Z2#QsS*#&V$K-V?)U)jf_r;q5{ zKG+QfwiFu&R~%w)&Vs~JAS5Hj`Nh}<*x7rjB=Hnzx<;-0IUm|S^p6WPUFG_dDnj89 zrE89{a7un+DX=L6nK5P#R)D$e{^V?B zQbHe!!X$=f7dYOGJesYiO&(a-FOO4U5*flQCj3C}ALA2@7!tYCh(0#}0000y{D4^000SaNLh0L04^f{04^f|c%?sf00007 zbV*G`2k8YK3Kkn4)3)0H00GrWL_t(o!|j(%%i2H?$N#GmB8lc8fnJmta|jW%P$)cC zze#&4^wxVR{Ve?qy?FEsD5N2WYzUf1FyLE=FZCsEFGUc2q+rAcnmNww?);e9o!J3i zgD}R(|KRg=3ITvxt@c6J_0K%d0{{+(!`Z@2CX<-cFbv^&-WS((JGsBQN~_g+CrJ_j zV6j-B-EJQ@7={5|*AoNi_xtcX?+pNC9-V>|AO*-Jb!wryiNoub)nBAd-755O3MEXxo@@yw>GD$31jE z7i+$4+mK}$9LHTxfoYmY!p&xb@pya^=H+tvB19BLuq+FWM&soB?)yF*$APA4==FLh zt@~sy&@@eAjFoSRM73IdTd&uDD2j6Y`*%8>pO$5P6-9A(p;c2#>F*o>P)b7p;Vl5J z>;B?6PAe9R-wK5Sy4@}v3 rUmH%7@OdVNVUSX(l$RvwpQhhGZQH-_;dIKN00000NkvXXu0mjfoly{I literal 0 HcmV?d00001 diff --git a/lx-icons/32/log.png b/lx-icons/32/log.png new file mode 100644 index 0000000000000000000000000000000000000000..ebc7be1ffe7c349336c654d68dddb68052f85400 GIT binary patch literal 906 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 diff --git a/lx-icons/48/download.png b/lx-icons/48/download.png new file mode 100644 index 0000000000000000000000000000000000000000..4302a7cb4688606cc66bd646455cf18ed15eeee1 GIT binary patch literal 1443 zcmV;U1zh@xP)y{D4^000SaNLh0L04^f{04^f|c%?sf00007 zbV*G`2k8YK4}#ViWQZB7(q!q}jj zy`Q${^}`{!#f@&zjUMCksXgbsr_b+wp8xZ{?~}r1tFNz@(4iRscI?<8O_(r2R{+h; z&AOcM|1I~drKLr;xzyCuWbE&J9WYZTzP|(P1Tglycwf!jd+xG3?E{`{Z~qlhsBu1B z{VX5u+M<=Oki?Xdk}>!EOvhQ;_8e&$41oMX7f!chzyQmi7GlmM0HtMRu-~9v$4o&{ z{+I(?x)2=j-I!^>trKr&*8L9x;P?7+E|)R_B-2SEu?PSuHLX8@_RbR=YB>VH`%5>n zX6{-5zWllci03{)e@mrOoIQUQfN(5?!)D5TfQmpRwGTZ4fXm@x%ia$Fu!YP_^HkaSnZbV z0fggWI?i+e;IzB0E8HO|O7cAgwP^6jIhVVMMPgWO7Lc+85R6=;;oAmH1EuSkbLXQ7 zA=uUW)ipr~tBPt>2frynESf+dkcLSxqJ*>C9Ms%hgQ}{0w|~#5h%y$AA%sAZvILNC zE1-6IwYIHDCRw&+siuL183<(mkZv$!SH$_Y{415m?Q-jH9XQkTJ1;b>0O0j`Z{%iM zWw_?^`e>?arm3zOkG)8L3t3lB7fUugrD-5yCV)5h0c>VFTURw)lfZHUAgV-|{aP&m zZ$0_8J~d#Jj2I;&h53bSTDF-@%QoY(xb>%jNG!sE!~3<*N?3mYilVT+ZM&uo)vOPI z^6?eay|jj0wTu8A=T7qQ+S&h&h0I&vW!uU|wykWWz~<4P26}=$tlIdprh%xLV#eh@ zK&gKUb7~(0V4P?49ae=(BBo%@Ga(!0>_%Y3hZi9@daRwW8U?^)HKAOs874tz`~nwG z^i$K29&4LFOfPnUs$1^@J8)X7{@h0i3$TZ*vQ zGIu9NeS0a(GE+;+k#&;o99gAZLnbLH4~UT9;0bwB2N9-dIYm&;1;2)tDDs2 zF2dIMRyq%NY6BISXlHczEtTM3*c+UfOY=}}N&H*CaRau^b}k;hgf(jBr|*w4*ttH~ z-3uW4>HdlZ0RL!4!#&l#xTwmzJo09Uw6|YxNPxZ3iSlu>vvcqHMM6ynGi?xnzA;~P zWLY-&X887$uPzh*xL5d8kMOA;F{!m!l+2o}$QF4P!)|HaAd_X;UtC>$y7ZG0;Zr># z@P}WNKVBwWQ=F>-dBpDy{D4^000SaNLh0L04^f{04^f|c%?sf00007 zbV*G`2k8YK3Kj+A+>5vX00PcQL_t(&-tC#cPuoBg$G;aF92|bc21jlp0#bxLrDN#O zrDK@N(*K};LZyyfx>WrqI(F+&shtQCEObF6awLQ;7!o`D5)LsaB(bkUTSS@!5}`VY zdf&~ychB#0ckiU<2mF?4ngru2N-3d~hQ=*sFfsk-XNgkE-v*ybrQQK}J|?k$G)?n6 z0PuHSX4lu(Gtp?Y&C+#U6bgk751OVymgT@BFBXf~+1dFDpaTGkqF{c0zHNfzIG~hv zIFe-V2EYIqz@x+m`LS}jjH9EYp5rq!Gf1b?AcTx2fNk4Y zUS7u3)Ku5;t*tGjQYl0tktBA+r zNG6luIBvKAhlhs{1ObvHLDMvZ!{MH`>2w;DQh1(+d_E6VReP47fgK@D(@?2Y5Q#(} zNz%P5swfI#u^1f3fh@~tG#bOtf!WzvtgNg+*Y&pE0bMIw>F8iWu; zqtULNoo2I%LZJZ5vT%8MiIbC)u71wW&H&)H<8rr0mH?ps%j+>7kIM@S3tv`OSH-`N zN3mFhVHn@`_Vzx^&CS(9q0oK1Y8Zx5Z375OYinz-rl+SrXEK@Fgf5jz$mjF>+uPeO zd7j^|)oQd~e_YT22L}iG($bRDXf&QFiUQYlvAerlE0s#GkB^Uk)a!MB5Pv*|pk}lA zrd%#R4Tr-oDwPT@E-v2dx}F2z4SY!ezna~I5E>*zHk*9{;BCELH*>k%Cjiw!<_{z0 gt_#R<+@r4YpWy{D4^000SaNLh0L04^f{04^f|c%?sf00007 zbV*G`2k8YK6)qZSG2Z(C00p~AL_t(&-qo7RPZM1fz<)DfTZIw~7Vtp;iKJLGwGkhf zuuu|K7#EEun&`sdBN|+>^q)ZE<69VYVXO-^QA>=FC`QAh#-bH@NGp(PRAMVFO-sw% zZ$qnm3be&(_)apJ_TF>voZp?9d*_^c;HTsOEr1J90HMb5`0-;A2n58yz<|)}^{KQ~ zW^HSkwG{wq_FMwA1C_0i(YB#b z2&dCYb8|Dm5Ks?{$47q9YPAZl*DKT(MN!25{riR6?M|z`*XtElt5pDl@jKB1WW;zO zgcute6M(SW?P>Sn^?KuvRZAA|Pit#yVSRl)nUEJRUJwih357zWdriGwPeDNeZEbDq z0F=(oPLU}&d-km8?(P-^1qC7$3Z>moXJ@AX6d91zcwzq}KA(?{jtUjJ1?UMVH0kX5RGXn7P*M?P?@I^BgbWW4bLh|^^78VC-}R$Mk1{wo`2PUt z>FHtLzI}7bYHDh5I-ScOKv5KK-@eV3EnDW4S5{VHv)ND-WoZCftyWcl(a}-XtXad_ zwQJ{;7Z(?^di83&UhmQXR;*Z|DnMUf9|sN`_+wpTVGyqAGR5N~JVgi@TMR|GoAL}bBD!6dr0+C20eSmN{tQvsJ<>J_}WAiSSd3kvp zJa~|gA3vrK5DW%Y19wjP7>&jS^-Lxc z?d|O;^&Nk2RegzvhK4wG>J&LSIScBQm6c()+nJo4O!aG2U4X8xF6!#)7S+>eG@Lwn zlHuXulpmpNAVD4s25~qXNRoss%Se)xIAmGIU@%ZoQIXUIVzC%EZrtF(g9l0J@7%eQ z=g*&0R8)j4%Zcm9XUX`Ho12SHr%TLQ+5cVgy?RInZ>eZwS z@_0O$OePElLsI%xRaMw*HqM_vPc#~3dU~2@G@AGwi9`s8!(6_68H>e&-|tVf6%L1E zQ5P-}iHOqDQsMLYl5+WMHk-J2@1AO|sl(wAfRF(S3k#EaPEVgc&4&*kk{Z#wcki-w z>sHlfxcI~%14*xf+qZA$_3PJ*>V5k3iHV5`%F4=A1IW$I{RuENHI-B&qtVFy`}Yw- zEb99nK71HiR{v`~C6{vDx^+aOQ9ghEyrABbCr_xUsmX|=l+qAMl7!W2UC<4joSfv= zty^r`v}ySP0GQ2YdV724t?%#e=iIq-=ybZ}2~b&C$+c_Oh(@FHuD-RkwHb>wr3{dt zpU<8>dl(%Z{jDw%iE#1aMNB5s@&y2BXlUU5`}e<C>mb zmG}1c($LUACM7L^^73-7U%x)b5fVah=gu9ftE)3&czp3q3t;8Sm7Ft9a zL#NZRe*OB40E9xJrFmqvwY77iYH!}W!D_Xt8X)*p;`?f80~n1)>~?$N|D#8b<_(>w zDQU;mOG--ccszXj_6=E<>FVlY|zrY6-Re18=Hz+$oR z>eVX(fk4Km&pDoCx|@7ozkU_FcI^_y#l<2J2#8F|ubX`R>Bi^{1_LD}B{VfPB^-=Q z$*j%&u*8k-n>TNYj*gCuj=taTPi%C<5>pMFPN(?t8!l- zC3_?seNs%f(d+f1r>94_TrRPB^JZ1jb66zZ=3n7s=(YV2aAyDj002ovPDHLkV1h+1 B8Ib@0 literal 0 HcmV?d00001 diff --git a/lx-icons/64/download.png b/lx-icons/64/download.png new file mode 100644 index 0000000000000000000000000000000000000000..cc12d8afa67fe03b6b9a0c8ecd196b5dee7a7bc8 GIT binary patch literal 1799 zcmV+i2l)7jP)y{D4^000SaNLh0L04^f{04^f|c%?sf00007 zbV*G`2k8YK4XmTrY!pe?o-wQ@;} ziWo%jffz_E7`bRr3K)!Xk&;L-5JQcTD5Yr-5DX?3tZmUm!q$i;Rv#=RfD}O}wY6p2 z?shNT+w5G94+~Rz*>;yL+oj|Gv|nb<&ivo`Z|6T}fB|y&@L>w$RI011hu5vLvXa3# zm%6&Tp-HHzsi7egsjaQ0BV$DlmN24UjAN0r?*9t_R8&;Zw8dhft7Pz<=ZJ@?Tp=#9D|N4B+gz?f!~@RNm4;rFHn3H$ASq(5XzSk3=!z6E=S z6T=aJf?4@6nNx=RgU(+Z&`mc;xU_T!UMX84;YrqHJU)A7LQnr~0wj^G|A)1J;Uq65 z4|5CWN|>P;c@Z$LXdX7K-XLLK;N?ZYkspts_GXQQzb>tk7Xbq$*_e#-k^=w$3P9*Ym&hxg#@^|KOQM@m457y=YE{s|Z;nHib*^H>uAz-G7M#g)&?i-6XSR;<`tELngS z%9vj$;A3I8IR+WjQ29lpRwE^CV*Ci0pZ_ejz5AYo^*TK$YIK(qe_Xf#$`o0N%{e#} z(FI6OO^OczU9N6iYq=)jqJpAPH^~4KgmRez{zblF?_)X@(CyO0?+d^v8bQ<4{U@NQ zvk7~uci;C?m}m*aBw^TlXkW3Rc79YE_aNr? z1|SL|7{wS6z_P655}mfrF^S@SUjQN@@reNd`tm*ikI&Q3B{YbCDXdh+_ROe8FP*&< z2lgG1aJ#iV0Wvv>9|5(EwfOn9pCnwh%6|)Gfi5dveXCer z1ax$EVEOG*m>Q*0_<tk#VA3=#sceinp?P^y$* z7$$UBAzrJ&Sf%B98@jzc(n~9KN)U(47*whb~^67Fq#OLa*DRh1f47lFa3M>IhoCJS)7op}4urkJw8 zHt9=?}6(UE9VK>L{v-dkSerDZ{_QAM>+IQEMn3YeCc35`<$Rh1g5 zOOa<5ild;bG=Q^mNF_-ynNy%tN7t7eG-Vvifei$QyjkQ$2*Ex-?REF|!#szuV@Jwm zxjET5|92C#`*jdL=W$Bvq&AJl?cvrcHbW+BqfTmmrkJZ!by5cT@D7HmW4_?3Xtgj@zXIu5bU-_d$G^%3ur&pgLZ{VTlG0D z6yGV)bHj1o-|l}OfIpV!NV+HLhm)E#^&2@$vPtg8)5w$?W|F@;kC>k}J87DJB3A1i zr1PN9NfShjdB&r;OQ)0k;yjZ1!c>l?>6Nh-aA9>#L`z^8MyWQbHzYrtw40%sMZ=}( pdz$&Mevz0G^}AUt7HT-%{sWQ(Ll?YJgCzg}002ovPDHLkV1nvUDPI5p literal 0 HcmV?d00001 diff --git a/lx-icons/64/download_error.png b/lx-icons/64/download_error.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd41616ee7054666cadf803288a73517fae4339 GIT binary patch literal 1187 zcmV;U1YG-xP)y{D4^000SaNLh0L04^f{04^f|c%?sf00007 zbV*G`2k8YK3KTaUJ-E6600ZYqL_t(|+U=W7PwPe$hTpMcupO}ZDB?p6vI+}e1*$~0 zgdIPk`>O4}{Rv(458TbI)O{CKB|u`u1|&pOk&q|~7_tBy5@b6GiJgRay1MCwCM1wW ze3LmlXXbePJ~`)%$72I8tXM1(45(2`U)Pq;=XngcwS_<_q)|E^q1Iw}ipe+D|!(l8gF19>DHk-xK(NWt0wzs!2Iy&lG0Q>vmz5`$w29A%9yXreX zKab7LP4GNFH~?(hMko}5rfIMCt5hn8Mx$_D7osS>8-g!WKoA6oqWEf`r>7?XK&e#f zX$k(t7r|n&*n}~Z0363bqtO^%0KJGG2mqa?N_Hc}05AXy00Y1PFaQhy1Hb?<01N;F zzyL4+3;+Z0!UBUJqEe}VQVLNN5eNjnECAJN71?aI`Dn{DO$39%uKi8ZgzLIcRTT>h z3z(Rgc+(z1-vK0(N#t@l$gF9f)hYx*=sF7eUIaBw!`Rpu5{bmSpYZ1M zd1Nvf%+1Zc`7wFl0q{JJjg1YIN+syJ{$_v2aS)HkAxRQcRfQx;Z>j;lm0>ID(pFbjTZ(cV2j}PK zczk@^zq`BppsMNv$8ogFAud0QJb%92zq7NmNeDSySy>T7q0qCoMx%j~lM`5$m5xTE z-_D%r&m`bHkC={NxrPFB?i^am#)zvSiX`c5Y9soGMemb_hyZbwU zkFi*+S=bE2z|GB#_wexWo387JlarH9FJn+j2}cN_K7~N*_4-e)>z*bO2^0ziB$G*$ z%jG{$Pf!2gdET}xi}oU(5b~dJN-0MO@qAbhz9EDh0{FUKuOA*99Q*(v4dAH<@vXJ` zH60?6h#ZMTRsl@+I!$-f=;zN#gTWx_MQ|%^{{rTDvG?ezxv~HN002ovPDHLkV1i;r B_Ll$v literal 0 HcmV?d00001 diff --git a/lx-icons/64/log.png b/lx-icons/64/log.png new file mode 100644 index 0000000000000000000000000000000000000000..e1eb8dc09fc8a77a04ee7c1ddde6287d223dd816 GIT binary patch literal 2015 zcmV<52O#)~P)y{D4^000SaNLh0L04^f{04^f|c%?sf00007 zbV*G`2k8YK6)z3*`D%Rt00$#UL_t(|+U1-5OViyO$6wRAip(ccB(%cknVF?%Dv<;w zOT$>EJ?IZT-a_UN(hvO$5fn%UW??@lWl2bu-NupxA1X`I=}c4WPSMm*6Ti4kxAXm> zHY{hiPu*_o4TpVp&biLGUi&-WfUmDF6%`d6IBU3PN@{9qO2oy*iP6!~C9gF$HYQXm zl~5=YrZb}UyU%7710p9UXT=>isk5_F0HU$6aoN@!8ygcjIXNZ>#WSyYp-?ES#(5!x zC@n1&fG8|1T-J5R#>Qrz3DKr&La0T)#jtM{{CnpP|(YUPZt5hlzfD%l1 zdu#gk`}_L^AoBC`1t9wS`4aEda>na#~wk`Sj`2p8=qup@E#79NgXA0r2th zAvQJ^jYebt0Q7o2w{PDjF)`8HCzVP`OG~4srpEpOba!{t+}zC8ty_QSkBW-o+O=y; zOib7}fTpG(W6K4_V%9F{OIUt3JVLF zoSd|M00x7B($Z269y~Z}_cxb9LP8iC8ltDC$MymA_V)7h=~K3E-#%{}Z*Omslapy} zZMA&>&CSik#l^9C^X7TmNF)+cQc|d@s)KX_^YAQWFJ*--_3YkoXlamupPEJUrQuC`+D&_d` z<0K^|E$V~Y+S*vZemx-}Aq%#3b8{mrD~qC{A~YHej*gDzCXq;RaB#5LcJ10VYqMn$ zKz4RETCEl#1VRXmMx*)I^uD&XmWqmsMFXg*so~tYbF5vvc2V|+4jtm}zyC(9R%0+2 zFc=INjYbRx15;B|=BCr>(ChW+bUGp1n$F0QT+M$HRvY5kkz{W@>7R;^JbGlas9?5VIrzVPRoBdi03TpFhvr=HthY z)YsRuZQC~62jJ)Dx8Uq;Q&SU(iHZ37`r1B#HEY%^D0?e}puD`C^z?Kb9UW~S0DQ~d zmXwstD|ircZrIM!dkRs0CKq;wOT!|?5$d@CNMD2{0OxR0H*Bie8N;Jm5MWG z&ais*YWo3T%HBSI{`{BozJC46l`B`we|)eT0Prn)o1dTm%j~U2qrqr25*iw6e*l7l zg3#%7ba!{pIj6q9p4{AA+}zyk4}hnqCuwPEb7pUKIvuxe-D2g5XXg+7 zMx&8?_wJ!oD(4;gwn+f%)~&Ocz3uJorLwYeL7{J(1OSkbkih->_c0g@=Dz0UW@2Ju z@bmMte*mGOq14va{*b+`tgIw6GjqYAZ<_?LY11a;a=H2E@s}@OC@3gk=gys2tCkgj zOeP~UGt->CZEI`8-QAtw;NU+406?7<8-+r_rAwC>92~TY(6=Sc^m*A!W(o=l1RzeGIwh=8 zKbOqZnzpOUrHF_K0ILaoTiTj`S1tU8vbm9w5dnyvo*t|F_O)8=tg<=y4TX~zFJ2VW z)6-Vv{N&`MxP1BYtinn74W+XmK76pMYkscM*<~-Dj){pusZ`?P;(`!@>gsCB%F0&s xZa<&m>6uz``Fu)BiWnasH None: parser = argparse.ArgumentParser( - description="Script only for use to compare the private key in the Network configurations to avoid errors with the network manager." + description="Script only for use to compare the private key in the" + "Network configurations to avoid errors with the network manager." ) parser.add_argument("search_string", help="Search string") args = parser.parse_args() diff --git a/settings b/settings deleted file mode 100644 index 6ef82b9..0000000 --- a/settings +++ /dev/null @@ -1,9 +0,0 @@ -# Configuration -on -# Theme -dark -# Tooltips -True -# Autostart -off - diff --git a/ssl_decrypt.py b/ssl_decrypt.py index 4ce87c2..c3cef08 100755 --- a/ssl_decrypt.py +++ b/ssl_decrypt.py @@ -5,7 +5,7 @@ from pathlib import Path import pwd import shutil from subprocess import CompletedProcess, run -from wp_app_config import AppConfig, logging +from shared_libs.wp_app_config import AppConfig, logging parser = argparse.ArgumentParser() parser.add_argument("--user", required=True, help="Username of the target file system") diff --git a/ssl_encrypt.py b/ssl_encrypt.py index f3068ba..0a00e7f 100755 --- a/ssl_encrypt.py +++ b/ssl_encrypt.py @@ -6,7 +6,7 @@ from pathlib import Path import pwd import shutil from subprocess import CompletedProcess, run -from wp_app_config import AppConfig, logging +from shared_libs.wp_app_config import AppConfig, logging parser = argparse.ArgumentParser() parser.add_argument("--user", required=True, help="Username of the target file system") diff --git a/start_wg.py b/start_wg.py index 7583eec..3336915 100755 --- a/start_wg.py +++ b/start_wg.py @@ -2,13 +2,13 @@ """ This script belongs to wirepy and is for the auto start of the tunnel """ - +import logging from subprocess import CompletedProcess, run -from wp_app_config import AppConfig, logging -from common_tools import ConfigManager +from shared_libs.wp_app_config import AppConfig +from shared_libs.common_tools import ConfigManager, LogConfig ConfigManager.init(AppConfig.SETTINGS_FILE) - +LogConfig.logger(ConfigManager.get("logfile")) if ConfigManager.get("autostart") != "off": process: CompletedProcess[str] = run( ["nmcli", "connection", "up", ConfigManager.get("autostart")], diff --git a/tunnel.py b/tunnel.py new file mode 100644 index 0000000..fe4cf9a --- /dev/null +++ b/tunnel.py @@ -0,0 +1,230 @@ +#!/usr/bin/python3 +import logging +import getpass +import zipfile +from datetime import datetime +from pathlib import Path +import shutil +from subprocess import run, CompletedProcess +import secrets +from shared_libs.wp_app_config import AppConfig, Msg +from shared_libs.common_tools import LxTools, CryptoUtil + +# Translate +_ = AppConfig.setup_translations() + + +class Tunnel: + """ + Class of Methods for Wire-Py + """ + + @staticmethod + def parse_files_to_dictionary( + directory: Path = None, filepath: str = None, content: str = None + ) -> tuple[dict, str] | dict | None: + data = {} + + if filepath is not None: + filepath = Path(filepath) + try: + content = filepath.read_text() + + # parse the content + address_line = next( + line for line in content.splitlines() if line.startswith("Address") + ) + dns_line = next( + line for line in content.splitlines() if line.startswith("DNS") + ) + endpoint_line = next( + line for line in content.splitlines() if line.startswith("Endpoint") + ) + private_key_line = next( + line + for line in content.splitlines() + if line.startswith("PrivateKey") + ) + + content = secrets.token_bytes(len(content)) + + # extract the values + address = address_line.split("=")[1].strip() + dns = dns_line.split("=")[1].strip() + endpoint = endpoint_line.split("=")[1].strip() + private_key = private_key_line.split("=")[1].strip() + + # Shorten the tunnel name to the maximum allowed length if it exceeds 12 characters. + original_stem = filepath.stem + truncated_stem = ( + original_stem[-12:] if len(original_stem) > 12 else original_stem + ) + + # save in the dictionary + data[truncated_stem] = { + "Address": address, + "DNS": dns, + "Endpoint": endpoint, + "PrivateKey": private_key, + } + + content = secrets.token_bytes(len(content)) + + except StopIteration: + pass + + elif directory is not None: + + if not directory.exists() or not directory.is_dir(): + logging.error( + "Temp directory does not exist or is not a directory.", + exc_info=True, + ) + return None + + # Get a list of all files in the directory + files = [file for file in AppConfig.TEMP_DIR.iterdir() if file.is_file()] + + # Search for the string in the files + for file in files: + try: + content = file.read_text() + # parse the content + address_line = next( + line + for line in content.splitlines() + if line.startswith("Address") + ) + dns_line = next( + line for line in content.splitlines() if line.startswith("DNS") + ) + endpoint_line = next( + line + for line in content.splitlines() + if line.startswith("Endpoint") + ) + + # extract values + address = address_line.split("=")[1].strip() + dns = dns_line.split("=")[1].strip() + endpoint = endpoint_line.split("=")[1].strip() + + # save values to dictionary + data[file.stem] = { + "Address": address, + "DNS": dns, + "Endpoint": endpoint, + } + + except Exception: + # Ignore errors and continue to the next file + continue + if content is not None: + content = secrets.token_bytes(len(content)) + if filepath is not None: + return data, truncated_stem + else: + return data + + @staticmethod + def get_active() -> str: + """ + Shows the Active Tunnel + """ + active = None + try: + process: CompletedProcess[str] = run( + ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show", "--active"], + capture_output=True, + text=True, + check=False, + ) + + active = next( + line.split(":")[0].strip() + for line in process.stdout.splitlines() + if line.endswith("wireguard") + ) + + if process.stderr and "error" in process.stderr.lower(): + logging.error(f"Error output on nmcli: {process.stderr}") + + except StopIteration: + active = None + except Exception as e: + logging.error(f"Error on nmcli: {e}") + active = None + + return active if active is not None else "" + + @staticmethod + def export() -> bool | None: + """ + This will export the tunnels. + A zipfile with the current date and time is created + in the user's home directory with the correct right + """ + now_time: datetime = datetime.now() + now_datetime: str = now_time.strftime("wg-exp-%m-%d-%Y-%H:%M") + + try: + AppConfig.ensure_directories() + CryptoUtil.decrypt(getpass.getuser()) + if len([file.name for file in AppConfig.TEMP_DIR.glob("*.conf")]) == 0: + + LxTools.msg_window( + AppConfig.IMAGE_PATHS["icon_info"], + AppConfig.IMAGE_PATHS["icon_msg"], + Msg.STR["sel_tl"], + Msg.STR["tl_first"], + ) + return False + else: + wg_tar: str = f"{AppConfig.BASE_DIR}/{now_datetime}" + try: + shutil.make_archive(wg_tar, "zip", AppConfig.TEMP_DIR) + with zipfile.ZipFile(f"{wg_tar}.zip", "r") as zf: + if zf.namelist(): + + LxTools.msg_window( + AppConfig.IMAGE_PATHS["icon_info"], + AppConfig.IMAGE_PATHS["icon_vpn"], + Msg.STR["exp_succ"], + Msg.STR["exp_in_home"], + ) + else: + logging.error( + "There was a mistake at creating the Zip file. File is empty." + ) + LxTools.msg_window( + AppConfig.IMAGE_PATHS["icon_error"], + AppConfig.IMAGE_PATHS["icon_msg"], + Msg.STR["exp_err"], + Msg.STR["exp_zip"], + ) + return False + return True + except PermissionError: + logging.error( + f"Permission denied when creating archive in {wg_tar}" + ) + return False + + except zipfile.BadZipFile as e: + logging.error(f"Invalid ZIP file: {e}") + return False + except TypeError: + pass + except Exception as e: + logging.error(f"Export failed: {str(e)}") + LxTools.msg_window( + AppConfig.IMAGE_PATHS["icon_error"], + AppConfig.IMAGE_PATHS["icon_msg"], + Msg.STR["exp_err"], + Msg.STR["exp_try"], + ) + return False + + finally: + LxTools.clean_files(AppConfig.TEMP_DIR) + AppConfig.ensure_directories() diff --git a/wg_start.service b/wg_start.service deleted file mode 100644 index 5d41844..0000000 --- a/wg_start.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Automatic Tunnel Start -After=network-online.target - -[Service] -Type=oneshot -ExecStartPre=/bin/sleep 5 -ExecStart=/usr/local/bin/start_wg.py - -[Install] -WantedBy=default.target diff --git a/wirepy.py b/wirepy.py index ca0f411..3edc8dc 100755 --- a/wirepy.py +++ b/wirepy.py @@ -2,7 +2,7 @@ """ this script is a simple GUI for managing Wireguard Tunnels """ - +import logging import getpass import shutil import sys @@ -11,21 +11,19 @@ import webbrowser from pathlib import Path from subprocess import CompletedProcess, run from tkinter import TclError, filedialog, ttk +from tunnel import Tunnel -from common_tools import ( +from shared_libs.gitea import GiteaUpdate +from shared_libs.common_tools import ( + LxTools, + CryptoUtil, + LogConfig, ConfigManager, ThemeManager, - CryptoUtil, - GiteaUpdate, - Tunnel, Tooltip, - LxTools, ) -from wp_app_config import AppConfig, Msg, logging -AppConfig.ensure_directories() -AppConfig.create_default_settings() -CryptoUtil.decrypt(getpass.getuser()) +from shared_libs.wp_app_config import AppConfig, Msg class Wirepy(tk.Tk): @@ -169,7 +167,11 @@ class FrameWidgets(ttk.Frame): self.settings.add_command( label=self.theme_label.get(), command=self.on_theme_toggle ) - + # Logviewer Menu + self.settings.add_command( + label="Log Viewer", + command=lambda: run(["logviewer", "--modul=wp_app_config"]), + ) # About BTN Menu / Label self.about_btn = ttk.Button( self.menu_frame, text=_("About"), style="Toolbutton", command=self.about @@ -451,12 +453,7 @@ class FrameWidgets(ttk.Frame): self.download.add_command( label=_("Download"), command=lambda: GiteaUpdate.download( - f"{AppConfig.DOWNLOAD_URL}/{res}.zip", - res, - AppConfig.IMAGE_PATHS["icon_info"], - AppConfig.IMAGE_PATHS["icon_vpn"], - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], + f"{AppConfig.DOWNLOAD_URL}/{res}.zip", res ), ) @@ -1145,10 +1142,14 @@ class FrameWidgets(ttk.Frame): if __name__ == "__main__": - + AppConfig.ensure_directories() + AppConfig.create_default_settings() + CryptoUtil.decrypt(getpass.getuser(), AppConfig.CONFIG_DIR) _ = AppConfig.setup_translations() LxTools.sigi(AppConfig.TEMP_DIR) + window = Wirepy() + LogConfig.logger(ConfigManager.get("logfile")) """ the hidden files are hidden in Filedialog """ diff --git a/wp_app_config.py b/wp_app_config.py old mode 100644 new mode 100755 index e678661..435f9cf --- a/wp_app_config.py +++ b/wp_app_config.py @@ -29,16 +29,10 @@ class AppConfig: """ # Logging - LOG_DIR = Path.home() / ".local/share/wirepy" + LOG_DIR = Path.home() / ".local/share/lxlogs" Path(LOG_DIR).mkdir(parents=True, exist_ok=True) LOG_FILE_PATH = LOG_DIR / "wirepy.log" - logging.basicConfig( - filename=f"{LOG_FILE_PATH}", - level=logging.ERROR, - format="%(asctime)s - %(levelname)s - %(message)s", - ) - # Localization APP_NAME: str = "wirepy" LOCALE_DIR: Path = Path("/usr/share/locale/") @@ -58,6 +52,7 @@ class AppConfig: "# Theme": "dark", "# Tooltips": True, "# Autostart": "off", + "# Logfile": LOG_FILE_PATH, } # Updates @@ -69,6 +64,7 @@ class AppConfig: # UI configuration UI_CONFIG: Dict[str, Any] = { "window_title": "Wire-Py", + "window_title2": "LogViewer", "window_size": (600, 383), "font_family": "Ubuntu", "font_size": 11, @@ -94,6 +90,7 @@ class AppConfig: "icon_stop": "/usr/share/icons/lx-icons/48/wg_vpn-stop.png", "icon_info": "/usr/share/icons/lx-icons/64/info.png", "icon_error": "/usr/share/icons/lx-icons/64/error.png", + "icon_log": "/usr/share/icons/lx-icons/48/log.png", } @staticmethod @@ -160,6 +157,12 @@ class AppConfig: if process.stderr: logging.error(f"{process.stderr} Code: {process.returncode}", exc_info=True) + @classmethod + def ensure_log(cls) -> None: + """Ensures that the log file exists""" + if not cls.LOG_FILE_PATH.exists(): + cls.LOG_FILE_PATH.touch() + # here is initializing the class for translation strings _ = AppConfig.setup_translations()