Writing the doc

This commit is contained in:
2026-03-27 17:02:59 +01:00
parent ac9b1b8c30
commit b5820eb301
19 changed files with 1108 additions and 31 deletions
+156 -4
View File
@@ -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))
+104
View File
@@ -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
+60
View File
@@ -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)
+22
View File
@@ -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)
+43
View File
@@ -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()
+23 -2
View File
@@ -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
+38 -4
View File
@@ -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:
+42 -2
View File
@@ -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
+21 -3
View File
@@ -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}")
+59
View File
@@ -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
+62 -1
View File
@@ -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:
+18
View File
@@ -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:
+21 -1
View File
@@ -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.")
+72
View File
@@ -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)
+31 -4
View File
@@ -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()
+52
View File
@@ -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
+14
View File
@@ -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,
+262 -9
View File
@@ -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:<current>:<total>", 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()
+8 -1
View File
@@ -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: