ajout du code pour l'api REST et discord oauth. Refacto
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
['src\\main.py'],
|
['src\\main.py'],
|
||||||
pathex=[],
|
pathex=['src'],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[
|
datas=[
|
||||||
('.\\styles\\styles.qss', 'styles'),
|
('.\\styles\\styles.qss', 'styles'),
|
||||||
@@ -65,34 +65,105 @@ a = Analysis(
|
|||||||
'PySide6.QtWebSockets',
|
'PySide6.QtWebSockets',
|
||||||
'PySide6.QtXml',
|
'PySide6.QtXml',
|
||||||
|
|
||||||
# Stdlib inutile en prod
|
# Tests / dev tools
|
||||||
'unittest',
|
"test",
|
||||||
'email',
|
"tests",
|
||||||
'html',
|
"unittest",
|
||||||
'http',
|
"doctest",
|
||||||
'xmlrpc',
|
"pydoc",
|
||||||
'pydoc',
|
"pydoc_data",
|
||||||
'doctest',
|
|
||||||
'difflib',
|
# Packaging / build tooling
|
||||||
'tkinter',
|
"distutils",
|
||||||
'curses',
|
"setuptools",
|
||||||
'readline',
|
"pkg_resources",
|
||||||
'xml',
|
"pip",
|
||||||
'xmlrpc',
|
"ensurepip",
|
||||||
'csv',
|
|
||||||
'multiprocessing',
|
# GUI stdlib inutiles
|
||||||
'concurrent',
|
"tkinter",
|
||||||
'asyncio',
|
"turtle",
|
||||||
'sqlite3',
|
"idlelib",
|
||||||
'ssl',
|
"curses",
|
||||||
'socket',
|
|
||||||
'ctypes',
|
# Legacy / obsolete
|
||||||
'lib2to3',
|
"lib2to3",
|
||||||
'test',
|
"2to3",
|
||||||
'distutils',
|
"nis",
|
||||||
'setuptools',
|
"ossaudiodev",
|
||||||
'pkg_resources',
|
"spwd",
|
||||||
'pip',
|
|
||||||
|
# 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,
|
noarchive=False,
|
||||||
optimize=2,
|
optimize=2,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, NotRequired, TypedDict, cast
|
from typing import Any, Callable, NotRequired, TypedDict, cast
|
||||||
|
|
||||||
from utils import get_bundle_dir
|
from tools.utils import get_executable_dir
|
||||||
|
|
||||||
class ConfigData(TypedDict):
|
class ConfigData(TypedDict):
|
||||||
discord_user_id: NotRequired[str]
|
discord_user_id: NotRequired[str]
|
||||||
@@ -16,7 +16,7 @@ class ConfigField(TypedDict):
|
|||||||
validator: Validator
|
validator: Validator
|
||||||
normalizer: Normalizer
|
normalizer: Normalizer
|
||||||
|
|
||||||
CONFIG_PATH = get_bundle_dir() / "config.json"
|
CONFIG_PATH = get_executable_dir() / "config.json"
|
||||||
|
|
||||||
DISCORD_USER_KEY = "discord_user_id"
|
DISCORD_USER_KEY = "discord_user_id"
|
||||||
VOLUME_KEY = "volume"
|
VOLUME_KEY = "volume"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput
|
|||||||
|
|
||||||
from config.config_manager import ConfigManager, VOLUME_KEY
|
from config.config_manager import ConfigManager, VOLUME_KEY
|
||||||
|
|
||||||
from constants import Resources
|
from tools.constants import Resources
|
||||||
|
|
||||||
class AudioController:
|
class AudioController:
|
||||||
# Encapsule toute la logique audio : lecture, volume, mute.
|
# Encapsule toute la logique audio : lecture, volume, mute.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from PySide6.QtCore import QPropertyAnimation, QEasingCurve
|
from PySide6.QtCore import QPropertyAnimation, QEasingCurve
|
||||||
from PySide6.QtWidgets import QGraphicsDropShadowEffect
|
from PySide6.QtWidgets import QGraphicsDropShadowEffect
|
||||||
|
|
||||||
from constants import Glow
|
from tools.constants import Glow
|
||||||
|
|
||||||
|
|
||||||
class GlowAnimator:
|
class GlowAnimator:
|
||||||
|
|||||||
10
src/main.py
10
src/main.py
@@ -1,5 +1,5 @@
|
|||||||
import sys
|
import sys
|
||||||
from utils import get_bundle_dir
|
from tools.utils import get_internal_dir
|
||||||
|
|
||||||
from PySide6.QtCore import QResource
|
from PySide6.QtCore import QResource
|
||||||
from PySide6.QtGui import QFontDatabase, QFont
|
from PySide6.QtGui import QFontDatabase, QFont
|
||||||
@@ -9,21 +9,18 @@ from PySide6.QtWidgets import QApplication
|
|||||||
import resources # noqa: F401 - required to register Qt resources
|
import resources # noqa: F401 - required to register Qt resources
|
||||||
|
|
||||||
from ui.main_window import MainWindow
|
from ui.main_window import MainWindow
|
||||||
from constants import Resources
|
from tools.constants import Resources
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Bundle path resolution
|
# Bundle path resolution
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
bundle_dir = get_internal_dir()
|
||||||
bundle_dir = get_bundle_dir()
|
|
||||||
|
|
||||||
QResource.registerResource(f"{bundle_dir}/resources.py")
|
QResource.registerResource(f"{bundle_dir}/resources.py")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Font helper
|
# Font helper
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def load_custom_font() -> str:
|
def load_custom_font() -> str:
|
||||||
font_id = QFontDatabase.addApplicationFont(Resources.FONT.value)
|
font_id = QFontDatabase.addApplicationFont(Resources.FONT.value)
|
||||||
if font_id == -1:
|
if font_id == -1:
|
||||||
@@ -37,7 +34,6 @@ def load_custom_font() -> str:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Entry point
|
# Entry point
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Resources(Enum):
|
|||||||
class Urls(Enum):
|
class Urls(Enum):
|
||||||
DISCORD = "https://discord.gg/A7eanmSkp2"
|
DISCORD = "https://discord.gg/A7eanmSkp2"
|
||||||
INTRANET = "https://la-taniere.fun/connexion/"
|
INTRANET = "https://la-taniere.fun/connexion/"
|
||||||
|
API_URL = 'https://prod.la-taniere.fun:30121/'
|
||||||
|
|
||||||
class Glow(Enum):
|
class Glow(Enum):
|
||||||
COLOR = QColor(255, 140, 0, 255)
|
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 PySide6.QtWidgets import QMainWindow, QSizePolicy
|
||||||
|
|
||||||
from config.config_manager import ConfigManager
|
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.audio_controller import AudioController
|
||||||
from controllers.glow_animator import GlowAnimator
|
from controllers.glow_animator import GlowAnimator
|
||||||
from controllers.window_dragger import WindowDragger
|
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