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 @@
a = Analysis(
['src\\main.py'],
pathex=[],
pathex=['src'],
binaries=[],
datas=[
('.\\styles\\styles.qss', 'styles'),
@@ -65,34 +65,105 @@ a = Analysis(
'PySide6.QtWebSockets',
'PySide6.QtXml',
# Stdlib inutile en prod
'unittest',
'email',
'html',
'http',
'xmlrpc',
'pydoc',
'doctest',
'difflib',
'tkinter',
'curses',
'readline',
'xml',
'xmlrpc',
'csv',
'multiprocessing',
'concurrent',
'asyncio',
'sqlite3',
'ssl',
'socket',
'ctypes',
'lib2to3',
'test',
'distutils',
'setuptools',
'pkg_resources',
'pip',
# Tests / dev tools
"test",
"tests",
"unittest",
"doctest",
"pydoc",
"pydoc_data",
# Packaging / build tooling
"distutils",
"setuptools",
"pkg_resources",
"pip",
"ensurepip",
# GUI stdlib inutiles
"tkinter",
"turtle",
"idlelib",
"curses",
# Legacy / obsolete
"lib2to3",
"2to3",
"nis",
"ossaudiodev",
"spwd",
# RPC / servers non utilisés
"xmlrpc",
"wsgiref",
"cgi",
"cgitb",
# Data / DB non utilisés
"sqlite3",
"dbm",
"dbm.dumb",
"csv",
# Concurrency non utilisée dans ton code
"multiprocessing",
"concurrent",
"asyncio",
# REPL / terminal
"readline",
"code",
# "codeop",
"cmd",
# mail / network protocols non utilisés
"mailbox",
"imaplib",
"poplib",
"smtplib",
"nntplib",
"telnetlib",
"ftplib",
"netrc",
# Docs / browsing / parsing non utilisés directement
"pydoc_data",
"mailbox",
"imaplib",
"poplib",
"smtplib",
"nntplib",
"telnetlib",
# XML optionnel : agressif mais plutôt safe ici
"xml.dom",
"xml.etree",
"xml.parsers",
"xml.sax",
# Compression / archive optionnelles si non utilisées
"bz2",
"lzma",
"gzip",
#"zipfile",
"tarfile",
"zipapp",
# audio stdlib non utilisée
"aifc",
"wave",
"sunau",
"chunk",
# divers peu probables
"mailcap",
"xdrlib",
"tabnanny",
"getpass",
# Windows services non utilisés
"win32service",
"win32serviceutil",
],
noarchive=False,
optimize=2,

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