ajout du code pour l'api REST et discord oauth. Refacto
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
10
src/main.py
10
src/main.py
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
87
src/tools/discord_oauth.py
Normal file
87
src/tools/discord_oauth.py
Normal 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"]
|
||||
33
src/tools/discord_tools.py
Normal file
33
src/tools/discord_tools.py
Normal 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
|
||||
76
src/tools/get_server_token.py
Normal file
76
src/tools/get_server_token.py
Normal 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
19
src/tools/utils.py
Normal 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]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user