ajout du code pour l'api REST et discord oauth. Refacto

This commit is contained in:
2026-03-16 21:42:25 +01:00
parent 5a6470511a
commit 54a8d5c40f
12 changed files with 324 additions and 48 deletions

View File

@@ -2,7 +2,7 @@ import json
from pathlib import Path
from typing import Any, Callable, NotRequired, TypedDict, cast
from utils import get_bundle_dir
from tools.utils import get_executable_dir
class ConfigData(TypedDict):
discord_user_id: NotRequired[str]
@@ -16,7 +16,7 @@ class ConfigField(TypedDict):
validator: Validator
normalizer: Normalizer
CONFIG_PATH = get_bundle_dir() / "config.json"
CONFIG_PATH = get_executable_dir() / "config.json"
DISCORD_USER_KEY = "discord_user_id"
VOLUME_KEY = "volume"

View File

@@ -3,7 +3,7 @@ from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput
from config.config_manager import ConfigManager, VOLUME_KEY
from constants import Resources
from tools.constants import Resources
class AudioController:
# Encapsule toute la logique audio : lecture, volume, mute.

View File

@@ -1,7 +1,7 @@
from PySide6.QtCore import QPropertyAnimation, QEasingCurve
from PySide6.QtWidgets import QGraphicsDropShadowEffect
from constants import Glow
from tools.constants import Glow
class GlowAnimator:

View File

@@ -1,5 +1,5 @@
import sys
from utils import get_bundle_dir
from tools.utils import get_internal_dir
from PySide6.QtCore import QResource
from PySide6.QtGui import QFontDatabase, QFont
@@ -9,21 +9,18 @@ from PySide6.QtWidgets import QApplication
import resources # noqa: F401 - required to register Qt resources
from ui.main_window import MainWindow
from constants import Resources
from tools.constants import Resources
# ---------------------------------------------------------------------------
# Bundle path resolution
# ---------------------------------------------------------------------------
bundle_dir = get_bundle_dir()
bundle_dir = get_internal_dir()
QResource.registerResource(f"{bundle_dir}/resources.py")
# ---------------------------------------------------------------------------
# Font helper
# ---------------------------------------------------------------------------
def load_custom_font() -> str:
font_id = QFontDatabase.addApplicationFont(Resources.FONT.value)
if font_id == -1:
@@ -37,7 +34,6 @@ def load_custom_font() -> str:
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
app = QApplication(sys.argv)

View File

@@ -19,6 +19,7 @@ class Resources(Enum):
class Urls(Enum):
DISCORD = "https://discord.gg/A7eanmSkp2"
INTRANET = "https://la-taniere.fun/connexion/"
API_URL = 'https://prod.la-taniere.fun:30121/'
class Glow(Enum):
COLOR = QColor(255, 140, 0, 255)

View File

