334 lines
14 KiB
Python
334 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Watchdog pour l'interprétation automatique des emails GMAO - Pattern DND."""
|
|
|
|
import sys
|
|
import os
|
|
import time
|
|
import json
|
|
import signal
|
|
import requests
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
# Définir le répertoire de travail
|
|
WORKDIR = os.environ.get('DOCKER_GMAO_WORKDIR', '/root/hermes-workspace/gmao-college')
|
|
os.chdir(WORKDIR)
|
|
sys.path.insert(0, WORKDIR)
|
|
|
|
# Activer le venv
|
|
venv_site_packages = os.path.join(WORKDIR, 'venv', 'lib', 'python3.13', 'site-packages')
|
|
if os.path.exists(venv_site_packages):
|
|
sys.path.insert(0, venv_site_packages)
|
|
|
|
# Charger les variables d'environnement depuis .env
|
|
from dotenv import load_dotenv
|
|
env_file = os.environ.get('DOCKER_GMAO_ENVFILE', os.path.join(WORKDIR, '.env'))
|
|
load_dotenv(env_file)
|
|
|
|
# Fichier de log et de dernière exécution
|
|
LOG_FILE = '/tmp/gmao_watchdog.log'
|
|
LAST_RUN_FILE = '/tmp/gmao_last_run.txt'
|
|
|
|
running = True
|
|
|
|
def log(message, level='info'):
|
|
"""Écrit un message dans le log et dans la base centralisee."""
|
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
print(f'[{timestamp}] {message}')
|
|
with open(LOG_FILE, 'a') as f:
|
|
f.write(f'[{timestamp}] {message}\n')
|
|
# Log centralise
|
|
try:
|
|
sys.path.insert(0, '/app')
|
|
from app_new.watchdog_logger import log_watchdog
|
|
log_watchdog('gmao_watchdog', message, level=level)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def get_last_run():
|
|
"""Retourne la date de dernière exécution."""
|
|
if os.path.exists(LAST_RUN_FILE):
|
|
try:
|
|
with open(LAST_RUN_FILE, 'r') as f:
|
|
return datetime.fromisoformat(f.read().strip())
|
|
except:
|
|
return None
|
|
return None
|
|
|
|
def save_last_run():
|
|
"""Sauvegarde la date de dernière exécution."""
|
|
with open(LAST_RUN_FILE, 'w') as f:
|
|
f.write(datetime.now().isoformat())
|
|
|
|
def should_run(gmao_context):
|
|
"""Vérifie si le watchdog doit s'exécuter selon l'intervalle configuré."""
|
|
if not gmao_context or not gmao_context.auto_interpret_enabled:
|
|
return False
|
|
|
|
last_run = get_last_run()
|
|
if not last_run:
|
|
return True
|
|
|
|
interval_minutes = gmao_context.auto_interpret_interval or 10
|
|
next_run = last_run + timedelta(minutes=interval_minutes)
|
|
|
|
return datetime.now() >= next_run
|
|
|
|
def sync_inbox():
|
|
"""Synchronise la boîte de réception Outlook."""
|
|
from app_new import create_app, db
|
|
from app_new.outlook.models import OutlookAccount, OutlookFolder, OutlookMail, OutlookAttachment
|
|
from app_new.outlook.auth import get_access_token
|
|
|
|
app = create_app()
|
|
with app.app_context():
|
|
# Récupérer le compte Outlook
|
|
account = OutlookAccount.query.filter_by(is_active=True).first()
|
|
if not account:
|
|
log("Aucun compte Outlook actif")
|
|
return {'success': False, 'error': 'Aucun compte Outlook actif'}
|
|
|
|
# Obtenir le token
|
|
access_token, error = get_access_token(account)
|
|
if error:
|
|
log(f"Erreur token: {error}")
|
|
return {'success': False, 'error': error}
|
|
|
|
# Trouver le dossier inbox par nom (Boîte de réception)
|
|
folder = OutlookFolder.query.filter_by(account_id=account.id, name='Boîte de réception').first()
|
|
if not folder:
|
|
# Fallback: chercher par folder_type
|
|
folder = OutlookFolder.query.filter_by(account_id=account.id, folder_type='inbox').first()
|
|
if not folder:
|
|
log("Dossier inbox non trouvé")
|
|
return {'success': False, 'error': 'Dossier inbox non trouvé'}
|
|
|
|
# Paramètres de synchronisation
|
|
max_messages = 50 # Limite pour le watchdog
|
|
headers = {'Authorization': f'Bearer {access_token}'}
|
|
|
|
# Récupérer les messages récents
|
|
url = f'https://outlook.office365.com/api/v2.0/me/MailFolders/{folder.id}/messages?$top={max_messages}&$orderby=ReceivedDateTime desc&$select=Id,Subject,From,ReceivedDateTime,IsRead,HasAttachments,BodyPreview,Body,ToRecipients,CcRecipients,Importance'
|
|
|
|
try:
|
|
resp = requests.get(url, headers=headers, timeout=30)
|
|
|
|
if resp.status_code != 200:
|
|
error_msg = resp.json().get('error', {}).get('message', resp.text) if resp.json() else resp.text
|
|
log(f"Erreur API Outlook: {error_msg}")
|
|
return {'success': False, 'error': error_msg}
|
|
|
|
data = resp.json()
|
|
mails_data = data.get('value', [])
|
|
|
|
saved_count = 0
|
|
updated_count = 0
|
|
|
|
for mail_data in mails_data:
|
|
mail_id = mail_data.get('Id')
|
|
existing = OutlookMail.query.filter_by(id=mail_id).first()
|
|
|
|
from_addr = mail_data.get('From', {}).get('EmailAddress', {})
|
|
body = mail_data.get('Body', {})
|
|
received_str = mail_data.get('ReceivedDateTime', '')
|
|
try:
|
|
received_at = datetime.fromisoformat(received_str.replace('Z', '+00:00'))
|
|
except:
|
|
received_at = datetime.utcnow()
|
|
|
|
# Destinataires
|
|
to_recipients = json.dumps([r.get('EmailAddress', {}).get('Address', '') for r in mail_data.get('ToRecipients', [])])
|
|
cc_recipients = json.dumps([r.get('EmailAddress', {}).get('Address', '') for r in mail_data.get('CcRecipients', [])])
|
|
|
|
if not existing:
|
|
mail = OutlookMail(
|
|
id=mail_id,
|
|
account_id=account.id,
|
|
folder_id=folder.id,
|
|
subject=mail_data.get('Subject', ''),
|
|
from_name=from_addr.get('Name', ''),
|
|
from_email=from_addr.get('Address', ''),
|
|
to_recipients=to_recipients,
|
|
cc_recipients=cc_recipients,
|
|
received_at=received_at,
|
|
is_read=mail_data.get('IsRead', False),
|
|
has_attachments=mail_data.get('HasAttachments', False),
|
|
body_preview=mail_data.get('BodyPreview', ''),
|
|
body_content=body.get('content', '') if body.get('contentType') == 'html' else '',
|
|
body_type=body.get('contentType', 'text'),
|
|
importance=mail_data.get('Importance', 'normal')
|
|
)
|
|
db.session.add(mail)
|
|
saved_count += 1
|
|
else:
|
|
# Mettre à jour si nécessaire
|
|
existing.is_read = mail_data.get('IsRead', existing.is_read)
|
|
existing.has_attachments = mail_data.get('HasAttachments', existing.has_attachments)
|
|
updated_count += 1
|
|
|
|
db.session.commit()
|
|
|
|
# Télécharger les pièces jointes PDF pour tous les emails avec pièces jointes
|
|
pdf_count = 0
|
|
emails_with_attachments = [m for m in mails_data if m.get('HasAttachments')]
|
|
|
|
for mail_data in emails_with_attachments:
|
|
mail_id = mail_data.get('Id')
|
|
try:
|
|
# Récupérer les pièces jointes
|
|
att_url = f'https://outlook.office365.com/api/v2.0/me/messages/{mail_id}/attachments'
|
|
att_resp = requests.get(att_url, headers=headers, timeout=30)
|
|
|
|
if att_resp.status_code == 200:
|
|
attachments_data = att_resp.json().get('value', [])
|
|
for att in attachments_data:
|
|
att_id = att.get('Id')
|
|
att_name = att.get('Name', '')
|
|
att_content_type = att.get('ContentType', '')
|
|
|
|
# Vérifier si c'est un PDF
|
|
if att_name.lower().endswith('.pdf') or 'pdf' in att_content_type.lower():
|
|
# Vérifier si déjà téléchargé
|
|
existing_att = OutlookAttachment.query.filter_by(id=att_id).first()
|
|
if not existing_att:
|
|
# Télécharger le contenu
|
|
content_url = f'https://outlook.office365.com/api/v2.0/me/messages/{mail_id}/attachments/{att_id}/$value'
|
|
content_resp = requests.get(content_url, headers=headers, timeout=60)
|
|
|
|
if content_resp.status_code == 200:
|
|
new_att = OutlookAttachment(
|
|
id=att_id,
|
|
mail_id=mail_id,
|
|
name=att_name,
|
|
content_type=att_content_type,
|
|
size=len(content_resp.content),
|
|
content=content_resp.content
|
|
)
|
|
db.session.add(new_att)
|
|
pdf_count += 1
|
|
log(f" PDF téléchargé: {att_name}")
|
|
except Exception as e:
|
|
log(f" Erreur téléchargement pièce jointe: {str(e)[:50]}")
|
|
|
|
if pdf_count > 0:
|
|
db.session.commit()
|
|
log(f"PDFs téléchargés: {pdf_count}")
|
|
|
|
# Mettre à jour la date de dernière connexion Outlook
|
|
account.connection_status = 'active'
|
|
account.last_connected_at = datetime.now(timezone.utc)
|
|
db.session.commit()
|
|
|
|
log(f"Sync: {saved_count} nouveaux, {updated_count} mis à jour")
|
|
return {'success': True, 'saved': saved_count, 'updated': updated_count}
|
|
|
|
except Exception as e:
|
|
log(f"Erreur sync: {str(e)}")
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
def run_watchdog():
|
|
"""Exécute un cycle du watchdog - retourne False si désactivé."""
|
|
global running
|
|
|
|
from app_new import create_app, db
|
|
from app_new.outlook.models import GmaoContext
|
|
from app_new.outlook.auto_interpret import run_auto_interpret
|
|
|
|
app = create_app()
|
|
with app.app_context():
|
|
# Récupérer le contexte GMAO (tous les contextes actifs)
|
|
gmao_context = GmaoContext.query.filter_by(auto_interpret_enabled=True).first()
|
|
|
|
# Vérifier si activé
|
|
if not gmao_context or not gmao_context.auto_interpret_enabled:
|
|
log("Watchdog désactivé dans la config")
|
|
return False # Arrêter le watchdog
|
|
|
|
if not should_run(gmao_context):
|
|
# Pas le moment, attendre
|
|
return True
|
|
|
|
interval = gmao_context.auto_interpret_interval or 10
|
|
log(f"Exécution de l'interprétation (intervalle: {interval} min)...")
|
|
|
|
try:
|
|
log("Démarrage synchronisation...")
|
|
# 1. D'abord synchroniser les emails
|
|
sync_result = sync_inbox()
|
|
log(f"Sync result: {sync_result}")
|
|
if sync_result.get('success'):
|
|
log(f"Sync: {sync_result.get('saved', 0)} nouveaux, {sync_result.get('updated', 0)} mis à jour")
|
|
else:
|
|
log(f"Erreur sync inbox: {sync_result.get('error', 'inconnue')}")
|
|
|
|
log("Démarrage interprétation...")
|
|
# 2. Ensuite interpréter
|
|
result = run_auto_interpret()
|
|
log(f"Interpret result: processed={result.get('processed', 0)}, created={result.get('created', 0)}")
|
|
|
|
if result.get('errors'):
|
|
for err in result.get('errors', []):
|
|
log(f" ⚠ Erreur email: {err[:100]}")
|
|
|
|
save_last_run()
|
|
log("save_last_run done")
|
|
|
|
# Commit final pour libérer la session
|
|
db.session.commit()
|
|
log("db.session.commit done")
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
import traceback
|
|
log(f"ERREUR dans le cycle: {str(e)}")
|
|
log(f"Traceback: {traceback.format_exc()[:500]}")
|
|
# Continuer quand même
|
|
try:
|
|
db.session.rollback()
|
|
except:
|
|
pass
|
|
|
|
return True # Continuer
|
|
|
|
def main():
|
|
"""Point d'entrée principal - pattern DND avec redémarrage automatique."""
|
|
global running
|
|
|
|
log("="*50)
|
|
log("Watchdog GMAO démarré (pattern DND)")
|
|
log("="*50)
|
|
|
|
# Capturer le signal d'arrêt
|
|
def signal_handler(signum, frame):
|
|
global running
|
|
running = False
|
|
log("Signal reçu, arrêt du watchdog...")
|
|
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
while running:
|
|
result = None
|
|
try:
|
|
result = run_watchdog()
|
|
if result is False:
|
|
log("Watchdog désactivé - arrêt")
|
|
break # Sortir proprement si désactivé
|
|
except Exception as e:
|
|
import traceback
|
|
log(f"Crash: {str(e)}", level='error')
|
|
log(f"Traceback: {traceback.format_exc()[-500:]}", level='error')
|
|
# Redémarrer automatiquement après crash
|
|
|
|
# Attendre avant la prochaine vérification
|
|
# Si le watchdog est désactivé, on attend plus longtemps pour éviter les redémarrages incessants
|
|
wait_seconds = 300 if result is False else 30
|
|
for _ in range(wait_seconds):
|
|
if not running:
|
|
break
|
|
time.sleep(1)
|
|
|
|
log("Watchdog GMAO arrêté")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|