From 0e74eebbe4587aee4192aee93f02f2f656a62102 Mon Sep 17 00:00:00 2001 From: WilliamBSITT Date: Wed, 29 Oct 2025 13:51:53 +0100 Subject: [PATCH 01/16] [ADD] add notification service and integrate push notifications for area workflows --- server/core/hook_engine.py | 7 +++- server/models/user.py | 3 +- server/routes/users.py | 9 +++++ server/services/__init__.py | 4 ++- server/services/github_service.py | 32 +++++++++++++++++ ...otification.py => notification_service.py} | 34 +++++++++++++++++-- server/services/spotify_service.py | 12 +++++++ 7 files changed, 95 insertions(+), 6 deletions(-) rename server/services/{notification.py => notification_service.py} (61%) diff --git a/server/core/hook_engine.py b/server/core/hook_engine.py index c66e570..42c4172 100644 --- a/server/core/hook_engine.py +++ b/server/core/hook_engine.py @@ -5,6 +5,8 @@ from models.area import Area from services import get_all_services from core.reaction_executor import reaction_executor +from services.notification_service import send_push_notification + logger = logging.getLogger("AREA-App") @@ -40,8 +42,11 @@ def check_hooks(app=None): logger.info(f"[check_hooks] AREA {area.id} triggered") area.last_run = now db.session.commit() - + reaction_executor(user, rea_srv, area.reaction, params=params, data=data) + titre = "Workflows from area is running" + description = area.name + send_push_notification(user, description, titre) except Exception as e: logger.exception(f"[check_hooks] Error AREA {area.id}: {e}") diff --git a/server/models/user.py b/server/models/user.py index 98b8295..a4b6a9c 100644 --- a/server/models/user.py +++ b/server/models/user.py @@ -8,6 +8,7 @@ class User(db.Model): password_hash = db.Column(db.Text, nullable=False) pictures = db.Column(db.LargeBinary(length=(2**24)-1), nullable=True) expo_push_token = db.Column(db.String(255), nullable=True) + allow_notifications = db.Column(db.Boolean, default=False) def set_password(self, password): self.password_hash = generate_password_hash(password) @@ -16,4 +17,4 @@ def check_password(self, password): return check_password_hash(self.password_hash, password) def to_dict(self): - return {"id": self.id, "email": self.email, "pictures": binascii.b2a_base64(self.pictures).decode('utf-8') if self.pictures else None} + return {"id": self.id, "email": self.email, "pictures": binascii.b2a_base64(self.pictures).decode('utf-8') if self.pictures else None, "allow_notifications": self.allow_notifications} diff --git a/server/routes/users.py b/server/routes/users.py index bf5737f..693e80d 100644 --- a/server/routes/users.py +++ b/server/routes/users.py @@ -123,14 +123,20 @@ def create_user(): data = request.get_json() email = data.get("email") password = data.get("password") + expo_push_token = data.get("expo_push_token") + notif = False if not email or not password: return jsonify({"error": "email or password required"}), 400 if User.query.filter_by(email=email).first(): return jsonify({"error": "user already exist"}), 400 + if expo_push_token: + notif = True user = User(email=email) user.set_password(password) + user.expo_push_token = expo_push_token + user.allow_notifications = notif db.session.add(user) db.session.commit() @@ -219,6 +225,7 @@ def update_user(user_id): email = data.get("email") password = data.get("password") pictures = data.get("pictures") + allow_notifications = data.get("allow_notifications") if email: user.email = email @@ -229,6 +236,8 @@ def update_user(user_id): user.pictures = base64.b64decode(pictures) else: user.pictures = pictures + if allow_notifications is not None: + user.allow_notifications = allow_notifications db.session.commit() return jsonify(user.to_dict()), 200 diff --git a/server/services/__init__.py b/server/services/__init__.py index 1471117..4d9cdad 100644 --- a/server/services/__init__.py +++ b/server/services/__init__.py @@ -5,6 +5,7 @@ from .gmail_service import GmailService from .spotify_service import SpotifyService from .github_service import GithubService +from .notification_service import NotificationService registered_services = [ OpenWeatherService(), @@ -13,7 +14,8 @@ GmailService(), SpotifyService(), NasaService(), - GithubService() + GithubService(), + NotificationService() ] def get_all_services(): diff --git a/server/services/github_service.py b/server/services/github_service.py index e39c814..ecfea11 100644 --- a/server/services/github_service.py +++ b/server/services/github_service.py @@ -31,6 +31,38 @@ def get_reactions(self): {"name": "create_issue", "description": "Crée une issue dans un dépôt GitHub"}, {"name": "star_repo", "description": "Ajoute une étoile à un dépôt GitHub"} ] + + def get_actions_params(self, action_name): + if action_name == "new_commit": + return [ + {"name": "repo", "type": "string", "description": "Dépôt à surveiller au format 'owner/repo'"}, + {"name": "token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"}, + {"name": "last_commit", "type": "string", "description": "SHA du dernier commit vu (utilisé pour le suivi)"} + ] + elif action_name == "new_issue": + return [ + {"name": "repo", "type": "string", "description": "Dépôt à surveiller au format 'owner/repo'"}, + {"name": "token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"}, + {"name": "last_issue", "type": "integer", "description": "ID de la dernière issue vue (utilisé pour le suivi)"} + ] + else: + return [] + + def get_reactions_params(self, reaction_name): + if reaction_name == "create_issue": + return [ + {"name": "repo", "type": "string", "description": "Dépôt cible au format 'owner/repo'"}, + {"name": "title", "type": "string", "description": "Titre de l’issue"}, + {"name": "body", "type": "string", "description": "Corps de l’issue (peut inclure des placeholders pour les données d’action)"}, + {"name": "token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"} + ] + elif reaction_name == "star_repo": + return [ + {"name": "repo", "type": "string", "description": "Dépôt à étoiler au format 'owner/repo'"}, + {"name": "token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"} + ] + else: + return [] def check_action(self, user, action, params=None): diff --git a/server/services/notification.py b/server/services/notification_service.py similarity index 61% rename from server/services/notification.py rename to server/services/notification_service.py index 5a533e0..99b0d0f 100644 --- a/server/services/notification.py +++ b/server/services/notification_service.py @@ -9,8 +9,29 @@ from requests.exceptions import ConnectionError, HTTPError from models.user import User +def send_push_notification(user, message, title): + expo_push_token = User.query.filter_by(id=user.id).first().expo_push_token + try: + response = PushClient().publish( + PushMessage( + to=expo_push_token, + title=title, + body=message, + sound="default", + ) + ) + print("Push notification sent:", response) + except PushServerError as exc: + print("Push server error:", exc) + except (ConnectionError, HTTPError) as exc: + print("Connection error:", exc) + except DeviceNotRegisteredError: + print("Device not registered, removing token") + except InvalidCredentialsError: + print("Invalid Expo credentials") + class NotificationService(BaseService): - name = "notification" + name = "notifications" def __init__(self): pass @@ -23,8 +44,16 @@ def get_reactions(self): return [ {"name": "send_notification", "description": "Envoyer une notification push à l'utilisateur."} ] + + def get_reactions_params(self, reaction_name): + if reaction_name == "send_notification": + return [ + {"name": "message", "type": "string", "description": "Le message de la notification."}, + {"name": "title", "type": "string", "description": "Le titre de la notification."} + ] + return [] - def check_reaction(self, user, reaction, params=None): + def execute_reaction(self, user, reaction, params=None, data=None): message = params.get("message", "Hello from AREA!") title = params.get("title", "AREA Notification") expo_push_token = User.query.filter_by(id=user.id).first().expo_push_token @@ -33,7 +62,6 @@ def check_reaction(self, user, reaction, params=None): if reaction not in available_reactions: print(f"Reaction non disponible: {reaction}") return None - if reaction == "send_notification": # print(f"Envoi de la notification à l'utilisateur {user.id}") try: diff --git a/server/services/spotify_service.py b/server/services/spotify_service.py index b499c0c..8ec2ee6 100644 --- a/server/services/spotify_service.py +++ b/server/services/spotify_service.py @@ -42,6 +42,18 @@ def get_reactions(self): {"name": "previous", "description": "Retourne à la musique précédente"} ] + def get_reactions_params(self, reaction_name): + if reaction_name == "play": + return [ + {"name": "track_uri", "type": "string", "description": "URI de la piste à jouer"}, + {"name": "tokens", "type": "object", "description": "Tokens d'authentification de l'utilisateur (access_token et refresh_token)"}, + {"name": "refresh_token", "type": "string", "description": "Token de rafraîchissement de l'utilisateur"} + ] + return [ + {"name": "tokens", "type": "object", "description": "Tokens d'authentification de l'utilisateur (access_token et refresh_token)"}, + {"name": "refresh_token", "type": "string", "description": "Token de rafraîchissement de l'utilisateur"} + ] + def check_action(self, user, action, params=None): tokens = params.get("tokens", {}) access_token = tokens.get("access_token") From 015bd3f06bc65a71c7189ccce5ffdd2c5411bc38 Mon Sep 17 00:00:00 2001 From: WilliamBSITT Date: Wed, 29 Oct 2025 14:10:48 +0100 Subject: [PATCH 02/16] [FIX] to not spam the serv with some error --- server/services/notification_service.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/services/notification_service.py b/server/services/notification_service.py index 99b0d0f..2f05887 100644 --- a/server/services/notification_service.py +++ b/server/services/notification_service.py @@ -20,15 +20,14 @@ def send_push_notification(user, message, title): sound="default", ) ) - print("Push notification sent:", response) except PushServerError as exc: - print("Push server error:", exc) + return except (ConnectionError, HTTPError) as exc: - print("Connection error:", exc) + return except DeviceNotRegisteredError: - print("Device not registered, removing token") + return except InvalidCredentialsError: - print("Invalid Expo credentials") + return class NotificationService(BaseService): name = "notifications" From 8c9b41df9016361febd84215f2a7aec7d0334c96 Mon Sep 17 00:00:00 2001 From: WilliamBSITT Date: Wed, 29 Oct 2025 16:02:33 +0100 Subject: [PATCH 03/16] [FIX] update Spotify auth URL and redirect to services with tokens --- server/models/service.py | 2 +- server/routes/spotify.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/models/service.py b/server/models/service.py index 33dc16a..d8d5404 100644 --- a/server/models/service.py +++ b/server/models/service.py @@ -31,7 +31,7 @@ def seed_services(): "name": "Spotify", "description": "Spotify is a digital music platform where you can stream millions of songs and podcasts. It lets you create and share playlists for any mood or moment. Discover new artists and listen to your favorite albums anytime, anywhere. You can connect with friends and see what they’re listening to. Perfect for anyone who loves music and wants instant access to it.", "image": image_to_binary("spotify"), - "auth_url": "https://accounts.spotify.com/authorize?client_id=a63f13819159493eb695b3c16785aa55&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Fspotify%2Fcallback&scope=user-read-currently-playing+user-read-playback-state&show_dialog=true" + "auth_url": "http://127.0.0.1:8080/spotify/callback" }, { "name": "OpenWeather", diff --git a/server/routes/spotify.py b/server/routes/spotify.py index f4e915d..ef001fb 100644 --- a/server/routes/spotify.py +++ b/server/routes/spotify.py @@ -59,6 +59,7 @@ def spotify_callback(): return jsonify({"error": res.text}), 400 tokens = res.json() + return redirect(f"http://localhost:8081/services?tokens={tokens}") return jsonify({ "message": "Spotify connected successfully!", "tokens": tokens From 50f4186cfab5f25c95ee0531990f2d4724e47976 Mon Sep 17 00:00:00 2001 From: WilliamBSITT Date: Thu, 30 Oct 2025 11:02:19 +0100 Subject: [PATCH 04/16] [FIX] clean up commented code and improve callback in GitHub service --- server/routes/github_service.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/server/routes/github_service.py b/server/routes/github_service.py index 0044077..c85bbf6 100644 --- a/server/routes/github_service.py +++ b/server/routes/github_service.py @@ -15,8 +15,8 @@ def github_login(): """ Redirige l'utilisateur vers GitHub pour autorisation OAuth """ - with open("oui.txt", "w") as f: - print(REDIRECT_URI, file=f) + # with open("oui.txt", "w") as f: + # print(REDIRECT_URI, file=f) frontend = request.args.get("frontend", "web") scope = "repo user" state = f"frontend:{frontend}" @@ -30,7 +30,6 @@ def github_login(): url = f"{AUTH_URL}?{urllib.parse.urlencode(params)}" return redirect(url) -# --- 🟡 Étape 2 : callback GitHub après autorisation @bp.route("/callback", methods=["GET"]) def github_callback(): """ @@ -51,7 +50,6 @@ def github_callback(): mobile_redirect_uri = "exp://10.18.208.5:8081" return redirect(f"{mobile_redirect_uri}?code={code}") - # --- 🌐 Sinon : flow classique web data = { "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, @@ -66,12 +64,12 @@ def github_callback(): if "access_token" not in tokens: return jsonify({"error": "GitHub OAuth failed", "details": tokens}), 400 + return redirect(f"http://localhost:8081/services?tokens={tokens}") return jsonify({ "message": "GitHub connected successfully!", "tokens": tokens }) -# --- 🔵 Étape 3 : échange du code (flow mobile) @bp.route("/exchange_token", methods=["POST"]) def github_exchange_token(): """ From 33c67e1c043c04b65d035f1b083e80270c046ecd Mon Sep 17 00:00:00 2001 From: jonazakana Date: Thu, 30 Oct 2025 15:50:25 +0100 Subject: [PATCH 05/16] finish some modifs --- server/core/hook_engine.py | 18 ++--- server/models/area.py | 10 ++- server/oui.txt | 1 - server/requirements.txt | 16 ----- server/routes/__init__.py | 4 +- server/routes/github_service.py | 30 +++++--- server/routes/ip_manager.py | 67 ++++++++++++++++++ server/routes/spotify.py | 27 ++++++-- server/services/spotify_service.py | 108 ++++++++++++++--------------- 9 files changed, 183 insertions(+), 98 deletions(-) delete mode 100644 server/oui.txt delete mode 100644 server/requirements.txt create mode 100644 server/routes/ip_manager.py diff --git a/server/core/hook_engine.py b/server/core/hook_engine.py index 42c4172..9699ad7 100644 --- a/server/core/hook_engine.py +++ b/server/core/hook_engine.py @@ -5,10 +5,8 @@ from models.area import Area from services import get_all_services from core.reaction_executor import reaction_executor -from services.notification_service import send_push_notification - -logger = logging.getLogger("AREA-App") +logger = logging.getLogger("AREA-API") def check_hooks(app=None): ctx = app.app_context() if app else current_app.app_context() @@ -34,22 +32,18 @@ def check_hooks(app=None): last_run = area.last_run.replace(tzinfo=timezone.utc) if (now - last_run) < timedelta(seconds=area.frequency): continue - + params = area.params or {} data = act_srv.check_action(user, area.action, params=params) if data: logger.info(f"[check_hooks] AREA {area.id} triggered") + logger.info(params) + area.set_params(params) area.last_run = now db.session.commit() - + reaction_executor(user, rea_srv, area.reaction, params=params, data=data) - titre = "Workflows from area is running" - description = area.name - send_push_notification(user, description, titre) except Exception as e: - logger.exception(f"[check_hooks] Error AREA {area.id}: {e}") - - - + logger.exception(f"[check_hooks] Error AREA {area.id}: {e}") \ No newline at end of file diff --git a/server/models/area.py b/server/models/area.py index 233672f..24399bd 100644 --- a/server/models/area.py +++ b/server/models/area.py @@ -1,5 +1,6 @@ from extensions import db from sqlalchemy.dialects.postgresql import JSON +from sqlalchemy.ext.mutable import MutableDict from utils.crypto_manager import crypto class Area(db.Model): @@ -14,7 +15,7 @@ class Area(db.Model): reaction = db.Column(db.String(80), nullable=False) frequency = db.Column(db.Integer, default=3600) last_run = db.Column(db.DateTime, default=None) - params = db.Column(db.JSON, nullable=True) + params = db.Column(MutableDict.as_mutable(db.JSON), nullable=True) enabled = db.Column(db.Boolean, default=True) public = db.Column(db.Boolean, default=False) @@ -25,12 +26,17 @@ def set_params(self, params: dict): safe = {} for k, v in (params or {}).items(): - if "password" in k.lower(): + if isinstance(v, dict): + safe[k] = {sub_k: crypto.encrypt(sub_v) if "password" in sub_k.lower() else sub_v + for sub_k, sub_v in v.items()} + elif "password" in k.lower(): safe[k] = crypto.encrypt(v) else: safe[k] = v + self.params = safe + def get_params(self) -> dict: safe = {} diff --git a/server/oui.txt b/server/oui.txt deleted file mode 100644 index 6d669fd..0000000 --- a/server/oui.txt +++ /dev/null @@ -1 +0,0 @@ -https://avowedly-uncomputed-velvet.ngrok-free.dev/git/callback diff --git a/server/requirements.txt b/server/requirements.txt deleted file mode 100644 index 01eda9f..0000000 --- a/server/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -Flask==3.0.3 -Flask-SQLAlchemy==3.1.1 -flask_migrate==4.0.7 -flask_jwt_extended==4.6.0 -pymysql==1.1.1 -python-dotenv==1.0.0 -requests==2.31.0 -flask-cors==3.0.10 -APScheduler==3.10.4 -flasgger -discord -cryptography -pytest -pytest-flask -pytest-mock -exponent-server-sdk diff --git a/server/routes/__init__.py b/server/routes/__init__.py index 11638cc..deabedb 100644 --- a/server/routes/__init__.py +++ b/server/routes/__init__.py @@ -6,6 +6,7 @@ from .service import bp as service_bp from .spotify import bp as spotify_bp from .github_service import bp as github_service_bp +from .ip_manager import bp as ip_manager_bp def register_routes(app): app.register_blueprint(users_bp) @@ -15,4 +16,5 @@ def register_routes(app): app.register_blueprint(area_bp) app.register_blueprint(json_bp) app.register_blueprint(spotify_bp) - app.register_blueprint(github_service_bp) \ No newline at end of file + app.register_blueprint(github_service_bp) + app.register_blueprint(ip_manager_bp) \ No newline at end of file diff --git a/server/routes/github_service.py b/server/routes/github_service.py index 0044077..8068851 100644 --- a/server/routes/github_service.py +++ b/server/routes/github_service.py @@ -1,5 +1,7 @@ from flask import Blueprint, jsonify, redirect, request import requests, urllib.parse, os +from .ip_manager import decode_ip +import json bp = Blueprint("github_service", __name__, url_prefix="/git") @@ -15,11 +17,20 @@ def github_login(): """ Redirige l'utilisateur vers GitHub pour autorisation OAuth """ - with open("oui.txt", "w") as f: - print(REDIRECT_URI, file=f) + frontend = request.args.get("frontend", "web") + encoded_ip = request.args.get("ip", None) + + if not encoded_ip: + return jsonify({"error": "Missing 'ip' parameter"}), 400 + + ip = decode_ip(encoded_ip) + scope = "repo user" - state = f"frontend:{frontend}" + state = json.dumps({ + "frontend": frontend, + "ip": ip + }) params = { "client_id": CLIENT_ID, @@ -40,18 +51,22 @@ def github_callback(): return jsonify({"error": request.args["error"]}), 400 code = request.args.get("code") - state = request.args.get("state", "frontend:web") + state_str = request.args.get("state", '{"frontend":"web"}') + + clean = state_str.replace('+', '') + data = json.loads(clean) + + frontend = data.get("frontend") + ip = data.get("ip") if not code: return jsonify({"error": "Missing authorization code"}), 400 - frontend = state.split(":")[1] if frontend == "mobile": - mobile_redirect_uri = "exp://10.18.208.5:8081" + mobile_redirect_uri = f"exp://{ip}:8083" return redirect(f"{mobile_redirect_uri}?code={code}") - # --- 🌐 Sinon : flow classique web data = { "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, @@ -71,7 +86,6 @@ def github_callback(): "tokens": tokens }) -# --- 🔵 Étape 3 : échange du code (flow mobile) @bp.route("/exchange_token", methods=["POST"]) def github_exchange_token(): """ diff --git a/server/routes/ip_manager.py b/server/routes/ip_manager.py new file mode 100644 index 0000000..857dd88 --- /dev/null +++ b/server/routes/ip_manager.py @@ -0,0 +1,67 @@ +from flask import request, jsonify, Blueprint +from itsdangerous import URLSafeSerializer, BadSignature +import os +import re +import ipaddress + +bp = Blueprint("ip_manager", __name__, url_prefix="/ip") + +SECRET_KEY = os.getenv("IP_SECRET_KEY", "default_secret_key") +SALT = os.getenv("IP_SALT", "ip_salt") + +serializer = URLSafeSerializer(SECRET_KEY, salt=SALT) + +def extract_ip(exp_url: str) -> str | None: + """ + Extrait l'IP d'une URL Expo (exp://IP:PORT) + """ + pattern = r"exp://([\d\.]+):\d+" + match = re.match(pattern, exp_url) + if match: + return match.group(1) + return None + +def validate_ip(ip_str): + try: + ipaddress.ip_address(ip_str) + return True + except ValueError: + return False + + + +def decode_ip(ip_encoded): + """ + Attendu JSON: { "encoded": "" } + Réponse JSON: { "ip": "1.2.3.4" } ou erreur 400/401 + """ + data = ip_encoded + if not ip_encoded: + return None + + try: + ip = serializer.loads(data) + except BadSignature: + return None + except Exception: + return None + + if not validate_ip(ip): + return None + + return ip + +@bp.route("/encode", methods=["POST"]) +def encode_ip(): + data = request.get_json(force=True, silent=True) + if not data or "ip" not in data: + return jsonify({"error": "Missing 'ip' in request body"}), 400 + + url = str(data["ip"]).strip() + ip = extract_ip(url) + with open("oui.txt", "w") as f: + print(f"Encoding IP: {ip}", file=f) + if not validate_ip(ip): + return jsonify({"error": "Invalid IP address format"}), 400 + token = serializer.dumps(ip) + return jsonify({"encoded": token}), 200 \ No newline at end of file diff --git a/server/routes/spotify.py b/server/routes/spotify.py index f4e915d..c20d690 100644 --- a/server/routes/spotify.py +++ b/server/routes/spotify.py @@ -3,6 +3,10 @@ import urllib.parse import os from datetime import datetime +from .ip_manager import decode_ip +import json +import re + bp = Blueprint("spotify_auth", __name__) @@ -15,8 +19,19 @@ @bp.route("/spotify/login", methods=["GET"]) def spotify_login(): frontend = request.args.get("frontend", "web") + encoded_ip = request.args.get("ip", None) + + if not encoded_ip: + return jsonify({"error": "Missing 'ip' parameter"}), 400 + + ip = decode_ip(encoded_ip) + if not ip: + return jsonify({"error": "Invalid or tampered 'ip' parameter"}), 400 scope = "user-read-currently-playing user-read-playback-state" - state = f"frontend:{frontend}" + state = json.dumps({ + "frontend": frontend, + "ip": ip + }) params = { "client_id": CLIENT_ID, @@ -34,15 +49,19 @@ def spotify_login(): @bp.route("/spotify/callback") def spotify_callback(): code = request.args.get("code") - state = request.args.get("state", "frontend:web") + state_str = request.args.get("state", '{"frontend":"web"}') - frontend = state.split(":")[1] + clean = state_str.replace('+', '') + data = json.loads(clean) + + frontend = data.get("frontend") + ip = data.get("ip") if not code: return jsonify({"error": "Missing authorization code"}), 400 if frontend == "mobile": - mobile_redirect_uri = f"exp://10.18.208.5:8081?code={code}" + mobile_redirect_uri = f"exp://{ip}:8083?code={code}" print(f"Redirecting to mobile app: {mobile_redirect_uri}") return redirect(mobile_redirect_uri) diff --git a/server/services/spotify_service.py b/server/services/spotify_service.py index 8ec2ee6..0d0efc2 100644 --- a/server/services/spotify_service.py +++ b/server/services/spotify_service.py @@ -2,7 +2,7 @@ import requests import re import os -from datetime import datetime +from datetime import datetime, timezone class SpotifyService(BaseService): @@ -42,67 +42,66 @@ def get_reactions(self): {"name": "previous", "description": "Retourne à la musique précédente"} ] - def get_reactions_params(self, reaction_name): - if reaction_name == "play": - return [ - {"name": "track_uri", "type": "string", "description": "URI de la piste à jouer"}, - {"name": "tokens", "type": "object", "description": "Tokens d'authentification de l'utilisateur (access_token et refresh_token)"}, - {"name": "refresh_token", "type": "string", "description": "Token de rafraîchissement de l'utilisateur"} - ] - return [ - {"name": "tokens", "type": "object", "description": "Tokens d'authentification de l'utilisateur (access_token et refresh_token)"}, - {"name": "refresh_token", "type": "string", "description": "Token de rafraîchissement de l'utilisateur"} - ] - def check_action(self, user, action, params=None): - tokens = params.get("tokens", {}) - access_token = tokens.get("access_token") - refresh_token = tokens.get("refresh_token") - - if not access_token or not refresh_token: - return None - - headers = {"Authorization": f"Bearer {access_token}"} - url = f"{self.api_base}/me/player/currently-playing" - - res = requests.get(url, headers=headers) - - if res.status_code == 401: - refreshed = self.refresh_token(refresh_token) - if not refreshed: + params = params or {} + access_token = params.get("access_token") + refresh_token = params.get("refresh_token") + + if not access_token or not refresh_token: + print("[Spotify] Tokens manquants") return None - - access_token = refreshed.get("access_token") - headers["Authorization"] = f"Bearer {access_token}" + + headers = {"Authorization": f"Bearer {access_token}"} + url = f"{self.api_base}/me/player/currently-playing" + res = requests.get(url, headers=headers) - - if res.status_code != 200: - print(f"[Spotify] Erreur API ({res.status_code}): {res.text}") - return None - data = res.json() - item = data.get("item") - if not item: - return None + if res.status_code == 401: + refreshed = self.refresh_token(refresh_token) + if not refreshed: + print("[Spotify] Refresh token échoué") + return None - track_name = item.get("name") - artist_name = item["artists"][0]["name"] if item.get("artists") else "Inconnu" - album_name = item["album"]["name"] - url = item["external_urls"]["spotify"] + access_token = refreshed.get("access_token") + headers["Authorization"] = f"Bearer {access_token}" + res = requests.get(url, headers=headers) - if action == "new_title": - last_title = params.get("last_title") - if last_title == track_name: + if res.status_code != 200: + print(f"[Spotify] Erreur API ({res.status_code}): {res.text}") return None - params["last_title"] = track_name - return { - "track": track_name, - "artist": artist_name, - "album": album_name, - "url": url - } + data = res.json() + item = data.get("item") + if not item: + return None + track_name = item.get("name") + artist_name = item["artists"][0]["name"] if item.get("artists") else "Inconnu" + album_name = item["album"]["name"] + track_url = item["external_urls"]["spotify"] + + if action == "currently_playing": + return { + "track": track_name, + "artist": artist_name, + "album": album_name, + "url": track_url + } + + elif action == "new_title": + last_title = params.get("last_title") + if last_title == track_name: + return None + params["last_title"] = track_name + params["last_updated"] = datetime.now(timezone.utc).isoformat() + return { + "track": track_name, + "artist": artist_name, + "album": album_name, + "url": track_url + } + + return None def execute_reaction(self, user, reaction, params=None, data=None): tokens = params.get("tokens", {}) @@ -154,5 +153,6 @@ def execute_reaction(self, user, reaction, params=None, data=None): return True - + def get_actions_params(self, action_name): + pass From b9a59b0a9485afde93e0c35b5ec2b55417b2292e Mon Sep 17 00:00:00 2001 From: jonazakana Date: Thu, 30 Oct 2025 16:11:22 +0100 Subject: [PATCH 06/16] add requirements.txt --- server/requirements.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 server/requirements.txt diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..fcda864 --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,16 @@ +Flask==3.0.3 +Flask-SQLAlchemy==3.1.1 +flask_migrate==4.0.7 +flask_jwt_extended==4.6.0 +pymysql==1.1.1 +python-dotenv==1.0.0 +requests==2.31.0 +flask-cors==3.0.10 +APScheduler==3.10.4 +flasgger +discord +cryptography +pytest +pytest-flask +pytest-mock +exponent-server-sdk \ No newline at end of file From a37a4919a6755f7f64f9125f0324fa09a6f0b8d3 Mon Sep 17 00:00:00 2001 From: jonazakana Date: Thu, 30 Oct 2025 21:16:30 +0100 Subject: [PATCH 07/16] add route for look remote db --- server/routes/service.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/routes/service.py b/server/routes/service.py index ddba861..bbba357 100644 --- a/server/routes/service.py +++ b/server/routes/service.py @@ -219,4 +219,9 @@ def service_fetch_by_name(service_name): ), 200 except Exception as e: - return jsonify({"error": str(e)}), 500 \ No newline at end of file + return jsonify({"error": str(e)}), 500 + + +@bp.route("/db") +def db(): + return redirect("http://localhost:8082", code=302) \ No newline at end of file From 7df6b0abaa84b25337f01e1bb5535b06ae4c8c6b Mon Sep 17 00:00:00 2001 From: jonazakana Date: Fri, 31 Oct 2025 10:12:46 +0100 Subject: [PATCH 08/16] add route for look remote db --- server/routes/github_service.py | 2 ++ server/routes/spotify.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/server/routes/github_service.py b/server/routes/github_service.py index 98dc491..e031cf7 100644 --- a/server/routes/github_service.py +++ b/server/routes/github_service.py @@ -52,6 +52,8 @@ def github_callback(): code = request.args.get("code") state_str = request.args.get("state", '{"frontend":"web"}') + with open("bite.txt", "w") as f: + print(f"GitHub callback state: {state_str}", file=f) clean = state_str.replace('+', '') data = json.loads(clean) diff --git a/server/routes/spotify.py b/server/routes/spotify.py index 2292e38..3317187 100644 --- a/server/routes/spotify.py +++ b/server/routes/spotify.py @@ -20,13 +20,11 @@ def spotify_login(): frontend = request.args.get("frontend", "web") encoded_ip = request.args.get("ip", None) - - if not encoded_ip: - return jsonify({"error": "Missing 'ip' parameter"}), 400 - ip = decode_ip(encoded_ip) - if not ip: - return jsonify({"error": "Invalid or tampered 'ip' parameter"}), 400 + if encoded_ip: + ip = decode_ip(encoded_ip) + if not ip: + return jsonify({"error": "Invalid or tampered 'ip' parameter"}), 400 scope = "user-read-currently-playing user-read-playback-state" state = json.dumps({ "frontend": frontend, @@ -51,6 +49,8 @@ def spotify_callback(): code = request.args.get("code") state_str = request.args.get("state", '{"frontend":"web"}') + with open("fouffe.txt", "w") as f: + print(f"Spotify callback state: {state_str}", file=f) clean = state_str.replace('+', '') data = json.loads(clean) From dc8b5fa7ed22d99c8f0a6ff0f6fa209acd255751 Mon Sep 17 00:00:00 2001 From: jonazakana Date: Fri, 31 Oct 2025 10:21:06 +0100 Subject: [PATCH 09/16] modif redirection port --- server/routes/github_service.py | 4 +--- server/routes/spotify.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/server/routes/github_service.py b/server/routes/github_service.py index e031cf7..6ea5648 100644 --- a/server/routes/github_service.py +++ b/server/routes/github_service.py @@ -52,8 +52,6 @@ def github_callback(): code = request.args.get("code") state_str = request.args.get("state", '{"frontend":"web"}') - with open("bite.txt", "w") as f: - print(f"GitHub callback state: {state_str}", file=f) clean = state_str.replace('+', '') data = json.loads(clean) @@ -65,7 +63,7 @@ def github_callback(): if frontend == "mobile": - mobile_redirect_uri = f"exp://{ip}:8083" + mobile_redirect_uri = f"exp://{ip}:8081" return redirect(f"{mobile_redirect_uri}?code={code}") data = { diff --git a/server/routes/spotify.py b/server/routes/spotify.py index 3317187..1f78c80 100644 --- a/server/routes/spotify.py +++ b/server/routes/spotify.py @@ -49,8 +49,6 @@ def spotify_callback(): code = request.args.get("code") state_str = request.args.get("state", '{"frontend":"web"}') - with open("fouffe.txt", "w") as f: - print(f"Spotify callback state: {state_str}", file=f) clean = state_str.replace('+', '') data = json.loads(clean) @@ -61,7 +59,7 @@ def spotify_callback(): return jsonify({"error": "Missing authorization code"}), 400 if frontend == "mobile": - mobile_redirect_uri = f"exp://{ip}:8083?code={code}" + mobile_redirect_uri = f"exp://{ip}:8081?code={code}" print(f"Redirecting to mobile app: {mobile_redirect_uri}") return redirect(mobile_redirect_uri) From 8a266563dd24389ede121d5621450db0a3c35a46 Mon Sep 17 00:00:00 2001 From: jonazakana Date: Fri, 31 Oct 2025 10:30:41 +0100 Subject: [PATCH 10/16] modif redirection port --- server/routes/spotify.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/routes/spotify.py b/server/routes/spotify.py index 1f78c80..8460ee3 100644 --- a/server/routes/spotify.py +++ b/server/routes/spotify.py @@ -20,6 +20,7 @@ def spotify_login(): frontend = request.args.get("frontend", "web") encoded_ip = request.args.get("ip", None) + port = request.args.get("port", "8081") if encoded_ip: ip = decode_ip(encoded_ip) @@ -28,7 +29,8 @@ def spotify_login(): scope = "user-read-currently-playing user-read-playback-state" state = json.dumps({ "frontend": frontend, - "ip": ip + "ip": ip, + "port": port }) params = { @@ -54,12 +56,13 @@ def spotify_callback(): frontend = data.get("frontend") ip = data.get("ip") + port = data.get("port") if not code: return jsonify({"error": "Missing authorization code"}), 400 if frontend == "mobile": - mobile_redirect_uri = f"exp://{ip}:8081?code={code}" + mobile_redirect_uri = f"exp://{ip}:{port}?code={code}" print(f"Redirecting to mobile app: {mobile_redirect_uri}") return redirect(mobile_redirect_uri) From 2a8eecfd4ec49a4fd560b7f8378477de6d2437bb Mon Sep 17 00:00:00 2001 From: jonazakana Date: Fri, 31 Oct 2025 10:37:26 +0100 Subject: [PATCH 11/16] modif redirection port --- server/routes/github_service.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/server/routes/github_service.py b/server/routes/github_service.py index 6ea5648..107638c 100644 --- a/server/routes/github_service.py +++ b/server/routes/github_service.py @@ -20,16 +20,19 @@ def github_login(): frontend = request.args.get("frontend", "web") encoded_ip = request.args.get("ip", None) + port = request.args.get("port", None) + + if encoded_ip: + ip = decode_ip(encoded_ip) + if not ip: + return jsonify({"error": "Invalid or tampered 'ip' parameter"}), 400 - if not encoded_ip: - return jsonify({"error": "Missing 'ip' parameter"}), 400 - - ip = decode_ip(encoded_ip) scope = "repo user" state = json.dumps({ "frontend": frontend, - "ip": ip + "ip": ip, + "port": port }) params = { @@ -57,13 +60,14 @@ def github_callback(): frontend = data.get("frontend") ip = data.get("ip") + port = data.get("port") if not code: return jsonify({"error": "Missing authorization code"}), 400 if frontend == "mobile": - mobile_redirect_uri = f"exp://{ip}:8081" + mobile_redirect_uri = f"exp://{ip}:{port}" return redirect(f"{mobile_redirect_uri}?code={code}") data = { From 1ff45c99a66f4383c2daca790b920f648d89b5a6 Mon Sep 17 00:00:00 2001 From: jonazakana Date: Fri, 31 Oct 2025 11:41:38 +0100 Subject: [PATCH 12/16] fix --- server/routes/github_service.py | 18 ++++++++++-------- server/routes/spotify.py | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/server/routes/github_service.py b/server/routes/github_service.py index 107638c..c63db1d 100644 --- a/server/routes/github_service.py +++ b/server/routes/github_service.py @@ -19,20 +19,22 @@ def github_login(): """ frontend = request.args.get("frontend", "web") - encoded_ip = request.args.get("ip", None) - port = request.args.get("port", None) - if encoded_ip: - ip = decode_ip(encoded_ip) - if not ip: - return jsonify({"error": "Invalid or tampered 'ip' parameter"}), 400 + if frontend == "mobile": + encoded_ip = request.args.get("ip", None) + port = request.args.get("port", None) + + if encoded_ip: + ip = decode_ip(encoded_ip) + if not ip: + return jsonify({"error": "Invalid or tampered 'ip' parameter"}), 400 scope = "repo user" state = json.dumps({ "frontend": frontend, - "ip": ip, - "port": port + "ip": ip if frontend == "mobile" else None, + "port": port if frontend == "mobile" else None }) params = { diff --git a/server/routes/spotify.py b/server/routes/spotify.py index 8460ee3..d52b5b8 100644 --- a/server/routes/spotify.py +++ b/server/routes/spotify.py @@ -19,18 +19,21 @@ @bp.route("/spotify/login", methods=["GET"]) def spotify_login(): frontend = request.args.get("frontend", "web") - encoded_ip = request.args.get("ip", None) - port = request.args.get("port", "8081") + + if frontend == "mobile": + encoded_ip = request.args.get("ip", None) + port = request.args.get("port", "8081") - if encoded_ip: - ip = decode_ip(encoded_ip) - if not ip: - return jsonify({"error": "Invalid or tampered 'ip' parameter"}), 400 + if encoded_ip: + ip = decode_ip(encoded_ip) + if not ip: + return jsonify({"error": "Invalid or tampered 'ip' parameter"}), 400 + scope = "user-read-currently-playing user-read-playback-state" state = json.dumps({ "frontend": frontend, - "ip": ip, - "port": port + "ip": ip if frontend == "mobile" else None, + "port": port if frontend == "mobile" else None }) params = { From 50b4602584376be51571e438cfa881b3d472a0cf Mon Sep 17 00:00:00 2001 From: WilliamBSITT Date: Fri, 31 Oct 2025 11:47:46 +0100 Subject: [PATCH 13/16] [ADD] missing params for spotify --- server/models/service.py | 4 ++-- server/services/spotify_service.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/server/models/service.py b/server/models/service.py index d8d5404..2808eac 100644 --- a/server/models/service.py +++ b/server/models/service.py @@ -31,7 +31,7 @@ def seed_services(): "name": "Spotify", "description": "Spotify is a digital music platform where you can stream millions of songs and podcasts. It lets you create and share playlists for any mood or moment. Discover new artists and listen to your favorite albums anytime, anywhere. You can connect with friends and see what they’re listening to. Perfect for anyone who loves music and wants instant access to it.", "image": image_to_binary("spotify"), - "auth_url": "http://127.0.0.1:8080/spotify/callback" + "auth_url": "http://127.0.0.1:8080/spotify/login" }, { "name": "OpenWeather", @@ -42,7 +42,7 @@ def seed_services(): "name": "GitHub", "description": "GitHub is a platform where developers store and share their code online. It allows teams to collaborate on software projects from anywhere. You can track changes, review contributions, and manage updates easily. Developers use it to build, test, and improve their applications together. It’s the world’s largest community for open-source and coding projects.", "image": image_to_binary("github"), - "auth_url": "https://github.com/login/oauth/authorize?client_id=Ov23liHHgFODT3eZrD2N&scope=repo+user", + "auth_url": "http://127.0.0.1:8080/git/login", }, { "name": "NASA", diff --git a/server/services/spotify_service.py b/server/services/spotify_service.py index 0d0efc2..355a965 100644 --- a/server/services/spotify_service.py +++ b/server/services/spotify_service.py @@ -34,6 +34,26 @@ def get_actions(self): {"name": "currently_playing", "description": "recupere la musique actuellement jouée"} ] + def get_reactions_params(self, reaction_name): + if reaction_name == "play": + return [ + {"name": "citrack_uri", "type": "String", "required": False, "description": "URI de la piste à jouer (optionnel)"}, + {"name": "tokens", "type": "Object", "required": True, "description": "Tokens d'authentification Spotify"}, + {"name": "refresh_token", "type": "String", "required": True, "description": "Refresh token Spotify"} + ] + return [ + {"name": "tokens", "type": "Object", "required": True, "description": "Tokens d'authentification Spotify"}, + {"name": "refresh_token", "type": "String", "required": True, "description": "Refresh token Spotify"} + ] + + def get_action_params(self, action_name): + return [ + {"name": "access_token", "type": "String", "required": True, "description": "Access token Spotify"}, + {"name": "refresh_token", "type": "String", "required": True, "description": "Refresh token Spotify"}, + {"name": "last_title", "type": "String", "required": False, "description": "Dernier titre détecté (pour new_title)"}, + {"name": "last_updated", "type": "String", "required": False, "description": "Dernière mise à jour (ISO format)"} + ] + def get_reactions(self): return [ {"name": "play", "description": "Joue une musique"}, From e11f72ccefaf08588ba3adab96d19cb7efde6892 Mon Sep 17 00:00:00 2001 From: WilliamBSITT Date: Fri, 31 Oct 2025 13:35:42 +0100 Subject: [PATCH 14/16] [FIX] rename token parameters to access_token for consistency and fix parameter required for spotify --- server/services/github_service.py | 4 ++-- server/services/spotify_service.py | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/server/services/github_service.py b/server/services/github_service.py index ecfea11..e39fcaa 100644 --- a/server/services/github_service.py +++ b/server/services/github_service.py @@ -54,12 +54,12 @@ def get_reactions_params(self, reaction_name): {"name": "repo", "type": "string", "description": "Dépôt cible au format 'owner/repo'"}, {"name": "title", "type": "string", "description": "Titre de l’issue"}, {"name": "body", "type": "string", "description": "Corps de l’issue (peut inclure des placeholders pour les données d’action)"}, - {"name": "token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"} + {"name": "access_token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"} ] elif reaction_name == "star_repo": return [ {"name": "repo", "type": "string", "description": "Dépôt à étoiler au format 'owner/repo'"}, - {"name": "token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"} + {"name": "access_token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"} ] else: return [] diff --git a/server/services/spotify_service.py b/server/services/spotify_service.py index 355a965..9943cb7 100644 --- a/server/services/spotify_service.py +++ b/server/services/spotify_service.py @@ -38,20 +38,18 @@ def get_reactions_params(self, reaction_name): if reaction_name == "play": return [ {"name": "citrack_uri", "type": "String", "required": False, "description": "URI de la piste à jouer (optionnel)"}, - {"name": "tokens", "type": "Object", "required": True, "description": "Tokens d'authentification Spotify"}, + {"name": "access_tokens", "type": "Object", "required": True, "description": "Tokens d'authentification Spotify"}, {"name": "refresh_token", "type": "String", "required": True, "description": "Refresh token Spotify"} ] return [ - {"name": "tokens", "type": "Object", "required": True, "description": "Tokens d'authentification Spotify"}, + {"name": "access_tokens", "type": "Object", "required": True, "description": "Tokens d'authentification Spotify"}, {"name": "refresh_token", "type": "String", "required": True, "description": "Refresh token Spotify"} ] - def get_action_params(self, action_name): + def get_actions_params(self, action_name): return [ {"name": "access_token", "type": "String", "required": True, "description": "Access token Spotify"}, {"name": "refresh_token", "type": "String", "required": True, "description": "Refresh token Spotify"}, - {"name": "last_title", "type": "String", "required": False, "description": "Dernier titre détecté (pour new_title)"}, - {"name": "last_updated", "type": "String", "required": False, "description": "Dernière mise à jour (ISO format)"} ] def get_reactions(self): @@ -173,6 +171,3 @@ def execute_reaction(self, user, reaction, params=None, data=None): return True - def get_actions_params(self, action_name): - pass - From 5b845a6a30e11b58efe90f789e282bdbf75d472f Mon Sep 17 00:00:00 2001 From: WilliamBSITT Date: Fri, 31 Oct 2025 13:45:59 +0100 Subject: [PATCH 15/16] [FIX] name of acces_token for github --- server/services/github_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/services/github_service.py b/server/services/github_service.py index e39fcaa..db33521 100644 --- a/server/services/github_service.py +++ b/server/services/github_service.py @@ -36,13 +36,13 @@ def get_actions_params(self, action_name): if action_name == "new_commit": return [ {"name": "repo", "type": "string", "description": "Dépôt à surveiller au format 'owner/repo'"}, - {"name": "token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"}, + {"name": "access_token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"}, {"name": "last_commit", "type": "string", "description": "SHA du dernier commit vu (utilisé pour le suivi)"} ] elif action_name == "new_issue": return [ {"name": "repo", "type": "string", "description": "Dépôt à surveiller au format 'owner/repo'"}, - {"name": "token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"}, + {"name": "access_token", "type": "string", "description": "Token d'accès GitHub avec les permissions nécessaires"}, {"name": "last_issue", "type": "integer", "description": "ID de la dernière issue vue (utilisé pour le suivi)"} ] else: From 90b24bc704e313c04169a7d328c14d02d6f13a87 Mon Sep 17 00:00:00 2001 From: WilliamBSITT Date: Fri, 31 Oct 2025 15:16:21 +0100 Subject: [PATCH 16/16] [FIX] typo error of spotify service --- server/services/spotify_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/services/spotify_service.py b/server/services/spotify_service.py index 9943cb7..fb0dde2 100644 --- a/server/services/spotify_service.py +++ b/server/services/spotify_service.py @@ -38,11 +38,11 @@ def get_reactions_params(self, reaction_name): if reaction_name == "play": return [ {"name": "citrack_uri", "type": "String", "required": False, "description": "URI de la piste à jouer (optionnel)"}, - {"name": "access_tokens", "type": "Object", "required": True, "description": "Tokens d'authentification Spotify"}, + {"name": "access_token", "type": "Object", "required": True, "description": "Tokens d'authentification Spotify"}, {"name": "refresh_token", "type": "String", "required": True, "description": "Refresh token Spotify"} ] return [ - {"name": "access_tokens", "type": "Object", "required": True, "description": "Tokens d'authentification Spotify"}, + {"name": "access_token", "type": "Object", "required": True, "description": "Tokens d'authentification Spotify"}, {"name": "refresh_token", "type": "String", "required": True, "description": "Refresh token Spotify"} ]