@@ -0,0 +1,87 @@
import requests
import webbrowser
import os
from urllib.parse import urlencode
from http.server import HTTPServer, BaseHTTPRequestHandler
from get_server_token import GetServerTokenForDiscord
# Disable stderr output
os.environ['PYTHONWARNINGS'] = 'ignore'
REDIRECT_URI = "http://localhost:5000/callback"
SCOPES = ["identify"]
CLIENT_ID = "1240007913175781508"
AUTENTICATION_SUCCESS_MESSAGE = """
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
</head>
<body>
<h1>Authentication réussie</h1>
<p>Vous pouvez maintenant fermer cette fenêtre et revenir au launcher de La Tanière.</p>
</body>
</html>
""".encode('utf-8')
class OAuthCallbackHandler(BaseHTTPRequestHandler):
code: str | None = None
def do_GET(self):
if "/callback" in self.path:
query = self.path.split("?")[1]
params = dict(p.split("=") for p in query.split("&"))
OAuthCallbackHandler.code = params.get("code")
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(AUTENTICATION_SUCCESS_MESSAGE)
# return discord application id (client id)
def get_discord_client_id() -> str:
return CLIENT_ID
# return discord user id
def get_discord_user_id() -> str:
# récupération des infos serveur lataupe
session_id = GetServerTokenForDiscord.authenticate()
client_secret = GetServerTokenForDiscord.get_token(session_id)
auth_url = "https://discord.com/api/oauth2/authorize"
params = {
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"response_type": "code",
"scope": " ".join(SCOPES),
}
webbrowser.open(f"{auth_url}?{urlencode(params)}")
server = HTTPServer(("localhost", 5000), OAuthCallbackHandler)
# celle ligne cache le stderr output pour ne pas afficher l'url de callback dans la console
# valable en debug mode
os.dup2(os.open(os.devnull, os.O_WRONLY), 2)
server.handle_request()
if not OAuthCallbackHandler.code:
raise RuntimeError("OAuth échoué")
token = requests.post(
"https://discord.com/api/oauth2/token",
data={
"client_id": CLIENT_ID,
"client_secret": client_secret,
"grant_type": "authorization_code",
"code": OAuthCallbackHandler.code,
"redirect_uri": REDIRECT_URI,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
).json()
user = requests.get(
"https://discord.com/api/users/@me",
headers={"Authorization": f"Bearer {token['access_token']}"},
).json()
return user["id"]

View File

@@ -0,0 +1,33 @@
import psutil
from pypresence import Presence
from get_server_token import GetServerTokenForDiscord
from constants import Urls
class DiscordToken:
@staticmethod
def decode_discord_token():
discord_token = GetServerTokenForDiscord.get_token(GetServerTokenForDiscord.authenticate(Urls.API_URL.value))
return discord_token
class CheckDiscord:
@staticmethod
def isdiscordrunning() -> bool:
for process in psutil.process_iter(["name"]):
if process.info["name"].lower() == "discord.exe":
return True
return False
@staticmethod
def isuserconnected(clientid: str) -> bool:
rpc = Presence(clientid)
try:
return True
except Exception:
return False
finally:
try:
rpc.close()
except Exception:
pass

View File

@@ -0,0 +1,76 @@
import base64
import requests
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
API_URL = 'https://prod.la-taniere.fun:30121/'
class GetServerTokenForDiscord:
derived_key: bytes | None = None
@staticmethod
def authenticate(server = API_URL):
if server is None:
server = API_URL
# ==========================
# Génération clé ECDH client
# ==========================
client_private = ec.generate_private_key(ec.SECP256R1())
client_public = client_private.public_key()
client_pub_pem = client_public.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
# ==========================
# AUTH
# ==========================
auth = requests.post(server + "/api_v2/auth", verify=False, json={
"client_pub": base64.b64encode(client_pub_pem).decode()
}).json()
server_pub = serialization.load_pem_public_key(
base64.b64decode(auth["server_pub"])
)
shared_key = client_private.exchange(ec.ECDH(), server_pub)
GetServerTokenForDiscord.derived_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=None,
info=b"fivem-private-server"
).derive(shared_key)
return auth["session_id"]
@staticmethod
def get_token(session_id: bytes, server = API_URL):
# ==========================
# DISCORD TOKEN
# ==========================
if server is None:
server = API_URL
download = requests.post(server + "/api_v2/tkn_auth", verify=False, headers={
"x-session-id": session_id
}).json()
nonce = base64.b64decode(download["nonce"])
encrypted_data = base64.b64decode(download["data"])
aesgcm = AESGCM(GetServerTokenForDiscord.derived_key) # type: ignore[arg-type]
return aesgcm.decrypt(nonce, encrypted_data, None)
# @staticmethod
# def register_discord_user(user_id: str, server = API_URL) -> str:
# if server is None:
# server = API_URL
# registeredId = requests.post(server + "/api_v2/connection/register", verify=False, json={
# "x-session-id": user_id
# }).json()
#
# return registeredId["discord_id"]

19
src/tools/utils.py Normal file
View File

@@ -0,0 +1,19 @@
import sys
from pathlib import Path
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.
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'.
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]

View File

@@ -8,7 +8,7 @@ from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QMainWindow, QSizePolicy
from config.config_manager import ConfigManager
from constants import NO_DISCORD, NO_STAFF, Urls
from tools.constants import NO_DISCORD, NO_STAFF, Urls
from controllers.audio_controller import AudioController
from controllers.glow_animator import GlowAnimator
from controllers.window_dragger import WindowDragger

View File

@@ -1,7 +0,0 @@
import sys
from pathlib import Path
def get_bundle_dir() -> Path:
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
return Path(sys._MEIPASS)
return Path(__file__).resolve().parent.parent