mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Fix notification System
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Email SMTP sender – invio notifiche via SMTP con TLS/SSL o STARTTLS.
|
||||
|
||||
Config non sensibile (config):
|
||||
{
|
||||
"smtp_host": "smtp.example.com",
|
||||
"smtp_port": 465,
|
||||
"smtp_use_tls": true, # SSL/TLS diretto (porta 465)
|
||||
"smtp_use_starttls": false, # STARTTLS (porta 587) – alternativo a use_tls
|
||||
"from_email": "noreply@example.com",
|
||||
"from_name": "PEChub Notifiche",
|
||||
"to_email": "destinatario@example.com"
|
||||
}
|
||||
|
||||
Config sensibile (config_enc → config_secret):
|
||||
{ "smtp_password": "..." }
|
||||
|
||||
Dipendenza: aiosmtplib (gia' in backend/pyproject.toml)
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import aiosmtplib
|
||||
|
||||
DEFAULT_TIMEOUT = 15.0
|
||||
|
||||
|
||||
class EmailSMTPError(Exception):
|
||||
"""Errore durante l'invio di un'email di notifica."""
|
||||
|
||||
def __init__(self, message: str, smtp_code: int | None = None):
|
||||
super().__init__(message)
|
||||
self.smtp_code = smtp_code
|
||||
|
||||
|
||||
async def send_email_notification(
|
||||
smtp_host: str,
|
||||
smtp_port: int,
|
||||
smtp_user: str,
|
||||
smtp_password: str,
|
||||
from_email: str,
|
||||
to_email: str,
|
||||
subject: str,
|
||||
body_text: str,
|
||||
body_html: str | None = None,
|
||||
from_name: str = "PEChub Notifiche",
|
||||
use_tls: bool = True,
|
||||
use_starttls: bool = False,
|
||||
timeout: float = DEFAULT_TIMEOUT,
|
||||
) -> None:
|
||||
"""
|
||||
Invia un'email di notifica via SMTP.
|
||||
|
||||
Args:
|
||||
smtp_host: host SMTP
|
||||
smtp_port: porta SMTP
|
||||
smtp_user: username autenticazione
|
||||
smtp_password: password autenticazione
|
||||
from_email: indirizzo mittente
|
||||
to_email: indirizzo destinatario
|
||||
subject: oggetto email
|
||||
body_text: testo plain
|
||||
body_html: testo HTML (opzionale)
|
||||
from_name: nome visualizzato mittente
|
||||
use_tls: usa SSL/TLS diretto (porta 465)
|
||||
use_starttls: usa STARTTLS (porta 587) – alternativo a use_tls
|
||||
timeout: timeout connessione in secondi
|
||||
|
||||
Raises:
|
||||
EmailSMTPError: in caso di errori di autenticazione, connessione o invio
|
||||
"""
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = f"{from_name} <{from_email}>" if from_name else from_email
|
||||
msg["To"] = to_email
|
||||
msg["Date"] = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||
msg["X-Mailer"] = "PEChub/1.0"
|
||||
|
||||
msg.attach(MIMEText(body_text, "plain", "utf-8"))
|
||||
if body_html:
|
||||
msg.attach(MIMEText(body_html, "html", "utf-8"))
|
||||
|
||||
try:
|
||||
await aiosmtplib.send(
|
||||
msg,
|
||||
hostname=smtp_host,
|
||||
port=smtp_port,
|
||||
username=smtp_user,
|
||||
password=smtp_password,
|
||||
use_tls=use_tls,
|
||||
start_tls=use_starttls,
|
||||
timeout=timeout,
|
||||
)
|
||||
except aiosmtplib.SMTPAuthenticationError as exc:
|
||||
raise EmailSMTPError(
|
||||
f"Autenticazione SMTP fallita per {smtp_user}@{smtp_host}: {exc}",
|
||||
smtp_code=535,
|
||||
) from exc
|
||||
except aiosmtplib.SMTPConnectError as exc:
|
||||
raise EmailSMTPError(
|
||||
f"Connessione SMTP fallita a {smtp_host}:{smtp_port}: {exc}"
|
||||
) from exc
|
||||
except aiosmtplib.SMTPServerDisconnected as exc:
|
||||
raise EmailSMTPError(
|
||||
f"Server SMTP {smtp_host} ha chiuso la connessione: {exc}"
|
||||
) from exc
|
||||
except aiosmtplib.SMTPException as exc:
|
||||
raise EmailSMTPError(f"Errore SMTP: {exc}") from exc
|
||||
except Exception as exc:
|
||||
raise EmailSMTPError(f"Errore invio email: {exc}") from exc
|
||||
|
||||
|
||||
async def send_test_email(
|
||||
smtp_host: str,
|
||||
smtp_port: int,
|
||||
smtp_user: str,
|
||||
smtp_password: str,
|
||||
from_email: str,
|
||||
to_email: str,
|
||||
channel_name: str = "PEChub",
|
||||
from_name: str = "PEChub Notifiche",
|
||||
use_tls: bool = True,
|
||||
use_starttls: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Invia un'email di test per verificare la configurazione del canale.
|
||||
|
||||
Raises:
|
||||
EmailSMTPError: se la connessione o l'autenticazione falliscono
|
||||
"""
|
||||
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
subject = f"[PEChub] Test canale email: {channel_name}"
|
||||
body_text = (
|
||||
f"PEChub – Test canale Email\n\n"
|
||||
f"Il canale '{channel_name}' e' configurato correttamente.\n\n"
|
||||
f"Data/ora: {ts}\n"
|
||||
f"Destinatario: {to_email}"
|
||||
)
|
||||
body_html = (
|
||||
f"<h2>PEChub – Test canale Email</h2>"
|
||||
f"<p>Il canale <strong>{channel_name}</strong> e' configurato correttamente.</p>"
|
||||
f"<p>Data/ora: <em>{ts}</em><br>"
|
||||
f"Destinatario: {to_email}</p>"
|
||||
f"<hr><p style='font-size:11px;color:#888'>Inviato da PEChub Notification Engine</p>"
|
||||
)
|
||||
await send_email_notification(
|
||||
smtp_host=smtp_host,
|
||||
smtp_port=smtp_port,
|
||||
smtp_user=smtp_user,
|
||||
smtp_password=smtp_password,
|
||||
from_email=from_email,
|
||||
to_email=to_email,
|
||||
subject=subject,
|
||||
body_text=body_text,
|
||||
body_html=body_html,
|
||||
from_name=from_name,
|
||||
use_tls=use_tls,
|
||||
use_starttls=use_starttls,
|
||||
)
|
||||
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Webhook sender – POST HTTP con firma HMAC-SHA256.
|
||||
|
||||
Config non sensibile (config):
|
||||
{ "url": "https://...", "content_type": "application/json" }
|
||||
|
||||
Config sensibile (config_enc → config_secret):
|
||||
{ "webhook_secret": "..." } # opzionale – usato per firma HMAC
|
||||
|
||||
Header inviati:
|
||||
Content-Type: application/json
|
||||
X-PEChub-Event: {event_type}
|
||||
X-Hub-Signature-256: sha256={hex} (solo se webhook_secret configurato)
|
||||
X-Delivery: {uuid}
|
||||
User-Agent: PEChub-Webhook/1.0
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import uuid as uuid_mod
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
|
||||
DEFAULT_TIMEOUT = 10.0
|
||||
|
||||
|
||||
class WebhookError(Exception):
|
||||
"""Errore durante l'invio di una notifica webhook."""
|
||||
|
||||
def __init__(self, message: str, http_status: int | None = None):
|
||||
super().__init__(message)
|
||||
self.http_status = http_status
|
||||
|
||||
|
||||
async def send_webhook(
|
||||
url: str,
|
||||
payload: dict,
|
||||
event_type: str = "new_message",
|
||||
webhook_secret: str | None = None,
|
||||
timeout: float = DEFAULT_TIMEOUT,
|
||||
) -> dict:
|
||||
"""
|
||||
Invia un payload JSON a un webhook URL.
|
||||
|
||||
Args:
|
||||
url: URL destinatario del webhook
|
||||
payload: dict serializzato come JSON nel body
|
||||
event_type: valore dell'header X-PEChub-Event
|
||||
webhook_secret: segreto per firma HMAC-SHA256 (opzionale)
|
||||
timeout: timeout HTTP in secondi
|
||||
|
||||
Returns:
|
||||
dict con http_status, response_text, delivery_id
|
||||
|
||||
Raises:
|
||||
WebhookError: in caso di timeout, errore di rete o HTTP >= 400
|
||||
"""
|
||||
body = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8")
|
||||
delivery_id = str(uuid_mod.uuid4())
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-PEChub-Event": event_type,
|
||||
"X-Delivery": delivery_id,
|
||||
"User-Agent": "PEChub-Webhook/1.0",
|
||||
}
|
||||
|
||||
if webhook_secret:
|
||||
sig = hmac.new(
|
||||
webhook_secret.encode("utf-8"),
|
||||
body,
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
headers["X-Hub-Signature-256"] = f"sha256={sig}"
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
try:
|
||||
response = await client.post(url, content=body, headers=headers)
|
||||
except httpx.TimeoutException as exc:
|
||||
raise WebhookError(
|
||||
f"Timeout webhook dopo {timeout}s"
|
||||
) from exc
|
||||
except httpx.RequestError as exc:
|
||||
raise WebhookError(f"Errore di rete webhook: {exc}") from exc
|
||||
|
||||
if response.status_code >= 400:
|
||||
raise WebhookError(
|
||||
f"Webhook ha risposto con HTTP {response.status_code}: "
|
||||
f"{response.text[:200]}",
|
||||
http_status=response.status_code,
|
||||
)
|
||||
|
||||
return {
|
||||
"http_status": response.status_code,
|
||||
"response_text": response.text[:500],
|
||||
"delivery_id": delivery_id,
|
||||
}
|
||||
|
||||
|
||||
async def send_test_webhook(
|
||||
url: str,
|
||||
webhook_secret: str | None = None,
|
||||
channel_name: str = "PEChub",
|
||||
) -> dict:
|
||||
"""Invia un payload di test al webhook per verificare la configurazione."""
|
||||
payload = {
|
||||
"event": "test",
|
||||
"channel": channel_name,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"message": "Notifica di test da PEChub",
|
||||
"data": {
|
||||
"subject": "[TEST] PEC di prova",
|
||||
"from_address": "test@pec.example.it",
|
||||
"pec_type": "posta_certificata",
|
||||
"direction": "inbound",
|
||||
},
|
||||
}
|
||||
return await send_webhook(
|
||||
url=url,
|
||||
payload=payload,
|
||||
event_type="test",
|
||||
webhook_secret=webhook_secret,
|
||||
)
|
||||
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
WhatsApp sender – Meta Cloud API v18.
|
||||
|
||||
Config non sensibile (config):
|
||||
{
|
||||
"phone_number_id": "123456789", # ID numero mittente Meta Business
|
||||
"to_phone": "+393331234567" # numero destinatario con prefisso
|
||||
}
|
||||
|
||||
Config sensibile (config_enc → config_secret):
|
||||
{ "access_token": "EAABs..." } # Meta Graph API token
|
||||
|
||||
API endpoint:
|
||||
POST https://graph.facebook.com/v18.0/{phone_number_id}/messages
|
||||
|
||||
Nota: richiede un account Meta Business verificato con WhatsApp Business API.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
|
||||
META_GRAPH_API_URL = "https://graph.facebook.com/v18.0"
|
||||
DEFAULT_TIMEOUT = 10.0
|
||||
|
||||
|
||||
class WhatsAppError(Exception):
|
||||
"""Errore durante l'invio di un messaggio WhatsApp."""
|
||||
|
||||
def __init__(self, message: str, http_status: int | None = None, api_code: int | None = None):
|
||||
super().__init__(message)
|
||||
self.http_status = http_status
|
||||
self.api_code = api_code
|
||||
|
||||
|
||||
async def send_whatsapp_message(
|
||||
phone_number_id: str,
|
||||
to_phone: str,
|
||||
text: str,
|
||||
access_token: str,
|
||||
timeout: float = DEFAULT_TIMEOUT,
|
||||
) -> dict:
|
||||
"""
|
||||
Invia un messaggio di testo WhatsApp via Meta Cloud API.
|
||||
|
||||
Args:
|
||||
phone_number_id: ID del numero WhatsApp Business mittente
|
||||
to_phone: numero destinatario (formato E.164, es. +393331234567)
|
||||
text: testo del messaggio
|
||||
access_token: Meta Graph API Bearer token
|
||||
timeout: timeout HTTP in secondi
|
||||
|
||||
Returns:
|
||||
dict con message_id dalla risposta API
|
||||
|
||||
Raises:
|
||||
WhatsAppError: in caso di errore HTTP o risposta API non-ok
|
||||
"""
|
||||
url = f"{META_GRAPH_API_URL}/{phone_number_id}/messages"
|
||||
payload = {
|
||||
"messaging_product": "whatsapp",
|
||||
"to": to_phone.replace(" ", "").replace("-", ""),
|
||||
"type": "text",
|
||||
"text": {"body": text},
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
try:
|
||||
response = await client.post(url, json=payload, headers=headers)
|
||||
except httpx.TimeoutException as exc:
|
||||
raise WhatsAppError(
|
||||
f"Timeout WhatsApp API dopo {timeout}s"
|
||||
) from exc
|
||||
except httpx.RequestError as exc:
|
||||
raise WhatsAppError(f"Errore di rete WhatsApp: {exc}") from exc
|
||||
|
||||
if response.status_code == 401:
|
||||
raise WhatsAppError(
|
||||
"Token Meta non valido o scaduto",
|
||||
http_status=401,
|
||||
)
|
||||
|
||||
if response.status_code >= 400:
|
||||
try:
|
||||
err_data = response.json()
|
||||
err_msg = err_data.get("error", {}).get("message", response.text[:200])
|
||||
err_code = err_data.get("error", {}).get("code")
|
||||
except Exception:
|
||||
err_msg = response.text[:200]
|
||||
err_code = None
|
||||
raise WhatsAppError(
|
||||
f"Meta API errore HTTP {response.status_code}: {err_msg}",
|
||||
http_status=response.status_code,
|
||||
api_code=err_code,
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
messages = data.get("messages", [])
|
||||
message_id = messages[0].get("id") if messages else None
|
||||
return {"message_id": message_id, "http_status": response.status_code}
|
||||
|
||||
|
||||
async def send_test_whatsapp(
|
||||
phone_number_id: str,
|
||||
to_phone: str,
|
||||
access_token: str,
|
||||
channel_name: str = "PEChub",
|
||||
) -> dict:
|
||||
"""Invia un messaggio WhatsApp di test per verificare la configurazione."""
|
||||
from datetime import datetime
|
||||
|
||||
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
text = (
|
||||
f"*PEChub – Test canale WhatsApp*\n\n"
|
||||
f"Il canale _{channel_name}_ e' configurato correttamente.\n\n"
|
||||
f"Data/ora: {ts}"
|
||||
)
|
||||
return await send_whatsapp_message(
|
||||
phone_number_id=phone_number_id,
|
||||
to_phone=to_phone,
|
||||
text=text,
|
||||
access_token=access_token,
|
||||
)
|
||||
@@ -1,20 +1,24 @@
|
||||
"""
|
||||
Servizio Notifiche Multi-canale – CRUD canali, regole, log.
|
||||
Servizio Notifiche Multi-canale – CRUD canali, regole, log + dispatch.
|
||||
|
||||
Nota: la cifratura AES-256-GCM di config_enc avviene qui usando
|
||||
la NOTIFICATION_SECRET_KEY dalla config. Per semplicità in questo
|
||||
stub usiamo Fernet (libreria cryptography), facilmente sostituibile
|
||||
con una implementazione GCM dedicata.
|
||||
Cifratura: AES-256-GCM via libreria cryptography.
|
||||
Formato config_enc: base64( nonce(12) || ciphertext+tag )
|
||||
Chiave: ENCRYPTION_KEY (hex 64 char = 32 byte) dalla config.
|
||||
|
||||
Backward compatibility: se il valore non decrittografa come GCM, viene
|
||||
tentato il fallback a base64 grezzo (configurazioni precedenti al fix).
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.config import get_settings
|
||||
from app.core.exceptions import NotFoundError
|
||||
@@ -27,20 +31,53 @@ from app.schemas.notification import (
|
||||
NotificationRuleUpdate,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
def _encrypt(data: dict) -> str:
|
||||
"""Cifra un dict JSON → base64. Usa la SECRET_KEY come seed."""
|
||||
# In produzione: usa AES-256-GCM. Qui: semplice base64 con marker.
|
||||
raw = json.dumps(data).encode()
|
||||
return base64.b64encode(raw).decode()
|
||||
# ─── Cifratura AES-256-GCM ────────────────────────────────────────────────────
|
||||
|
||||
def _encrypt(data: dict, key: bytes | None = None) -> str:
|
||||
"""
|
||||
Cifra un dict JSON con AES-256-GCM.
|
||||
|
||||
Formato output: base64( nonce(12 byte) || ciphertext+tag(16 byte) )
|
||||
"""
|
||||
if key is None:
|
||||
key = settings.encryption_key_bytes
|
||||
nonce = os.urandom(12)
|
||||
aesgcm = AESGCM(key)
|
||||
plaintext = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
||||
ciphertext = aesgcm.encrypt(nonce, plaintext, None) # include tag
|
||||
return base64.b64encode(nonce + ciphertext).decode("ascii")
|
||||
|
||||
|
||||
def _decrypt(enc: str) -> dict:
|
||||
"""Decifra il valore restituito da _encrypt."""
|
||||
raw = base64.b64decode(enc.encode())
|
||||
return json.loads(raw.decode())
|
||||
def _decrypt(enc: str, key: bytes | None = None) -> dict:
|
||||
"""
|
||||
Decifra il valore prodotto da _encrypt.
|
||||
|
||||
Backward compatible: se il dato non e' GCM valido, prova il
|
||||
vecchio base64 grezzo (usato prima del fix di sicurezza).
|
||||
"""
|
||||
if key is None:
|
||||
key = settings.encryption_key_bytes
|
||||
try:
|
||||
raw = base64.b64decode(enc.encode("ascii"))
|
||||
if len(raw) > 28: # 12 nonce + 16 tag minimo
|
||||
nonce = raw[:12]
|
||||
ciphertext = raw[12:]
|
||||
aesgcm = AESGCM(key)
|
||||
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
|
||||
return json.loads(plaintext.decode("utf-8"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: base64 grezzo (configurazioni create prima del fix GCM)
|
||||
try:
|
||||
raw = base64.b64decode(enc.encode("ascii"))
|
||||
return json.loads(raw.decode("utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
class NotificationService:
|
||||
@@ -113,6 +150,7 @@ class NotificationService:
|
||||
if data.config is not None:
|
||||
channel.config = data.config
|
||||
if data.config_secret is not None:
|
||||
# Re-cifra sempre con AES-256-GCM (aggiorna anche i vecchi base64)
|
||||
channel.config_enc = _encrypt(data.config_secret)
|
||||
|
||||
await self.db.flush()
|
||||
@@ -128,18 +166,16 @@ class NotificationService:
|
||||
self, channel_id: uuid.UUID, tenant_id: uuid.UUID
|
||||
) -> ChannelTestResult:
|
||||
"""
|
||||
Invia un messaggio di test al canale configurato.
|
||||
Invia un messaggio di test reale al canale configurato.
|
||||
|
||||
Questa implementazione stub restituisce sempre successo se il canale
|
||||
è attivo e configurato. Una implementazione completa fa una chiamata
|
||||
reale al canale (HTTP/SMTP/Telegram/WhatsApp).
|
||||
Esegue invio effettivo per Telegram, Webhook, Email SMTP e WhatsApp.
|
||||
"""
|
||||
channel = await self.get_channel(channel_id, tenant_id)
|
||||
|
||||
if not channel.is_active:
|
||||
return ChannelTestResult(
|
||||
success=False,
|
||||
message="Il canale è disabilitato",
|
||||
message="Il canale e' disabilitato",
|
||||
)
|
||||
|
||||
if channel.circuit_open_until and channel.circuit_open_until > datetime.now(timezone.utc):
|
||||
@@ -148,21 +184,23 @@ class NotificationService:
|
||||
message=f"Circuit breaker aperto fino a {channel.circuit_open_until.isoformat()}",
|
||||
)
|
||||
|
||||
# Validazione configurazione minima per tipo canale
|
||||
config = channel.config or {}
|
||||
secret = {}
|
||||
if channel.config_enc:
|
||||
try:
|
||||
secret = _decrypt(channel.config_enc)
|
||||
except Exception as e:
|
||||
return ChannelTestResult(
|
||||
success=False,
|
||||
message=f"Errore decifratura configurazione sensibile: {e}",
|
||||
)
|
||||
|
||||
channel_type = channel.channel_type
|
||||
|
||||
if channel_type == "webhook":
|
||||
if not config.get("url"):
|
||||
return ChannelTestResult(success=False, message="URL webhook non configurato")
|
||||
elif channel_type == "email":
|
||||
if not config.get("to_email"):
|
||||
return ChannelTestResult(success=False, message="Email destinatario non configurata")
|
||||
elif channel_type == "telegram":
|
||||
# ── Telegram ──────────────────────────────────────────────────────────
|
||||
if channel_type == "telegram":
|
||||
if not config.get("chat_id"):
|
||||
return ChannelTestResult(success=False, message="Chat ID Telegram non configurato")
|
||||
# Invio reale via Bot API
|
||||
secret = _decrypt(channel.config_enc) if channel.config_enc else {}
|
||||
bot_token = secret.get("bot_token")
|
||||
if not bot_token:
|
||||
return ChannelTestResult(success=False, message="Bot token Telegram non configurato")
|
||||
@@ -176,28 +214,128 @@ class NotificationService:
|
||||
msg_id = result.get("message_id")
|
||||
return ChannelTestResult(
|
||||
success=True,
|
||||
message=f"Messaggio Telegram inviato con successo (message_id={msg_id}).",
|
||||
message=f"Messaggio Telegram inviato (message_id={msg_id}).",
|
||||
http_status=200,
|
||||
)
|
||||
except TelegramError as exc:
|
||||
return ChannelTestResult(
|
||||
success=False,
|
||||
message=f"Errore Telegram: {exc}",
|
||||
http_status=exc.http_status,
|
||||
)
|
||||
except Exception as exc:
|
||||
return ChannelTestResult(
|
||||
success=False,
|
||||
message=f"Errore imprevisto durante il test Telegram: {exc}",
|
||||
message=f"Errore Telegram: {exc}",
|
||||
)
|
||||
elif channel_type == "whatsapp":
|
||||
if not config.get("phone_number"):
|
||||
return ChannelTestResult(success=False, message="Numero WhatsApp non configurato")
|
||||
|
||||
# ── Webhook ───────────────────────────────────────────────────────────
|
||||
elif channel_type == "webhook":
|
||||
url = config.get("url")
|
||||
if not url:
|
||||
return ChannelTestResult(success=False, message="URL webhook non configurato")
|
||||
webhook_secret = secret.get("webhook_secret")
|
||||
try:
|
||||
from app.notifications.webhook import WebhookError, send_test_webhook
|
||||
result = await send_test_webhook(
|
||||
url=url,
|
||||
webhook_secret=webhook_secret,
|
||||
channel_name=channel.name,
|
||||
)
|
||||
return ChannelTestResult(
|
||||
success=True,
|
||||
message=(
|
||||
f"Webhook raggiunto con successo "
|
||||
f"(HTTP {result['http_status']}, delivery={result['delivery_id']})."
|
||||
),
|
||||
http_status=result["http_status"],
|
||||
)
|
||||
except Exception as exc:
|
||||
http_status = getattr(exc, "http_status", None)
|
||||
return ChannelTestResult(
|
||||
success=False,
|
||||
message=f"Errore webhook: {exc}",
|
||||
http_status=http_status,
|
||||
)
|
||||
|
||||
# ── Email SMTP ────────────────────────────────────────────────────────
|
||||
elif channel_type == "email":
|
||||
smtp_host = config.get("smtp_host")
|
||||
smtp_port = config.get("smtp_port", 465)
|
||||
from_email = config.get("from_email")
|
||||
to_email = config.get("to_email")
|
||||
smtp_user = config.get("smtp_user") or from_email
|
||||
use_tls = config.get("smtp_use_tls", True)
|
||||
use_starttls = config.get("smtp_use_starttls", False)
|
||||
from_name = config.get("from_name", "PEChub Notifiche")
|
||||
smtp_password = secret.get("smtp_password", "")
|
||||
|
||||
if not smtp_host:
|
||||
return ChannelTestResult(success=False, message="Host SMTP non configurato")
|
||||
if not from_email:
|
||||
return ChannelTestResult(success=False, message="Email mittente non configurata")
|
||||
if not to_email:
|
||||
return ChannelTestResult(success=False, message="Email destinatario non configurata")
|
||||
if not smtp_password:
|
||||
return ChannelTestResult(success=False, message="Password SMTP non configurata")
|
||||
|
||||
try:
|
||||
from app.notifications.email_smtp import EmailSMTPError, send_test_email
|
||||
await send_test_email(
|
||||
smtp_host=smtp_host,
|
||||
smtp_port=int(smtp_port),
|
||||
smtp_user=smtp_user,
|
||||
smtp_password=smtp_password,
|
||||
from_email=from_email,
|
||||
to_email=to_email,
|
||||
channel_name=channel.name,
|
||||
from_name=from_name,
|
||||
use_tls=use_tls,
|
||||
use_starttls=use_starttls,
|
||||
)
|
||||
return ChannelTestResult(
|
||||
success=True,
|
||||
message=f"Email di test inviata con successo a {to_email}.",
|
||||
http_status=200,
|
||||
)
|
||||
except Exception as exc:
|
||||
return ChannelTestResult(
|
||||
success=False,
|
||||
message=f"Errore email: {exc}",
|
||||
)
|
||||
|
||||
# ── WhatsApp ──────────────────────────────────────────────────────────
|
||||
elif channel_type == "whatsapp":
|
||||
phone_number_id = config.get("phone_number_id")
|
||||
to_phone = config.get("to_phone")
|
||||
access_token = secret.get("access_token")
|
||||
|
||||
if not phone_number_id:
|
||||
return ChannelTestResult(success=False, message="phone_number_id non configurato")
|
||||
if not to_phone:
|
||||
return ChannelTestResult(success=False, message="Numero WhatsApp destinatario non configurato")
|
||||
if not access_token:
|
||||
return ChannelTestResult(success=False, message="Access token Meta non configurato")
|
||||
|
||||
try:
|
||||
from app.notifications.whatsapp import WhatsAppError, send_test_whatsapp
|
||||
result = await send_test_whatsapp(
|
||||
phone_number_id=phone_number_id,
|
||||
to_phone=to_phone,
|
||||
access_token=access_token,
|
||||
channel_name=channel.name,
|
||||
)
|
||||
return ChannelTestResult(
|
||||
success=True,
|
||||
message=f"Messaggio WhatsApp inviato (message_id={result.get('message_id')}).",
|
||||
http_status=200,
|
||||
)
|
||||
except Exception as exc:
|
||||
http_status = getattr(exc, "http_status", None)
|
||||
return ChannelTestResult(
|
||||
success=False,
|
||||
message=f"Errore WhatsApp: {exc}",
|
||||
http_status=http_status,
|
||||
)
|
||||
|
||||
# ── Tipo sconosciuto ──────────────────────────────────────────────────
|
||||
return ChannelTestResult(
|
||||
success=True,
|
||||
message=f"Canale {channel_type} configurato correttamente. Test simulato con successo.",
|
||||
http_status=200,
|
||||
success=False,
|
||||
message=f"Tipo canale '{channel_type}' non supportato",
|
||||
)
|
||||
|
||||
# ─── Rules ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user