Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 33 additions & 20 deletions server/core/area_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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()

76 changes: 58 additions & 18 deletions server/core/hook_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
logger = logging.getLogger("AREA-App")

def check_hooks(app=None):
"""
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:
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:
Expand All @@ -23,28 +27,64 @@ 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:
# 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

params = area.params or {}
data = act_srv.check_action(user, area.action, params=params)
combined_data = {}
has_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)
# === MULTI-ACTIONS ===
for action_def in (area.actions or []):
try:
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}")
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:
has_triggered = True
combined_data.update(data)
logger.info(f"[check_hooks] AREA {area.id} triggered via {action_def['service']}.{action_name}")

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 []):
try:
rea_srv = services_map.get(reaction_def.get("service"))
reaction_name = reaction_def.get("name")
params = reaction_def.get("params", {})

logger.info(f"[check_hooks] Executing reaction {reaction_def}")

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 {getattr(area, 'id', '?')}: {e}")

12 changes: 7 additions & 5 deletions server/core/reaction_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)




54 changes: 27 additions & 27 deletions server/models/area.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

# 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
Loading