From 73afc6ccd147619b28d5026509e0abc5a0304ebd Mon Sep 17 00:00:00 2001 From: jonazakana Date: Mon, 27 Oct 2025 13:52:13 +0100 Subject: [PATCH 1/3] edit engine --- server/core/hook_engine.py | 58 +++++++++++++++++++++--------- server/core/reaction_executor.py | 12 ++++--- server/models/area.py | 54 ++++++++++++++-------------- server/routes/area.py | 1 + server/services/gmail_service.py | 12 +++---- server/services/weather_service.py | 26 +++++++------- 6 files changed, 94 insertions(+), 69 deletions(-) diff --git a/server/core/hook_engine.py b/server/core/hook_engine.py index c66e570..3ce259c 100644 --- a/server/core/hook_engine.py +++ b/server/core/hook_engine.py @@ -9,12 +9,16 @@ logger = logging.getLogger("AREA-App") def check_hooks(app=None): + """ + Parcourt tous les Area activés, vérifie les actions + et exécute les réactions correspondantes si une action est déclenchée. + """ ctx = app.app_context() if app else current_app.app_context() with ctx: - logger.info("[check_hooks] checking AREAs...") + logger.info("[check_hooks] Checking AREAs...") + services_map = {s.name: s for s in get_all_services()} areas = Area.query.filter_by(enabled=True).all() - now = datetime.now(timezone.utc) for area in areas: @@ -23,28 +27,48 @@ def check_hooks(app=None): if not user: continue - act_srv = services_map.get(area.action_service) - rea_srv = services_map.get(area.reaction_service) - if not act_srv or not rea_srv: - continue - if area.last_run and area.frequency != 60: 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) + triggered_data = None + triggered = False - if data: - 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) + for action_def in (area.actions or []): + logger.info(action_def) + act_srv = services_map.get(action_def.get("service")) + action_name = action_def.get("name") + params = action_def.get("params", {}) - except Exception as e: - logger.exception(f"[check_hooks] Error AREA {area.id}: {e}") + if not act_srv: + logger.warning(f"[check_hooks] Unknown action service: {action_def.get('service')}") + continue + data = act_srv.check_action(user, action_name, params=params) + if data: + triggered = True + triggered_data = data + logger.info(f"[check_hooks] AREA {area.id} triggered via {action_def['service']}.{action_name}") + break + if not triggered: + continue + area.last_run = now + db.session.commit() + + for reaction_def in (area.reactions or []): + logger.info(reaction_def) + rea_srv = services_map.get(reaction_def.get("service")) + reaction_name = reaction_def.get("name") + params = reaction_def.get("params", {}) + + if not rea_srv: + logger.warning(f"[check_hooks] Unknown reaction service: {reaction_def.get('service')}") + continue + + reaction_executor(user, rea_srv, reaction_name, params=params, data=triggered_data) + + except Exception as e: + logger.exception(f"[check_hooks] Error AREA {area.id if 'area' in locals() else '?'}: {e}") diff --git a/server/core/reaction_executor.py b/server/core/reaction_executor.py index d7f8eef..0d8c603 100644 --- a/server/core/reaction_executor.py +++ b/server/core/reaction_executor.py @@ -4,25 +4,27 @@ def reaction_executor(user, service, reaction, params=None, data=None): try: - logger.info(f"[reaction_executor] execute {reaction} for {user.email} on {service.name}") + logger.info(f"[reaction_executor] Execute {reaction} for {user.email} on {service.name}") - # formater le message si besoin + # Format dynamique des messages (optionnel) if params and "message" in params and data: try: params["message"] = params["message"].format(**data) except KeyError as e: missing = e.args[0] - logger.error(f"[reaction_executor] variable manquante dans le message : {missing}") + logger.warning(f"[reaction_executor] Missing variable in message: {missing}") params["message"] = f"[Erreur] variable manquante : {missing}" + # Appel du service service.execute_reaction(user, reaction, params=params, data=data) - logger.info(f"[reaction_executor] ✅ reaction {reaction} executed for {user.email}") + logger.info(f"[reaction_executor] Reaction {reaction} executed for {user.email}") return True, None except Exception as e: - logger.error(f"[reaction_executor] ❌ error executing {reaction} for {user.email}: {e}", exc_info=True) + logger.error(f"[reaction_executor] Error executing {reaction} for {user.email}: {e}", exc_info=True) return False, str(e) + diff --git a/server/models/area.py b/server/models/area.py index 233672f..0e1d17c 100644 --- a/server/models/area.py +++ b/server/models/area.py @@ -8,38 +8,38 @@ class Area(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) - action_service = db.Column(db.String(80), nullable=False) - action = db.Column(db.String(80), nullable=False) - reaction_service = db.Column(db.String(80), nullable=False) - reaction = db.Column(db.String(80), nullable=False) + + actions = db.Column(db.JSON, nullable=False) + reactions = db.Column(db.JSON, nullable=False) + frequency = db.Column(db.Integer, default=3600) last_run = db.Column(db.DateTime, default=None) - params = db.Column(db.JSON, nullable=True) enabled = db.Column(db.Boolean, default=True) public = db.Column(db.Boolean, default=False) user = db.relationship("User", backref="areas") - def set_params(self, params: dict): - - safe = {} - for k, v in (params or {}).items(): - if "password" in k.lower(): - safe[k] = crypto.encrypt(v) - else: - safe[k] = v - self.params = safe - - def get_params(self) -> dict: - - safe = {} - for k, v in (self.params or {}).items(): - if isinstance(v, str) and "password" in k.lower(): - try: - safe[k] = crypto.decrypt(v) - except Exception: - safe[k] = v - else: - safe[k] = v - return safe \ No newline at end of file + + # def set_params(self, params: dict): + + # safe = {} + # for k, v in (params or {}).items(): + # if "password" in k.lower(): + # safe[k] = crypto.encrypt(v) + # else: + # safe[k] = v + # self.params = safe + + # def get_params(self) -> dict: + + # safe = {} + # for k, v in (self.params or {}).items(): + # if isinstance(v, str) and "password" in k.lower(): + # try: + # safe[k] = crypto.decrypt(v) + # except Exception: + # safe[k] = v + # else: + # safe[k] = v + # return safe \ No newline at end of file diff --git a/server/routes/area.py b/server/routes/area.py index 6d109e7..b3d32b0 100644 --- a/server/routes/area.py +++ b/server/routes/area.py @@ -71,6 +71,7 @@ def create_area(): enabled=data.get("enabled", True), name=data.get("name", "My AREA"), frequency=data.get("frequency", 3600), + ) if err: diff --git a/server/services/gmail_service.py b/server/services/gmail_service.py index 8298d64..740b3e7 100644 --- a/server/services/gmail_service.py +++ b/server/services/gmail_service.py @@ -64,11 +64,11 @@ def execute_reaction(self, user, reaction, params=None, data=None): print("[GmailService] Paramètres manquants") return False - try: - password = crypto.decrypt(password_enc) - except Exception as e: - print(f"[GmailService] Erreur de déchiffrement: {e}") - return False + # try: + # password = crypto.decrypt(password_enc) + # except Exception as e: + # print(f"[GmailService] Erreur de déchiffrement: {e}") + # return False if not self.validate_email(to_address): print(f"[GmailService] Adresse email invalide: {to_address}") @@ -88,7 +88,7 @@ def execute_reaction(self, user, reaction, params=None, data=None): content = content_template msg = self.create_email(from_address, to_address, subject, content) - return self.send_email(from_address, password, to_address, msg) + return self.send_email(from_address, password_enc, to_address, msg) else: print(f"[GmailService] Réaction inconnue: {reaction}") diff --git a/server/services/weather_service.py b/server/services/weather_service.py index 4bcb799..c07b757 100644 --- a/server/services/weather_service.py +++ b/server/services/weather_service.py @@ -23,41 +23,39 @@ def check_action(self, user, action, params=None): if action != "get_weather": return None - interval = 3600 - - last = params.get("last_triggered_at") - if last: - if isinstance(last, str): - last = datetime.fromisoformat(last) - - if datetime.now(timezone.utc) - last < timedelta(seconds=interval): - return None - - city = (params or {}).get("city", "Paris") + params = params or {} + city = params.get("city", "Paris") weather = self.fetch_weather(city) + if not weather: + print(f"[OpenWeatherService] Failed to fetch weather for city: {city}") return None - - params["last_triggered_at"] = datetime.now(timezone.utc).isoformat() - + return { "city": city, "temp": weather["main"]["temp"], "desc": weather["weather"][0]["description"] } + def execute_reaction(self, user, reaction, params=None, data=None): pass def fetch_weather(self, city): + if not self.api_key: + raise ValueError("OPENWEATHER_API_KEY manquant dans le .env") + resp = requests.get(self.base_url, params={ "q": city, "appid": self.api_key, "units": "metric", "lang": "fr" }) + if resp.status_code != 200: + print(f"[OpenWeatherService] API error: {resp.text}") return None + return resp.json() def get_actions_params(self, action_name): From a9ecf16789edd193b0d1fa9b3edef8bb75c8d989 Mon Sep 17 00:00:00 2001 From: jonazakana Date: Mon, 27 Oct 2025 14:58:18 +0100 Subject: [PATCH 2/3] edit areas CRUD --- server/core/area_manager.py | 53 ++++++++++++++++----------- server/core/hook_engine.py | 72 ++++++++++++++++++++++--------------- server/routes/area.py | 28 ++++++++------- 3 files changed, 92 insertions(+), 61 deletions(-) diff --git a/server/core/area_manager.py b/server/core/area_manager.py index d8f65ac..4921a31 100644 --- a/server/core/area_manager.py +++ b/server/core/area_manager.py @@ -7,45 +7,57 @@ def _get_services_map(): """Retourne un dict { service_name: service_instance }""" return {s.name: s for s in get_all_services()} -def create_area(user_id, action_service, action, reaction_service, reaction, params=None, enabled=True, name="My AREA", frequency=3600, public=False): + +def create_area(user_id, actions, reactions, params=None, enabled=True, name="My AREA", frequency=3600, public=False): + """Crée un AREA avec plusieurs actions et réactions.""" user = User.query.get(user_id) if not user: return None, "Utilisateur introuvable" services = _get_services_map() - act_srv = services.get(action_service) - rea_srv = services.get(reaction_service) - if not act_srv or not rea_srv: - return None, "Service action ou réaction introuvable" - allowed_actions = {a["name"] for a in act_srv.get_actions()} - allowed_reactions = {r["name"] for r in rea_srv.get_reactions()} + for action in actions: + srv_name = action.get("service") + action_name = action.get("name") + srv = services.get(srv_name) + + if not srv: + return None, f"Service d’action inconnu : {srv_name}" + allowed_actions = {a["name"] for a in srv.get_actions()} + if action_name not in allowed_actions: + return None, f"Action '{action_name}' invalide pour {srv_name}" + + + for reaction in reactions: + srv_name = reaction.get("service") + reaction_name = reaction.get("name") + srv = services.get(srv_name) + + if not srv: + return None, f"Service de réaction inconnu : {srv_name}" + allowed_reactions = {r["name"] for r in srv.get_reactions()} + if reaction_name not in allowed_reactions: + return None, f"Réaction '{reaction_name}' invalide pour {srv_name}" - if action not in allowed_actions: - return None, f"Action '{action}' invalide pour {action_service}" - if reaction not in allowed_reactions: - return None, f"Reaction '{reaction}' invalide pour {reaction_service}" area = Area( name=name, user_id=user_id, - action_service=action_service, - action=action, - reaction_service=reaction_service, - reaction=reaction, + actions=actions, + reactions=reactions, enabled=enabled, frequency=frequency, - public=public + public=public, + params=params or {} ) - area.set_params(params) + db.session.add(area) db.session.commit() return area, None def delete_area(area_id, user_id=None): - """Supprime un AREA""" q = Area.query.filter_by(id=area_id) if user_id: q = q.filter_by(user_id=user_id) @@ -56,8 +68,8 @@ def delete_area(area_id, user_id=None): db.session.commit() return True, None + def toggle_area(area_id, enabled=True, user_id=None): - """Active/désactive un AREA""" q = Area.query.filter_by(id=area_id) if user_id: q = q.filter_by(user_id=user_id) @@ -68,9 +80,10 @@ def toggle_area(area_id, enabled=True, user_id=None): db.session.commit() return area, None + def list_areas(user_id=None): - """Liste tous les AREAs ou ceux d’un utilisateur""" q = Area.query if user_id: q = q.filter_by(user_id=user_id) return q.all() + diff --git a/server/core/hook_engine.py b/server/core/hook_engine.py index 3ce259c..a405cd0 100644 --- a/server/core/hook_engine.py +++ b/server/core/hook_engine.py @@ -10,8 +10,8 @@ def check_hooks(app=None): """ - Parcourt tous les Area activés, vérifie les actions - et exécute les réactions correspondantes si une action est déclenchée. + Parcourt tous les Area activés, exécute toutes les actions et + agrège leurs résultats avant de déclencher toutes les réactions. """ ctx = app.app_context() if app else current_app.app_context() with ctx: @@ -27,48 +27,64 @@ def check_hooks(app=None): if not user: continue - if area.last_run and area.frequency != 60: + # Gestion de la fréquence + if area.last_run: last_run = area.last_run.replace(tzinfo=timezone.utc) if (now - last_run) < timedelta(seconds=area.frequency): continue - triggered_data = None - triggered = False + combined_data = {} + has_triggered = False + # === MULTI-ACTIONS === for action_def in (area.actions or []): - logger.info(action_def) - act_srv = services_map.get(action_def.get("service")) - action_name = action_def.get("name") - params = action_def.get("params", {}) + try: + act_srv = services_map.get(action_def.get("service")) + action_name = action_def.get("name") + params = action_def.get("params", {}) - if not act_srv: - logger.warning(f"[check_hooks] Unknown action service: {action_def.get('service')}") - continue + logger.info(f"[check_hooks] Checking action {action_def}") + + if not act_srv: + logger.warning(f"[check_hooks] Unknown action service: {action_def.get('service')}") + continue - data = act_srv.check_action(user, action_name, params=params) - if data: - triggered = True - triggered_data = data - logger.info(f"[check_hooks] AREA {area.id} triggered via {action_def['service']}.{action_name}") - break + data = act_srv.check_action(user, action_name, params=params) + if data: + has_triggered = True + combined_data.update(data) + logger.info(f"[check_hooks] AREA {area.id} triggered via {action_def['service']}.{action_name}") - if not triggered: + except Exception as e: + logger.error(f"[check_hooks] Action {action_def.get('service')} failed: {e}", exc_info=True) + continue # continue avec les autres actions + + if not has_triggered: continue + # Marquer comme exécuté area.last_run = now db.session.commit() + # === MULTI-RÉACTIONS === for reaction_def in (area.reactions or []): - logger.info(reaction_def) - rea_srv = services_map.get(reaction_def.get("service")) - reaction_name = reaction_def.get("name") - params = reaction_def.get("params", {}) + try: + rea_srv = services_map.get(reaction_def.get("service")) + reaction_name = reaction_def.get("name") + params = reaction_def.get("params", {}) - if not rea_srv: - logger.warning(f"[check_hooks] Unknown reaction service: {reaction_def.get('service')}") - continue + logger.info(f"[check_hooks] Executing reaction {reaction_def}") - reaction_executor(user, rea_srv, reaction_name, params=params, data=triggered_data) + if not rea_srv: + logger.warning(f"[check_hooks] Unknown reaction service: {reaction_def.get('service')}") + continue + + reaction_executor(user, rea_srv, reaction_name, params=params, data=combined_data) + + except Exception as e: + logger.error(f"[check_hooks] Reaction {reaction_def.get('service')} failed: {e}", exc_info=True) + continue # continue même si une réaction plante except Exception as e: - logger.exception(f"[check_hooks] Error AREA {area.id if 'area' in locals() else '?'}: {e}") + logger.exception(f"[check_hooks] Error AREA {getattr(area, 'id', '?')}: {e}") + diff --git a/server/routes/area.py b/server/routes/area.py index b3d32b0..21c03a4 100644 --- a/server/routes/area.py +++ b/server/routes/area.py @@ -51,7 +51,7 @@ def create_area(): example: "send_message" params: type: object - example: { "city": "Nancy", "message": "🌦️ Météo {city} : {temp}°C, {desc}", "channel_id": "1424684119471689759" } + example: { "city": "Nancy", "message": "Météo {city} : {temp}°C, {desc}", "channel_id": "1424684119471689759" } responses: 201: description: AREA créé avec succès @@ -61,34 +61,36 @@ def create_area(): user_id = int(get_jwt_identity()) data = request.get_json() + actions = data.get("actions") + reactions = data.get("reactions") + + if not actions or not reactions: + return jsonify({"error": "Les champs 'actions' et 'reactions' sont obligatoires"}), 400 + area, err = area_manager.create_area( user_id, - data.get("action_service").lower(), - data.get("action"), - data.get("reaction_service").lower(), - data.get("reaction"), - params=data.get("params", {}), + actions=actions, + reactions=reactions, enabled=data.get("enabled", True), name=data.get("name", "My AREA"), frequency=data.get("frequency", 3600), - + public=data.get("public", False) ) if err: return jsonify({"error": err}), 400 return jsonify({ - "name": area.name, "id": area.id, - "action_service": area.action_service, - "action": area.action, - "reaction_service": area.reaction_service, - "reaction": area.reaction, - "params": area.params, + "name": area.name, + "actions": area.actions, + "reactions": area.reactions, "enabled": area.enabled, "frequency": int(area.frequency), + "public": area.public }), 201 + @bp.route("/", methods=["GET"]) @jwt_required() def get_area(area_id): From 97ba690dc7d8917c18e32a47542722a1b51a5994 Mon Sep 17 00:00:00 2001 From: jonazakana Date: Mon, 27 Oct 2025 15:02:08 +0100 Subject: [PATCH 3/3] edit crud --- server/routes/area.py | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/server/routes/area.py b/server/routes/area.py index 21c03a4..d07f967 100644 --- a/server/routes/area.py +++ b/server/routes/area.py @@ -129,12 +129,9 @@ def get_area(area_id): return jsonify({ "id": result[0].id, "name": result[0].name, - "action_service": result[0].action_service, - "action": result[0].action, - "reaction_service": result[0].reaction_service, - "reaction": result[0].reaction, + "actions": result[0].actions, + "reaction": result[0].reactions, "frequency": result[0].frequency, - "params": result[0].params, "last_run": result[0].last_run.isoformat() if result[0].last_run else None, "enabled": result[0].enabled, "public": result[0].public @@ -166,10 +163,8 @@ def list_public_areas(): { "id": a.id, "name": a.name, - "action_service": a.action_service, - "action": a.action, - "reaction_service": a.reaction_service, - "reaction": a.reaction, + "action": a.actions, + "reaction": a.reactions, "frequency": a.frequency } for a in areas if a.public ]) @@ -201,8 +196,8 @@ def list_areas(): { "id": a.id, "name": a.name, - "action_service": a.action_service, - "reaction_service": a.reaction_service, + "actions": a.actions, + "reactions": a.reactions, "enabled": a.enabled } for a in areas ]) @@ -308,16 +303,10 @@ def update_area(area_id): area.enabled = data["enabled"] if "name" in data: area.name = data["name"] - if "action_service" in data: - area.action_service = data["action_service"].lower() - if "action" in data: - area.action = data["action"] - if "reaction_service" in data: - area.reaction_service = data["reaction_service"].lower() - if "reaction" in data: - area.reaction = data["reaction"] - if "params" in data: - area.params = data["params"] + if "actions" in data: + area.actions = data["actions"] + if "reactions" in data: + area.reactions = data["reactions"] if "frequency" in data: area.frequency = data["frequency"] if "public" in data: