diff --git a/src/config/config_manager.py b/src/config/config_manager.py index a8e11e8..781be9c 100644 --- a/src/config/config_manager.py +++ b/src/config/config_manager.py @@ -13,6 +13,22 @@ Validator = Callable[[Any], bool] Normalizer = Callable[[Any], Any] class ConfigField(TypedDict): + """ + Representation of a configuration field with associated properties. + + The `ConfigField` class specifies the structure of a configuration field + used in system configuration settings. It defines the default value, + validation logic, and normalization process for a configuration field. + This ensures that any provided value complies with predefined rules + and converts to a consistent format if necessary. + + :ivar default: The default value assigned to the configuration field. + :type default: Any + :ivar validator: A callable used to validate the value of the configuration field. + :type validator: Validator + :ivar normalizer: A callable used to normalize the value of the configuration field. + :type normalizer: Normalizer + """ default: Any validator: Validator normalizer: Normalizer @@ -36,6 +52,18 @@ CONFIG_SCHEMA: dict[str, ConfigField] = { } class ConfigManager: + """ + Manages application configuration by providing methods to retrieve, update, and store + configuration settings. Ensures configuration values comply with predefined schema and + validations. + + This class is designed to handle configuration files efficiently, providing streamlined + methods for interaction with the application's configuration schema and default values. + + :ivar path: Path to the configuration file. Defaults to a predefined `CONFIG_PATH` + if no path is provided. + :type path: Path + """ def __init__(self, path: Path | None = None) -> None: self.path = path or CONFIG_PATH self._data: ConfigData = self._load() @@ -43,6 +71,18 @@ class ConfigManager: # Lecture du fichier de configuration def _load(self) -> ConfigData: + """ + Loads configuration data from the file at the specified path. If the file does + not exist, contains invalid JSON, or is not a dictionary, an empty configuration + dictionary is returned. + + :raises OSError: If an error occurs while trying to open the file. + :raises json.JSONDecodeError: If the file contains invalid JSON. + + :return: The loaded configuration data as a dictionary, or an empty dictionary + if the file does not exist or contains invalid data. + :rtype: ConfigData + """ if not self.path.exists(): return {} @@ -57,8 +97,17 @@ class ConfigManager: return cast(ConfigData, data) - # Sauvegarde du fichier de configuration def save(self) -> None: + """ + Saves the current state to the specified file path in JSON format. + + The method ensures the target directory exists before attempting to save the + data. If the object is already in a clean state (not dirty), the method exits + without performing any action. + + :raises OSError: If there is an error creating the directory for the file path + or writing to the file. + """ if not self._dirty: return @@ -70,11 +119,35 @@ class ConfigManager: self._dirty = False def _get_field(self, key: str) -> ConfigField: + """ + Fetches the configuration field associated with the provided key. + + This method retrieves the corresponding `ConfigField` object for a given key + from the predefined configuration schema (`CONFIG_SCHEMA`). If the key does + not exist in the schema, a `KeyError` is raised. + + :param key: The key corresponding to the configuration field to be retrieved. + :type key: str + :return: The configuration field object associated with the provided key. + :rtype: ConfigField + :raises KeyError: Raised if the provided key does not exist in the configuration schema. + """ if key not in CONFIG_SCHEMA: raise KeyError(f"Unknown config key: {key}") return CONFIG_SCHEMA[key] def get(self, key: str) -> Any: + """ + Retrieves the value associated with the specified key from the internal data storage. If the key is + not present, a default value specified in the field definition is used. If the value does not pass + the validation function defined in the field, the default value is returned instead. + + :param key: The key whose associated value needs to be retrieved. + :type key: str + :return: The value associated with the specified key. If the key is not present or the value fails + validation, the default value for the field is returned. + :rtype: Any + """ field = self._get_field(key) value = self._data.get(key, field["default"]) @@ -84,6 +157,19 @@ class ConfigManager: return value def set(self, key: str, value: Any) -> None: + """ + Sets a value for the given key, normalizing the value and validating it + before storing it. If the value for the key is already normalized and hasn't + changed, the operation is skipped. Marks the underlying data as dirty after + a successful update. + + :param key: The key for which the value is being set. + :type key: str + :param value: The value to be set for the specified key. The type is flexible, + and it is processed by the key's defined normalizer and validator. + :type value: Any + :return: None + """ field = self._get_field(key) normalized = field["normalizer"](value) @@ -98,6 +184,15 @@ class ConfigManager: self._dirty = True def reset_all(self) -> None: + """ + Resets all configuration settings to their default values as defined + in the configuration schema, and saves the updated configuration. + + This method retrieves the default values from the configuration schema + (CONFIG_SCHEMA) and replaces the current configuration with the defaults. + + :return: None + """ defaults: ConfigData = cast( ConfigData, {key: field["default"] for key, field in CONFIG_SCHEMA.items()}, @@ -105,6 +200,16 @@ class ConfigManager: self.save(defaults) def get_all(self) -> ConfigData: + """ + Return all configuration data. + + This method retrieves all configuration values based on the defined + `CONFIG_SCHEMA` and returns them as a dictionary-like object. + + :return: A dictionary-like object containing all configuration data mapped by + their respective keys. + :rtype: ConfigData + """ return cast( ConfigData, {key: self.get(key) for key in CONFIG_SCHEMA}, @@ -114,25 +219,72 @@ class ConfigManager: # SETTERS MÉTIER # --------------------------------------------------------------------------- - # Set Discord ID def set_discord_user(self, user_id: str) -> None: + """ + Sets the Discord user ID in the application storage. + + :param user_id: The unique identifier of the Discord user to be stored. + :type user_id: str + :return: None + """ self.set(DISCORD_USER_KEY, user_id) - # Set volume def set_volume(self, volume: int) -> None: + """ + Sets the volume to a specific value. + + This method updates the volume by setting it to the provided value. + It is used to control the volume level in the application. + + :param volume: The desired volume level to be set. + :type volume: int + :return: None + """ self.set(VOLUME_KEY, volume) # --------------------------------------------------------------------------- # GETTERS MÉTIER # --------------------------------------------------------------------------- - # Get discord ID def get_default(self, key: str): + """ + Retrieve the default value associated with a specified key from the + configuration schema. + + This method fetches the default setting for a specific configuration + key as defined in the CONFIG_SCHEMA dictionary. + + :param key: The key representing the configuration for which the + default value is being retrieved. + :type key: str + :return: The default value associated with the given key in + CONFIG_SCHEMA. + :rtype: Any + """ return CONFIG_SCHEMA[key]["default"] # Get volume value def get_discord_user(self) -> str: + """ + Gets the Discord username associated with a specific key. + + This method retrieves and casts the stored value associated with the + DISCORD_USER_KEY to a string. + + :return: The Discord username corresponding to the given key. + :rtype: str + """ return cast(str, self.get(DISCORD_USER_KEY)) def get_volume(self) -> int: + """ + Calculates and retrieves the volume value as an integer. + + The method accesses the constant `VOLUME_KEY` and fetches + the corresponding value from an internal storage. It ensures + the value is returned as an integer. + + :return: The volume value retrieved as an integer. + :rtype: int + """ return cast(int, self.get(VOLUME_KEY)) diff --git a/src/config/constants.py b/src/config/constants.py index 56ded3d..fc14623 100644 --- a/src/config/constants.py +++ b/src/config/constants.py @@ -22,10 +22,44 @@ AUTENTICATION_SUCCESS_MESSAGE = """ # ENUMS # --------------------------------------------------------------------------- class Resources(Enum): + """ + Enumeration for resource identifiers. + + Represents resource paths for various assets used in an application. + These resources can include file paths for audio, fonts, and other + assets. The purpose of the class is to centralize and standardize + resource identifiers for easier management and usage in software + development. + + :cvar MP3: Path to the audio resource file. + :cvar FONT: Path to the font resource file. + """ MP3 = ':/assets/the-beat-of-nature.mp3' FONT = ':/assets/Avocado-Cake-Demo.otf' class Urls(Enum): + """ + Enumeration of various URLs and constants used within the application. + + This class provides a centralized location for managing static URLs and + related constants required for functionality such as Discord OAuth callback, + API connections, and server communication. + + :cvar DISCORD: URL for joining the Discord server. + :type DISCORD: str + :cvar INTRANET: URL for accessing the website of La Tanière. + :type INTRANET: str + :cvar API_URL: URL for the API endpoint of La Tanière. + :type API_URL: str + :cvar FIVEMURL: Connection URL for connecting to the FiveM server. + :type FIVEMURL: str + :cvar LOCAL_CALLBACK_PORT: Port number used for local callback. + :type LOCAL_CALLBACK_PORT: int + :cvar LOCAL_CALLBACK_HOST: Hostname of the local callback. + :type LOCAL_CALLBACK_HOST: str + :cvar LOCAL_CALLBACK_URL: Complete URL for the local OAuth Discord callback. + :type LOCAL_CALLBACK_URL: str + """ DISCORD = 'https://discord.gg/A7eanmSkp2' # <- La Tanière invitation Discord INTRANET = 'https://la-taniere.fun/connexion/' # <- La Tanière site web API_URL = 'https://prod.la-taniere.fun:30121' # <- La Tanière Api url @@ -35,6 +69,33 @@ class Urls(Enum): LOCAL_CALLBACK_URL = f'http://{LOCAL_CALLBACK_HOST}:{LOCAL_CALLBACK_PORT}/callback' # <- Url de Callback OAuth DiscordA class ApiEndPoints(Enum): + """ + Enumeration representing various API endpoints. + + This class contains a collection of constant values that represent the + available API endpoints used for accessing specific functionalities within an + application. Each enumeration member corresponds to a distinct API endpoint + and its value represents the endpoint URL. + + :ivar API_VERSION: Version of the API being utilized. + :type API_VERSION: str + :ivar QUEUE_STATUS: Endpoint for retrieving the status of the queue. + :type QUEUE_STATUS: str + :ivar QUEUE_LEAVE: Endpoint for leaving the queue. + :type QUEUE_LEAVE: str + :ivar QUEUE_JOIN: Endpoint for joining the queue. + :type QUEUE_JOIN: str + :ivar QUEUE_REFRESH: Endpoint for refreshing queue information. + :type QUEUE_REFRESH: str + :ivar WHITELIST_URL_ENDPOINT: Endpoint for checking whitelist status. + :type WHITELIST_URL_ENDPOINT: str + :ivar REGISTER_USER: Endpoint for registering a new user. + :type REGISTER_USER: str + :ivar TOKEN_AUTH: Endpoint for token-based authentication. + :type TOKEN_AUTH: str + :ivar AUTHENTICATION: Endpoint for user authentication. + :type AUTHENTICATION: str + """ API_VERSION = 'api_v2' QUEUE_STATUS = 'queue/status/' QUEUE_LEAVE = 'queue/leave' @@ -46,10 +107,39 @@ class ApiEndPoints(Enum): AUTHENTICATION = f'{API_VERSION}/auth' class DiscordApplicationReferences(Enum): + """ + Contains references and constants specific to the Discord application. + + This class serves as a collection of constants and configurations + related to the Discord application, such as predefined scopes and + client identifiers. It is primarily used for maintaining consistency + and avoiding hardcoding these values in multiple places. + + :ivar SCOPES: List of OAuth2 scopes used for Discord application authorization. + :type SCOPES: list + :ivar CLIENT_ID: Unique identifier of the Discord application. + :type CLIENT_ID: str + """ SCOPES = ["identify"] CLIENT_ID = "1240007913175781508" class Glow(Enum): + """ + Enumeration for representing glow effect constants. + + This class provides constants related to the glow effect, such as color, blur + intensity, and animation duration. These constants are utilized for configuring + visual effects involving glowing elements in an application. + + :cvar COLOR: Default color used for the glow effect. + :type COLOR: QColor + :cvar BLUR_BASE: The base blur intensity for the glow effect. + :type BLUR_BASE: int + :cvar BLUR_PEAK: The peak blur intensity for the glow effect when fully animated. + :type BLUR_PEAK: int + :cvar ANIM_DURATION: Duration of the glow animation in milliseconds. + :type ANIM_DURATION: int + """ COLOR = QColor(255, 140, 0, 255) BLUR_BASE = 15 BLUR_PEAK = 70 @@ -60,6 +150,20 @@ class Glow(Enum): # --------------------------------------------------------------------------- @dataclass class PlayerServerInfo: + """ + Represents server-related information about a player. + + This class is used to store and manage information about a player's server-related + permissions and session details. It keeps track of whether the player has staff privileges, + whether they are whitelisted, and their current session ID. + + :ivar is_staff: Indicates if the player has staff permissions. + :type is_staff: bool + :ivar is_whitelist: Indicates if the player is whitelisted. + :type is_whitelist: bool + :ivar session_id: The session ID of the player for the current server session. + :type session_id: str + """ is_staff: bool = False is_whitelist: bool = False session_id: str = None diff --git a/src/controllers/audio_controller.py b/src/controllers/audio_controller.py index e7e0527..47f7cf4 100644 --- a/src/controllers/audio_controller.py +++ b/src/controllers/audio_controller.py @@ -8,6 +8,22 @@ from config.constants import Resources class AudioController: + """ + Summary of what the class does. + + This class encapsulates all audio-related logic, including playback, volume + management, and mute functionality. It interfaces with configuration management, + slider controls, and mute buttons to provide a comprehensive audio control system. + + :ivar tmp: Temporary file created to store the audio resource. This file is + used by the audio playback engine during runtime. + :type tmp: tempfile._TemporaryFileWrapper + :ivar _audio_engine: Underlying audio engine responsible for playback management. + :type _audio_engine: cma.Engine + :ivar _sound: Object representing the currently playing sound, with properties + like volume and looping enabled. + :type _sound: cma.Sound + """ # Encapsule toute la logique audio : lecture, volume, mute. def __init__(self, config: ConfigManager, slider, mute_btn): @@ -51,6 +67,18 @@ class AudioController: # ------------------------------------------------------------------ def toggle_mute(self) -> None: + """ + Toggle the mute state of the audio system. + + This method toggles between muted and unmuted states. When muting, it saves + the current volume level for restoration upon unmuting. When unmuting, it + restores the volume to the previously saved level. + + :raises AttributeError: If internal attributes required for volume handling + are missing or incorrectly configured. + + :return: None + """ if not self._is_muted: self._previous_volume = self._slider.value() self._apply_volume(0) @@ -64,6 +92,15 @@ class AudioController: # ------------------------------------------------------------------ def _on_slider_changed(self, value: int) -> None: + """ + Handles the changes in the slider value and updates the appropriate + attributes and state accordingly. This method adjusts the volume + whenever the slider value changes and toggles the mute button if + necessary. + + :param value: The new slider value, representing the volume level as + an integer between 0 and 100. + """ self._is_muted = value == 0 if hasattr(self, '_sound'): # cyminiaudio attend souvent un float entre 0.0 et 1.0 @@ -72,6 +109,21 @@ class AudioController: self._refresh_mute_btn() def _apply_volume(self, value: int, save: bool = True) -> None: + """ + Adjusts and applies the volume value for a sound object and UI interaction. + + This method modifies the volume of a given sound object if it exists, updates the slider UI + to reflect the new volume value, and optionally saves this volume value in the configuration. + Mute button state is refreshed after applying the changes. + + :param value: The new volume value to apply, as an integer percentage (0-100). + :type value: int + :param save: Boolean flag indicating whether the volume should be saved to the configuration. + Defaults to True. + :type save: bool, optional + :return: This method does not return any value. + :rtype: None + """ self._slider.blockSignals(True) self._slider.setValue(value) self._slider.blockSignals(False) @@ -86,6 +138,14 @@ class AudioController: self._refresh_mute_btn() def _refresh_mute_btn(self) -> None: + """ + Updates the state of the mute button based on the current slider value. If the + slider value is 0, the button is marked as muted; otherwise, it is marked + as unmuted. The visual appearance of the button is refreshed to reflect + the updated state. + + :return: None + """ muted = self._slider.value() == 0 self._mute_btn.setProperty("muted", muted) self._mute_btn.style().unpolish(self._mute_btn) diff --git a/src/controllers/glow_animator.py b/src/controllers/glow_animator.py index 1de3e27..02ba9bb 100644 --- a/src/controllers/glow_animator.py +++ b/src/controllers/glow_animator.py @@ -5,6 +5,15 @@ from config.constants import Glow class GlowAnimator: + """ + Manages the pulsing glow effect for a widget. + + Provides functionality to animate a glowing effect with adjustable blur + radii and enables starting and stopping the animation on the target widget. + + :ivar widget: The target widget for the glow effect. + :type widget: QWidget + """ # Gère l'effet de lueur pulsée sur un widget. def __init__(self, widget): @@ -24,9 +33,22 @@ class GlowAnimator: self._anim.setLoopCount(-1) def start(self) -> None: + """ + Starts the animation and applies a graphical effect to the associated widget. + + This function sets a previously defined graphical effect on the widget and + starts the associated animation. + + :return: None + """ self._widget.setGraphicsEffect(self._effect) self._anim.start() def stop(self) -> None: + """ + Stops the animation and removes the graphics effect from the associated widget. + + :return: None + """ self._anim.stop() self._widget.setGraphicsEffect(None) diff --git a/src/controllers/window_dragger.py b/src/controllers/window_dragger.py index 27dc7d3..6675abc 100644 --- a/src/controllers/window_dragger.py +++ b/src/controllers/window_dragger.py @@ -4,22 +4,65 @@ from PySide6.QtWidgets import QMainWindow class WindowDragger: + """ + Manages window dragging behavior for a QMainWindow instance. + + Provides methods to enable click-and-drag functionality, allowing the + user to reposition a window by clicking and dragging with the left mouse + button. This class requires the use of Qt framework's event system. + + :ivar window: The QMainWindow instance that will be associated with the + dragging behavior. + :type window: QMainWindow + """ def __init__(self, window: QMainWindow): self._window = window self._drag_pos = None def mouse_press(self, event: QtGui.QMouseEvent) -> None: + """ + Handle mouse press events and determine the position offset between + the click location and the top-left corner of the window. This is + used to facilitate window dragging operations. + + :param event: The mouse event containing information about the + mouse button pressed and its position. + :type event: QtGui.QMouseEvent + :return: None + """ if event.button() == Qt.MouseButton.LeftButton: # On stocke le vecteur entre le clic et le coin haut-gauche de la fenêtre self._drag_pos = event.globalPosition().toPoint() - self._window.frameGeometry().topLeft() event.accept() # On informe Qt que l'event est géré def mouse_move(self, event: QtGui.QMouseEvent) -> None: + """ + Handles the mouse move event for repositioning a window. + + This method is used to move a window when the left mouse button is held + down, and the dragging position has been initialized. The window is moved + based on the global mouse position offset by the initial drag position. + + :param event: The mouse event containing the current state of the + mouse, including its buttons and global position. + :type event: QtGui.QMouseEvent + :return: None + """ # Vérification stricte du bouton gauche ET de l'existence du point d'ancrage if (event.buttons() & Qt.MouseButton.LeftButton) and self._drag_pos is not None: self._window.move(event.globalPosition().toPoint() - self._drag_pos) event.accept() def mouse_release(self, event: QtGui.QMouseEvent) -> None: + """ + Handles the mouse release event for the widget. + + This method is triggered when the mouse button is released, + handling necessary cleanup and accepting the event. + + :param event: The mouse release event containing details of the mouse action. + :type event: QtGui.QMouseEvent + :return: None + """ self._drag_pos = None event.accept() diff --git a/src/discord/discord_oauth.py b/src/discord/discord_oauth.py index cb5abdd..882f514 100644 --- a/src/discord/discord_oauth.py +++ b/src/discord/discord_oauth.py @@ -26,7 +26,16 @@ class OAuthCallbackHandler(BaseHTTPRequestHandler): def do_GET(self): """ - callback pour discord auth + Handles incoming GET requests and processes the callback from an OAuth authorization flow. + + Parses the URL path and verifies that it matches the expected callback endpoint. If the path does + not match "/callback", a 404 error is sent to the client. Extracts the authorization code from + query parameters if it exists, stores it, and responds to the client with a success message + rendered with the company logo. + + :raises HTTPError: Sends a 404 HTTP error if the requested path does not match "/callback". + :param self: Reference to the current instance of the class. + :return: None """ parsed_url = urlparse(self.path) if parsed_url.path != "/callback": @@ -51,7 +60,19 @@ def get_discord_client_id() -> str: # return discord user id def get_discord_user_id() -> tuple[str, str]: """ - Retourne l'id du compte discord de l'utilisateur via l'oauh discord. + Authenticate a user via Discord OAuth2 and retrieve their Discord user ID along with + the session ID. This function manages the entire OAuth flow, including opening the + authorization URL in the user's browser, handling the callback, exchanging the + authorization code for an access token, and retrieving the user's profile information. + + :raises ApiError: Raised in the following scenarios: + + - If no authorization code is received after the OAuth callback. + - If the token endpoint returns invalid JSON or if the `access_token` is missing. + - If the Discord profile endpoint returns invalid JSON or if the user's ID is missing. + + :return: A tuple containing the user's Discord ID and the associated session ID. + :rtype: tuple[str, str] """ # récupération des infos serveur la tanière diff --git a/src/discord/discord_tools.py b/src/discord/discord_tools.py index ac60e49..6ff0274 100644 --- a/src/discord/discord_tools.py +++ b/src/discord/discord_tools.py @@ -7,10 +7,25 @@ from fivemserver.get_server_token import GetServerTokenForDiscord class DiscordToken: """ - Décode le token discord + Provides functionality to decode a Discord token by utilizing an authentication + process with a predefined API URL. + + This class is designed to offer a method for securely retrieving and decoding + the Discord token after successful authentication. It ensures the proper + handling of the token and raises relevant exceptions in case of authentication + or retrieval failures. """ @staticmethod def decode_discord_token(): + """ + Decodes the Discord token by authenticating through a specified API URL and retrieving + the token value. + + :raises AuthenticationError: If the authentication with the API URL fails. + :raises TokenRetrievalError: If there is an issue retrieving the Discord token. + :return: The decoded Discord token. + :rtype: str + """ discord_token = GetServerTokenForDiscord.get_token( GetServerTokenForDiscord.authenticate(Urls.API_URL.value) ) @@ -21,7 +36,18 @@ class CheckDiscord: @staticmethod def isdiscordrunning() -> bool: """ - Vérifie si Discord est en cours d'exécution sur l'ordinateur. (Vérifie aussi pour Linux) + Checks if Discord is running on the system. + + This method iterates through the running processes on the system and + compares their names to identify whether the Discord application processes + are actively running. It checks for different variations of Discord + process names, including "discord.exe", "discordcanary.exe", "discord", + and "discord canary". + + :return: A boolean value indicating whether a Discord process is + currently running. Returns True if Discord is identified; otherwise, + returns False. + :rtype: bool """ for process in psutil.process_iter(["name"]): if ( @@ -36,8 +62,16 @@ class CheckDiscord: @staticmethod def isuserconnected() -> bool: """ - Vérifie si l'utilisateur Discord est connecté. - ⚠️ne vérifie pas le user id discord. + Determines whether a user is successfully connected to the Discord Presence service. + + This static method attempts to establish a connection with the Discord Presence + service using the provided client ID. If successful, it returns True. In case + of any exceptions during the connection attempt, the method safely handles + them and ensures the connection is closed before returning False. + + :returns: Boolean value indicating whether the user is connected to the + Discord Presence service. + :rtype: bool """ rpc = Presence(DiscordApplicationReferences.CLIENT_ID.value) try: diff --git a/src/fivemserver/auth_worker.py b/src/fivemserver/auth_worker.py index 7100edd..05dc5d6 100644 --- a/src/fivemserver/auth_worker.py +++ b/src/fivemserver/auth_worker.py @@ -7,6 +7,22 @@ from tools.http_client import ApiError # Importe ton exception personnalisée class AuthWorker(QThread): + """ + Handles authentication of a Discord user using a separate thread. + + This class is designed to manage the authentication process for a Discord + user in a non-blocking manner by utilizing a QThread. It emits signals to + communicate the authentication results back to the main UI thread. It + provides proper control mechanisms to start, stop, and manage the + lifecycle of the thread while performing secure API calls and handling + exceptions during the process. + + :ivar discord_user_id: The Discord user ID used for authentication. + :type discord_user_id: str + :ivar finished: Signal emitted upon completion, passing the success status, + session ID, and error message. + :type finished: pyqtSignal(bool, str, str) + """ # Signaux pour communiquer avec l'UI # finished(success, session_id, error_message) finished = Signal(bool, str, str) @@ -17,7 +33,24 @@ class AuthWorker(QThread): self._is_running = True def run(self): - """Exécuté dans un thread séparé.""" + """ + Executes the authentication and user registration process for a Discord user. + This method handles obtaining an authentication token from the server, registering + the Discord user with the retrieved token, and communicates the result through + a signal. + + :raises ApiError: If an error occurs during API-related operations, like failing + to fetch a valid token or registration issues. + :raises Exception: For critical errors such as SSL or networking issues encountered + during the process. + :param self: The instance of the current object executing the method. + + :return: Emits a signal indicating the completion of authentication and registration + process with the following values: + - A boolean indicating success (True) or failure (False). + - The session ID if the authentication is successful, otherwise an empty string. + - An error message explaining the reason for failure, if any. + """ session_id = "" error_msg = "" success = False @@ -61,6 +94,13 @@ class AuthWorker(QThread): self.finished.emit(success, session_id, error_msg) def stop(self): - """Permet d'annuler le thread proprement.""" + """ + Stops the running thread with a request for interruption. + + This method sets the thread state to not running and requests an + interruption of the thread execution. + + :return: None + """ self._is_running = False self.requestInterruption() # Demande l'interruption à QThread diff --git a/src/fivemserver/fivemlauncher.py b/src/fivemserver/fivemlauncher.py index be19ec2..9205ffd 100644 --- a/src/fivemserver/fivemlauncher.py +++ b/src/fivemserver/fivemlauncher.py @@ -5,16 +5,34 @@ from config.constants import Urls class FiveMLauncher: + """ + A class to manage the launching of the FiveM application. + + This class provides functionality to launch the FiveM executable if the + specified path exists or to open the default FiveM URL if no valid executable + is found. It is designed to streamline the process of launching the FiveM + application for end-users with minimal configuration required. + + :ivar fivem_path: Path to the FiveM executable on the system. + :type fivem_path: str + """ def __init__(self, fivem_path: str): self.fivem_path = os.path.expandvars(fivem_path) @staticmethod def launch(): """ - if not os.path.exists(self.fivem_path): - raise FileNotFoundError("❌ FiveM.exe introuvable") + Launches an external application by opening the specified URL using the subprocess module. - subprocess.Popen(self.fivem_path, shell=True) + This static method uses the `subprocess.Popen` function to open a URL, effectively launching + an external application (e.g., a browser or a platform-specific client) associated with the + given URL scheme. It does not return any value and does not handle failures directly. + + Raises: + Any exceptions raised by `subprocess.Popen`, such as `FileNotFoundError` or `PermissionError`, + will propagate unless handled by the caller. + + :return: None """ subprocess.Popen(f"explorer {Urls.FIVEMURL.value}") diff --git a/src/fivemserver/get_server_token.py b/src/fivemserver/get_server_token.py index 1ed02b8..66cd873 100644 --- a/src/fivemserver/get_server_token.py +++ b/src/fivemserver/get_server_token.py @@ -10,10 +10,35 @@ from tools.http_client import http_post, ApiError class GetServerTokenForDiscord: + """ + Provides functionalities for authenticating a client with a server, retrieving + session tokens, and registering a Discord user. This class utilizes ECDH key + exchange for secure communication, and is used to interact with specific server + endpoints required for authentication and token handling. + + :ivar derived_key: The derived shared key generated during the ECDH key exchange + in the authentication process. Holds the derived key or None if the + authentication process hasn't been completed. + :type derived_key: bytes | None + """ derived_key: bytes | None = None @staticmethod def authenticate(server: str | None = Urls.API_URL.value) -> str: + """ + Authenticate with a server by performing Elliptic Curve Diffie-Hellman (ECDH) key exchange and retrieve a session ID. + + This method uses ECDH to generate a shared key between the client and server. The client generates a private/public key + pair, then sends its public key to the server. The server responds with its public key, which is used to establish a + shared secret. The shared key is derived using HKDF for further secure communication. A session ID is returned + upon successful authentication. + + :param server: The URL of the server to authenticate with. If not provided, the default API URL is used. + :type server: str | None + :return: The session ID provided by the server upon successful authentication. + :rtype: str + :raises ApiError: If an error occurs during authentication or if the server response is invalid. + """ if server is None: server = Urls.API_URL.value @@ -60,6 +85,22 @@ class GetServerTokenForDiscord: @staticmethod def get_token(session_id: bytes, server: str | None = Urls.API_URL.value) -> bytes: + """ + Retrieve a Discord token using the provided session ID and server URL. + + This method sends an HTTP POST request to the specified server to fetch + an encrypted token, decrypts it using the previously established derived + key, and returns the resulting token. + + :param session_id: Session ID used for authentication. + :type session_id: bytes + :param server: Optional server URL for the token endpoint. If not provided, a default URL is used. + :type server: str | None + :return: The decrypted Discord token. + :rtype: bytes + :raises ApiError: Raised if the derived key is missing, if the server response contains invalid + data, or if JSON parsing fails. + """ # ========================== # DISCORD TOKEN # ========================== @@ -94,6 +135,24 @@ class GetServerTokenForDiscord: @staticmethod def register_discord_user( discord_user_id: str, session_id: str, server: str | None = Urls.API_URL.value) -> bool: + """ + Registers a Discord user to the external server using the provided session ID and server URL. + + This method sends a POST request to the specific API endpoint to register the + Discord user. If the `server` parameter is not provided, a default URL will be + used as the server base URL. The registration is completed when the external + server responds successfully, returning a boolean status indicating success. + + :param discord_user_id: The unique identifier for the Discord user to register. + :type discord_user_id: str + :param session_id: The session ID to authenticate the registration request. + :type session_id: str + :param server: The base URL of the server. Defaults to the pre-configured API URL. + :type server: Optional[str] + :return: True if the user registration is successful, False otherwise. + :rtype: bool + :raises ApiError: If the API request fails or an invalid JSON response is returned. + """ if server is None: server = Urls.API_URL.value diff --git a/src/fivemserver/queuemanager.py b/src/fivemserver/queuemanager.py index 2efcf5e..a00239c 100644 --- a/src/fivemserver/queuemanager.py +++ b/src/fivemserver/queuemanager.py @@ -8,6 +8,17 @@ from config.constants import Urls, ApiEndPoints class QueueManager: + """ + Manages user interaction with a queue system, including joining, checking status, + leaving, and refreshing the session. This class provides mechanisms to handle + user updates while maintaining the session's freshness and responsiveness. + + :ivar user_id: Unique identifier for the user in the queue system. + :type user_id: str + :ivar on_update: Callback function invoked to provide updates on the + queue status. + :type on_update: Callable[[str], None + """ def __init__(self, user_id: str, on_update: Callable[[str], None]): self.user_id = user_id self.on_update = on_update @@ -16,9 +27,22 @@ class QueueManager: self.REFRESH_INTERVAL = 30 # ← en secondes def stop(self): + """ + Stops the running state of the current object or process. This method is + typically used to change an internal state that determines whether a process + is actively running or not. + + :return: None + """ self._running = False def join_queue(self) -> dict: + """ + Joins the user to the queue by sending a POST request with the user's UUID. + + :return: The response of the API in JSON format as a dictionary. + :rtype: dict + """ res = requests.post( f"{Urls.API_URL.value}/{ApiEndPoints.QUEUE_JOIN.value}", json={"uuid": self.user_id}, @@ -27,12 +51,27 @@ class QueueManager: return res.json() def check_status(self) -> dict: + """ + Fetches the current status of the queue for the user. + + This method sends a GET request to the specified API endpoint to retrieve the + queue status for the given user and returns the response as a dictionary. + + :return: A dictionary containing the response from the API. + :rtype: dict + """ res = requests.get( f"{Urls.API_URL.value}/{ApiEndPoints.QUEUE_STATUS.value}/{self.user_id}" ) return res.json() def leave_queue(self): + """ + Removes the user identified by their UUID from the active queue. + + :return: None + :rtype: None + """ requests.post( f"{Urls.API_URL.value}/{ApiEndPoints.QUEUE_LEAVE.value}", json={"uuid": self.user_id}, @@ -40,7 +79,16 @@ class QueueManager: ) def refresh_session(self): - """Informe le serveur que le client est toujours présent, sans changer le session_id.""" + """ + Refreshes the session by sending a POST request to the session refresh endpoint. + + This function attempts to use the current session ID to send a request to refresh + the user's session. If the session ID is unavailable or the request fails, the + function exits without raising any exceptions, allowing the session on the server + side to expire naturally. + + :return: None + """ session_id = PlayerServerInfo.session_id if not session_id: return @@ -58,6 +106,19 @@ class QueueManager: pass # On ignore silencieusement, le serveur expirera de lui-même def start(self): + """ + Executes the queue processing logic, manages the session refresh, and handles updates + related to queue position. + + This method starts by checking the user's initial position in the queue. If the position + is at the front (position 0), it updates the status to "ok" and stops further execution. + Otherwise, it enters a loop where it periodically refreshes the session and checks the + queue position until the user's position reaches the front or the process is stopped. + + :raises Exception: If there is an issue in checking the status or refreshing the session. + + :return: None + """ join = self.join_queue() if join.get("position") == 0: diff --git a/src/fivemserver/whitelistmanager.py b/src/fivemserver/whitelistmanager.py index 8266339..d4ce24a 100644 --- a/src/fivemserver/whitelistmanager.py +++ b/src/fivemserver/whitelistmanager.py @@ -4,8 +4,26 @@ from tools.http_client import ApiError, http_get class WhiteList: + """ + Handles whitelist checking functionality for a given user and URL. + + Provides methods for communicating with an API endpoint to determine + if a user is whitelisted and/or has staff permissions on a server. + + """ @staticmethod def check_whitelist(url: str, discord_user_id: str) -> None: + """ + Checks if a Discord user is in the whitelist of a specific server by making a request to + a given API endpoint. Also updates class attributes `is_whitelist` and `is_staff` with + the results from the API. + + :param url: The base URL of the API endpoint to check the whitelist. + :type url: str + :param discord_user_id: The Discord user ID to check against the whitelist. + :type discord_user_id: str + :raises ApiError: If a network-related issue occurs or if the JSON response is invalid. + """ try: api_data = http_get(f"{url}/{ApiEndPoints.WHITELIST_URL_ENDPOINT.value}/{discord_user_id}").json() except ApiError: diff --git a/src/main.py b/src/main.py index 51a09f6..76faee2 100644 --- a/src/main.py +++ b/src/main.py @@ -38,7 +38,17 @@ if sys.platform.startswith("win"): # Setup # --------------------------------------------------------------------------- def setup_environment(app, bundle_dir): - # Utilisation de pathlib pour la robustesse + """ + Sets up the environment for the given application by loading a custom stylesheet and font. It attempts to apply + a stylesheet from a specified bundle directory and set a custom font. If either action fails, the function uses + fallback options. + + :param app: The application instance where the environment setup is to be applied. + :type app: QApplication + :param bundle_dir: The directory path containing the resources like stylesheets and fonts. + :type bundle_dir: str + :return: None + """ style_path = Path(bundle_dir) / "styles" / "styles.qss" # Tentative de chargement du style @@ -60,6 +70,16 @@ def setup_environment(app, bundle_dir): # Font helper # --------------------------------------------------------------------------- def load_custom_font() -> str: + """ + Loads a custom font resource into the application and retrieves its primary font family. + This function ensures that a font specified in the application's resources is properly loaded + and ready for use. If the font cannot be loaded or no font families are found, a runtime error is raised. + + :raises RuntimeError: If the font cannot be loaded from resources or if no font families are found + within the loaded font. + :return: The primary font family name of the loaded custom font. + :rtype: str + """ font_id = QFontDatabase.addApplicationFont(Resources.FONT.value) if font_id == -1: raise RuntimeError("Failed to load font from resources.") diff --git a/src/tools/http_client.py b/src/tools/http_client.py index f38410f..e715727 100644 --- a/src/tools/http_client.py +++ b/src/tools/http_client.py @@ -11,6 +11,23 @@ HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"] logger = logging.getLogger(__name__) class ApiError(RuntimeError): + """ + Represents an error that occurs during API operations. + + This class is used to handle API-related errors that include additional + information such as the URL being accessed and the corresponding status + code. It extends the RuntimeError class and provides a structured format + for encapsulating these details. + + :ivar url: The URL associated with the API operation where the error + occurred. This attribute is optional and may be None if the URL is + not available. + :type url: str | None + :ivar status_code: The HTTP status code returned by the API that + indicates the cause of the error. This attribute is optional and may + be None if the status code is not available. + :type status_code: int | None + """ def __init__(self, message: str, *, url: str | None = None, status_code: int | None = None) -> None: super().__init__(message) self.url = url @@ -27,6 +44,23 @@ def http_request( json: dict[str, Any] | None = None, timeout: int = DEFAULT_TIMEOUT, ) -> requests.Response: + """ + Executes an HTTP request using the specified parameters and returns the server's response. This + method is a wrapper around the ``requests`` library's ``request`` function, including error + handling and logging for failed requests. + + :param method: The HTTP method to use for the request, such as GET, POST, PUT, DELETE, etc. + :param url: The URL to which the HTTP request is sent. + :param headers: An optional dictionary of headers to include in the request. + :param params: Optional URL parameters to append to the request. + :param data: Optional form data to include in the request body. + :param json: Optional JSON payload to include in the request body. + :param timeout: The maximum time, in seconds, to wait for the request to complete before raising + a timeout exception. Defaults to `DEFAULT_TIMEOUT`. + :return: The HTTP response object returned by the server. + :raises ApiError: If the HTTP request fails or an exception occurs, an ApiError is raised with + details of the failure and the corresponding HTTP status code if available. + """ try: response = requests.request( method=method, @@ -58,6 +92,23 @@ def http_get( params: dict[str, Any] | None = None, timeout: int = DEFAULT_TIMEOUT, ) -> requests.Response: + """ + Makes an HTTP GET request to the given URL with optional headers, query parameters, + and a timeout value. Returns the response object upon successful execution of the + request. This function internally utilizes the `http_request` method to perform + the actual HTTP operation. + + :param url: The URL to which the GET request is sent. + :type url: str + :param headers: Optional headers to include with the HTTP request. + :type headers: dict[str, str] | None + :param params: Optional query parameters to include in the URL of the HTTP request. + :type params: dict[str, Any] | None + :param timeout: The timeout value (in seconds) for the request. + :type timeout: int + :return: The response object containing the HTTP response data. + :rtype: requests.Response + """ return http_request("GET", url, headers=headers, params=params, timeout=timeout) @@ -69,4 +120,25 @@ def http_post( json: dict[str, Any] | None = None, timeout: int = DEFAULT_TIMEOUT, ) -> requests.Response: + """ + Sends an HTTP POST request to the specified URL with optional headers, data, JSON, + and a customizable timeout value. Simplifies making POST requests through a + user-friendly interface. + + :param url: The target URL where the POST request will be sent. + :type url: str + :param headers: Optional dictionary of HTTP headers to include in the request. + :type headers: dict[str, str] | None + :param data: Optional dictionary representing form-encoded data to be sent in the + request body. + :type data: dict[str, Any] | None + :param json: Optional dictionary representing JSON data to be serialized and sent in + the request body. + :type json: dict[str, Any] | None + :param timeout: The maximum amount of time, in seconds, to wait for the request to + be completed. Uses a default timeout value if unspecified. + :type timeout: int + :return: The HTTP response received after the POST request is completed. + :rtype: requests.Response + """ return http_request("POST", url, headers=headers, data=data, json=json, timeout=timeout) diff --git a/src/tools/utils.py b/src/tools/utils.py index f49b68a..699aa29 100644 --- a/src/tools/utils.py +++ b/src/tools/utils.py @@ -6,21 +6,48 @@ from PySide6.QtWidgets import QApplication PROJECT_ROOT = Path(__file__).resolve().parents[2] def get_internal_dir() -> Path: - # Retourne le chemin vers les ressources figées à l'intérieur de l'EXE (_MEIPASS). - # En mode script, retourne le dossier du fichier .py. + """ + Returns the internal directory path for the application. + + This function determines the internal directory path depending on whether the + application is running in a frozen state (e.g., bundled by tools like PyInstaller) or + in a standard Python environment. In a frozen state, it retrieves the path from the + frozen application's temporary directory. Otherwise, it computes the directory path + from the current file's location. + + :return: The resolved internal directory path for the application. + :rtype: Path + """ if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): return Path(sys._MEIPASS).resolve() return Path(__file__).resolve().parents[2] def get_executable_dir() -> Path: - # Retourne le chemin du dossier contenant réellement le fichier .exe. - # C'est ici que se trouve votre 'config.json'. + """ + Determines the directory of the current executable or script. + + This function identifies the directory where the running executable or script + is located. If the program is frozen (e.g., bundled into an executable using + tools like PyInstaller), it returns the directory of the executable. Otherwise, + it resolves the parent directory of the script two levels higher. + + :return: The directory path of the executable or script. + :rtype: Path + """ if getattr(sys, 'frozen', False): # sys.executable est le chemin complet vers l'application .exe return Path(sys.executable).parent.resolve() return Path(__file__).resolve().parents[2] def quit_application(exit_code: int = 0) -> None: + """ + Terminates the application by closing all windows, exiting the QApplication instance + (if it exists), and finally calling sys.exit with the provided exit code. + + :param exit_code: Integer specifying the exit code to terminate the application. + Default is 0. + :return: None + """ app = QApplication.instance() if app is not None: app.closeAllWindows() diff --git a/src/ui/custom_message_box.py b/src/ui/custom_message_box.py index d3550d0..07805fc 100644 --- a/src/ui/custom_message_box.py +++ b/src/ui/custom_message_box.py @@ -5,6 +5,20 @@ from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, class CustomMessageBox(QDialog): + """ + A custom message box dialog class for displaying notifications with configurable title, message, + icon type, and button layout. The class provides a visually enhanced, frameless message box with + fade-in animation and supports drag movement by clicking on the title bar or any non-button area. + + :ivar INFO: Constant representing the informational message type. + :type INFO: str + :ivar WARNING: Constant representing the warning message type. + :type WARNING: str + :ivar OK: Constant representing the "OK" button configuration. + :type OK: str + :ivar OK_CANCEL: Constant representing the "OK" and "Cancel" button configuration. + :type OK_CANCEL: str + """ # Enums pour la configuration INFO = "info" WARNING = "warning" @@ -114,20 +128,58 @@ class CustomMessageBox(QDialog): self.old_pos = None def showEvent(self, event): + """ + Handles the show event and triggers the fade animation start. + + :param event: The show event that occurs when the widget is shown. It is + passed to the method automatically as an argument. + :type event: QShowEvent + + :return: None + """ super().showEvent(event) self.fade_anim.start() def mousePressEvent(self, e): + """ + Handles the mouse press event for the widget. Enables movement + of the widget only when the left mouse button is clicked and + the click occurs on the title bar or anywhere except on buttons. + + :param e: The mouse event object containing information about the + mouse press, such as the clicked button and position. + Expected to be an instance of QMouseEvent. + """ # On permet le déplacement uniquement si on clique sur la barre de titre # ou n'importe où sauf sur les boutons if e.button() == Qt.LeftButton: self.old_pos = e.globalPosition().toPoint() def mouseMoveEvent(self, e): + """ + Handles the mouse move event, allowing the object to move along with the mouse drag. + + This method calculates the difference between the current global position of the mouse + and the previously recorded position, then adjusts the position of the object + accordingly. It updates the stored position to the current global position at the end + of the move. + + :param e: The mouse event containing the current position of the mouse. + :type e: QMouseEvent + """ if self.old_pos: delta = e.globalPosition().toPoint() - self.old_pos self.move(self.x() + delta.x(), self.y() + delta.y()) self.old_pos = e.globalPosition().toPoint() def mouseReleaseEvent(self, e): + """ + Handles the mouse release event for the associated widget. + + This method is triggered when a mouse button is released while interacting + with the widget. It resets any stored state related to the mouse press. + + :param e: The mouse release event received. + :type e: QMouseEvent + """ self.old_pos = None diff --git a/src/ui/error_dialog.py b/src/ui/error_dialog.py index b2592cc..b7e7fdb 100644 --- a/src/ui/error_dialog.py +++ b/src/ui/error_dialog.py @@ -4,6 +4,20 @@ from ui.custom_message_box import CustomMessageBox def show_qt_error(parent: QWidget | None, title: str, message: str) -> None: + """ + Displays an error message dialog with a given title and message. The dialog is + displayed as a warning type with an "OK" button for user acknowledgment. + + :param parent: The parent QWidget to associate the error dialog with. Can be set + to None to display the dialog without an explicit parent. + :type parent: QWidget | None + :param title: The title to display in the error dialog window. + :type title: str + :param message: The message content to display in the error dialog. + :type message: str + :return: This function does not return a value. + :rtype: None + """ msg = CustomMessageBox( title=title, message=message, diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 2bc0bd9..eba3db8 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -29,6 +29,31 @@ if platform.startswith('linux'): environ["QT_QPA_PLATFORM"] = "xcb" class MainWindow(QMainWindow): + """ + MainWindow class for managing the main application window and its functionalities. + + This class is responsible for managing the UI, user interactions, and application state. + It provides mechanisms to handle user authentication, connection to external services, and queued tasks. + The class is designed to be used with the PyQt framework and provides logic for dynamically updating + the UI elements and interacting with other components. + + :ivar ui: The main UI object for managing visual components and layouts. + :type ui: Ui_MainWindow + :ivar config: Configuration manager instance for handling application settings. + :type config: ConfigManager + :ivar stored_user_id: The Discord user ID stored in the configuration. + :type stored_user_id: str + :ivar queue_thread: The thread handling queue operations. + :type queue_thread: Optional[Thread] + :ivar queue_position_value: The current queue position. + :type queue_position_value: Optional[int] + :ivar close_timer: Timer object for handling the final closure of the application. + :type close_timer: QTimer + :ivar countdown_timer: Timer object for updating visual countdown timers. + :type countdown_timer: QTimer + :ivar remaining_time: The remaining time for queue or countdown operations, in seconds. + :type remaining_time: int + """ #update = Signal(str) # Reçoit les callbacks de QueueManager def __init__(self, bundle_dir: str, config_manager: ConfigManager): @@ -146,6 +171,13 @@ class MainWindow(QMainWindow): # ------------------------------------------------------------------ def _connect_signals(self) -> None: + """ + Establishes connections between UI elements and their corresponding + functionality. Each signal emitted by a user interaction with a specific + UI button is linked to its respective handler method. + + :return: None + """ self.ui.close_btn.clicked.connect(self.close) self.ui.minimize_btn.clicked.connect(self.showMinimized) self.ui.connexion_btn.clicked.connect(self._on_connexion) @@ -155,6 +187,13 @@ class MainWindow(QMainWindow): self.ui.no_whitelist_btn.clicked.connect(self.close) def _center_window(self) -> None: + """ + Centers the window on the screen that the cursor is currently located on. + If the cursor is not on any screen, it uses the primary screen for centering. + The window's geometry is adjusted and moved accordingly. + + :return: None + """ self.adjustSize() screen = ( QGuiApplication.screenAt(QCursor.pos()) @@ -169,8 +208,15 @@ class MainWindow(QMainWindow): # ------------------------------------------------------------------ def _on_connexion(self) -> None: - """Appelée lors du clic sur le bouton connexion.""" + """ + Handles the user connection process, including verification of prerequisites, + session validation, and background authentication for the current user. This + method ensures a smooth user experience by implementing UI locking during + authentication and proceeding with appropriate actions if a valid session + already exists. + :return: None + """ # 1. Sécurités de base if not self.stored_user_id or self.stored_user_id.isspace(): show_qt_error(self, "Connexion impossible", "Identifiant Discord absent.") @@ -195,13 +241,38 @@ class MainWindow(QMainWindow): @staticmethod def _on_discord() -> None: + """ + Opens the Discord URL in the default web browser. + + This method is a static utility function that launches the Discord URL defined + in the Urls enumeration. It uses the `webbrowser.open` method to handle the + opening process. + + :return: Nothing is returned + :rtype: None + """ webbrowser.open(Urls.DISCORD.value) def _on_intranet(self) -> None: + """ + Opens the intranet URL using the default web browser and starts the glow process. + + :return: None + """ webbrowser.open(Urls.INTRANET.value) self._glow.start() def _on_discord_auth_btn(self) -> None: + """ + Handles the authentication process for Discord when the auth button is clicked. + + This method retrieves the Discord user ID and token via OAuth, updates the + local configuration, and initiates a background worker to handle server + registration. The UI button is disabled and the cursor is set to a waiting + state during the network operations to prevent multiple attempts. + + :return: None + """ try: # 1. Récupération OAuth (souvent via navigateur, assez rapide) test = discord_oauth.get_discord_user_id() @@ -231,7 +302,22 @@ class MainWindow(QMainWindow): show_qt_error(self, "Erreur", f"Une erreur inattendue est survenue : {exc}") def _on_discord_auth_finished(self, success: bool, session_id: str, error_message: str): - """Callback après l'enregistrement suite à une auth Discord.""" + """ + Handles the completion of the Discord authentication process. + + This method is triggered once the Discord authentication flow is completed. + It determines the success or failure state of the process and accordingly + triggers user interface updates and calls associated actions, such as verifying + the whitelist status. + + :param success: Indicates whether the authentication was successful. + :type success: bool + :param session_id: The session ID obtained after the authentication process. + :type session_id: str + :param error_message: The error message returned if the process failed. + :type error_message: str + :return: None + """ QGuiApplication.restoreOverrideCursor() self.ui.discord_auth_btn.setEnabled(True) @@ -258,18 +344,49 @@ class MainWindow(QMainWindow): # ------------------------------------------------------------------ def mousePressEvent(self, event: QMouseEvent) -> None: - # On délègue au dragger + """ + Handles mouse press event for the widget. + + This method is invoked when a mouse button is pressed over the widget. It + delegates the event to an internal dragger object for handling. If the event + is not marked as accepted by the dragger, the parent class implementation + of the mouse press event is called. + + :param event: The QMouseEvent instance representing the mouse press event. + :type event: QMouseEvent + :return: None + """ self._dragger.mouse_press(event) # On ne remonte pas au parent si on a déjà "accepté" l'event if not event.isAccepted(): super().mousePressEvent(event) def mouseMoveEvent(self, event: QMouseEvent) -> None: + """ + Handle the mouse move event. + + This method processes the mouse movement event and delegates the functionality + to the associated dragger. If the event is not accepted after processing, it + passes the event up to the superclass implementation. + + :param event: The mouse event being handled. + :type event: QMouseEvent + :return: None + """ self._dragger.mouse_move(event) if not event.isAccepted(): super().mouseMoveEvent(event) def mouseReleaseEvent(self, event: QMouseEvent) -> None: + """ + Handles the mouse release event. This function processes the event, allowing + custom handling logic through a dragger object, and then delegates to the parent + class's handler if the event is not already accepted. + + :param event: The mouse release event to be processed. + :type event: QMouseEvent + :return: None + """ self._dragger.mouse_release(event) if not event.isAccepted(): super().mouseReleaseEvent(event) @@ -279,6 +396,18 @@ class MainWindow(QMainWindow): # ------------------------------------------------------------------ def closeEvent(self, event) -> None: + """ + Handles the close event of the application. + + This method is executed when the application window is being closed. It ensures + the termination of any running threads, saves the application configuration, and + performs necessary cleanup routines before invoking the superclass's close event + handler. + + :param event: The close event instance that provides event-related data and allows + control over the closing process. + :return: None + """ if self.queue_thread and self.queue_thread.isRunning(): self.queue_thread.stop() self.queue_thread.wait() # Attend que le thread se termine proprement @@ -287,7 +416,15 @@ class MainWindow(QMainWindow): super().closeEvent(event) def cleanup(self): - """À appeler lors de la fermeture de la fenêtre principale""" + """ + Stops all active timers and performs cleanup operations. + + This method ensures that any active resources, such as timers or temporary + files, are properly stopped or removed to avoid resource leaks. It is a + utility to clean up and reset components associated with the instance. + + :raises OSError: If an error occurs during the removal of the temporary file. + """ if self.close_timer: self.close_timer.stop() @@ -315,7 +452,17 @@ class MainWindow(QMainWindow): # Schedule de fermeture du launcher # ------------------------------------------------------------------ def schedule_close(self, delay_ms: int = 60000): - """Lance ou redémarre le compte à rebours de fermeture.""" + """ + Schedules a process to close after a specified delay in milliseconds. The countdown + timer also starts to reflect the remaining time in seconds. Timers already running + are reset to prevent duplicate behavior, and the display is updated immediately. + + :param delay_ms: The delay in milliseconds after which the process should + be closed. Defaults to 60000 milliseconds (1 minute) if not specified + or an invalid value is provided. + :type delay_ms: int + :return: None + """ # 1. Sécurité sur les entrées if not isinstance(delay_ms, int) or delay_ms <= 0: delay_ms = 60000 @@ -335,7 +482,17 @@ class MainWindow(QMainWindow): self._update_countdown_display() def _update_countdown(self): - """Appelée toutes les secondes par le countdown_timer.""" + """ + Decreases the remaining time by one and updates the countdown display. + + This method is responsible for decrementing the `remaining_time` attribute + on each invocation. If the remaining time reaches zero or below, the + `countdown_timer` is stopped immediately and no further action is + performed in this method. Otherwise, it proceeds to update the countdown + display to reflect the current remaining time. + + :return: None + """ self.remaining_time -= 1 if self.remaining_time <= 0: @@ -345,7 +502,13 @@ class MainWindow(QMainWindow): self._update_countdown_display() def _update_countdown_display(self): - """Met à jour le texte dans l'interface.""" + """ + Updates the countdown display on the user interface with the remaining time + in minutes and seconds format. The remaining time will always be formatted + as a positive value, even if it is less than zero. + + :return: None + """ minutes = max(0, self.remaining_time // 60) seconds = max(0, self.remaining_time % 60) self.ui.queue_lbl.setText(f"⏳ Fermeture dans {minutes:02d}:{seconds:02d}") @@ -354,6 +517,17 @@ class MainWindow(QMainWindow): # Queue managment # ------------------------------------------------------------------ def start_queue(self): + """ + Starts a separate thread for managing a queue and connects it to the relevant + update handler. This method initializes and starts a `QueueThread` instance + using the stored user ID and links it with the update handling functionality + to ensure real-time queue updates are properly handled. + + All threading and queue update mechanisms are encapsulated within this method. + + :param self: Reference to the calling object. + :return: None + """ self.queue_thread = QueueThread(self.stored_user_id, parent=self) # ← parent=self self.queue_thread.update.connect(self.handle_update) self.queue_thread.start() @@ -362,6 +536,17 @@ class MainWindow(QMainWindow): # self.handle_update("position:3:10") def handle_update(self, message: str): + """ + Handles updates based on a given message and performs relevant UI actions. + + :param message: The message received, which determines the update logic. Accepts: + - "ok": Indicates that it's the user's turn, updating the UI accordingly. + - "ready": Equivalent to "ok", indicating the user's turn. + - Messages starting with "position:": Provide the current position and total + in the queue in the format "position::", updating the queue + information in the UI. + :return: None + """ if message == "ok": self.ui.queue_lbl.setVisible(True) self.ui.queue_position.setVisible(False) @@ -385,6 +570,26 @@ class MainWindow(QMainWindow): pass def _ensure_server_session(self) -> bool: + """ + Ensures the establishment of a server session, handling authentication + and session registration for a Discord user. + + The method first checks if a valid stored user ID exists. If there is + no stored user ID or it is blank, the session creation will fail. If a + session token (`session_id`) is already present, it will return + immediately with success. Otherwise, it proceeds to authenticate with + the server to obtain a session token and registers the Discord user + against this session. + + Errors during registration are caught and handled separately, and the + method logs any issues encountered during the process. If the + authentication and registration are successful, the session is + considered established. + + :return: A boolean indicating whether the server session was successfully + established. + :rtype: bool + """ if not self.stored_user_id or self.stored_user_id.isspace(): return False @@ -417,7 +622,16 @@ class MainWindow(QMainWindow): return False def _on_auth_finished(self, success: bool, session_id: str, error_message: str): - """Appelée quand le AuthWorker a terminé.""" + """ + Handles the completion of the authentication process by updating the UI, + cleaning up the worker, and proceeding with subsequent logic based on the + authentication result. + + :param success: A boolean indicating whether the authentication was successful. + :param session_id: The session identifier returned upon successful authentication. + :param error_message: A descriptive error message in case authentication failed. + :return: None + """ # 1. Restauration de l'UI QGuiApplication.restoreOverrideCursor() self.ui.connexion_btn.setEnabled(True) @@ -439,7 +653,19 @@ class MainWindow(QMainWindow): f"Impossible de se connecter au serveur.\n\n{error_message}") def _proceed_to_queue_or_launch(self): - """Continuation de la logique après une session valide.""" + """ + Handles the process of either starting the queue, updating the user's + position in the queue, or launching the FiveM application if all conditions + are met. This function first checks the queue position. If there is none, + the queue is started. If the current position in the queue is not at the + front (position 0), an error message is displayed. If the user is at the + front of the queue, the application launches, and further connections are + disabled. + + :raises Exception: If an error occurs during the launching process. + + :return: None + """ try: # Si on n'a pas encore de position en file d'attente, on la lance if self.queue_position_value is None: @@ -460,6 +686,16 @@ class MainWindow(QMainWindow): show_qt_error(self, "Erreur de Lancement", f"Détails : {exc}") class QueueThread(QThread): + """ + QueueThread class. + + Implements a thread that manages a queue operation using a `QueueManager`. The + class emits a signal to update external components whenever a relevant change + occurs in the queue. Designed to run processing tasks in a separate thread. + + :ivar update: Signal emitted with a string message to notify about updates. + :type update: Signal + """ update = Signal(str) def __init__(self, user_id: str, parent=None): # ← parent=None @@ -470,7 +706,24 @@ class QueueThread(QThread): ) def run(self): + """ + Initiates the start operation for the associated manager. This method + is responsible for invoking the `start` method of the `manager` object, + ensuring that the necessary operations managed by the `manager` commence + execution. + + :return: None + :rtype: None + """ self.manager.start() def stop(self): + """ + Stops the operation managed by the associated manager instance. + + This method delegates the stop operation to the `manager` object, ensuring + the termination of any ongoing processes or operations it controls. + + :return: None + """ self.manager.stop() diff --git a/src/ui/win_dwm.py b/src/ui/win_dwm.py index 9cceb33..1d7ad5f 100644 --- a/src/ui/win_dwm.py +++ b/src/ui/win_dwm.py @@ -2,7 +2,14 @@ import ctypes from sys import platform def enable_blur_behind(hwnd: int) -> None: - """Force DWM à activer la composition derrière la fenêtre.""" + """ + Enable the blur-behind effect on the specified window handle (HWND). This function enables + the glass/blur effect on supported Windows environments, enhancing the UI appearance. + + :param hwnd: The handle of the window (HWND) where the blur-behind effect will be applied. + :type hwnd: int + :return: None + """ if not platform.startswith('win'): return try: