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 @@
|
||||
# Senders notifiche multi-canale per il worker
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Email SMTP sender (worker) – invio notifiche via aiosmtplib.
|
||||
|
||||
Copia del sender backend: i due container sono separati.
|
||||
"""
|
||||
|
||||
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):
|
||||
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.
|
||||
|
||||
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: {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.SMTPException as exc:
|
||||
raise EmailSMTPError(f"Errore SMTP: {exc}") from exc
|
||||
except Exception as exc:
|
||||
raise EmailSMTPError(f"Errore invio email: {exc}") from exc
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Telegram Bot API – invio messaggi via sendMessage (worker).
|
||||
|
||||
Copia del sender backend: i due container sono separati e non
|
||||
possono condividere package, quindi il codice e' duplicato.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
|
||||
TELEGRAM_API_BASE = "https://api.telegram.org"
|
||||
DEFAULT_TIMEOUT = 10.0
|
||||
|
||||
|
||||
class TelegramError(Exception):
|
||||
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_message(
|
||||
bot_token: str,
|
||||
chat_id: str,
|
||||
text: str,
|
||||
parse_mode: str = "HTML",
|
||||
disable_web_page_preview: bool = True,
|
||||
timeout: float = DEFAULT_TIMEOUT,
|
||||
) -> dict:
|
||||
"""
|
||||
Invia un messaggio a un canale/gruppo/utente Telegram.
|
||||
|
||||
Returns:
|
||||
dict con il risultato della API Telegram (result.message_id, ecc.)
|
||||
|
||||
Raises:
|
||||
TelegramError: in caso di errore HTTP o risposta API non-ok
|
||||
"""
|
||||
url = f"{TELEGRAM_API_BASE}/bot{bot_token}/sendMessage"
|
||||
payload: dict = {"chat_id": chat_id, "text": text}
|
||||
if parse_mode:
|
||||
payload["parse_mode"] = parse_mode
|
||||
if disable_web_page_preview:
|
||||
payload["link_preview_options"] = {"is_disabled": True}
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
try:
|
||||
response = await client.post(url, json=payload)
|
||||
except httpx.TimeoutException as exc:
|
||||
raise TelegramError(f"Timeout Telegram ({timeout}s)") from exc
|
||||
except httpx.RequestError as exc:
|
||||
raise TelegramError(f"Errore di rete Telegram: {exc}") from exc
|
||||
|
||||
if response.status_code != 200:
|
||||
raise TelegramError(
|
||||
f"Telegram API HTTP {response.status_code}: {response.text[:200]}",
|
||||
http_status=response.status_code,
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
if not data.get("ok"):
|
||||
api_code = data.get("error_code")
|
||||
description = data.get("description", "Errore sconosciuto")
|
||||
raise TelegramError(
|
||||
f"Telegram API error {api_code}: {description}",
|
||||
http_status=response.status_code,
|
||||
api_code=api_code,
|
||||
)
|
||||
|
||||
return data.get("result", {})
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Webhook sender (worker) – POST HTTP con firma HMAC-SHA256.
|
||||
|
||||
Copia del sender backend: i due container sono separati.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import uuid as uuid_mod
|
||||
|
||||
import httpx
|
||||
|
||||
DEFAULT_TIMEOUT = 10.0
|
||||
|
||||
|
||||
class WebhookError(Exception):
|
||||
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.
|
||||
|
||||
Returns:
|
||||
dict con http_status, 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 HTTP {response.status_code}: {response.text[:200]}",
|
||||
http_status=response.status_code,
|
||||
)
|
||||
|
||||
return {
|
||||
"http_status": response.status_code,
|
||||
"delivery_id": delivery_id,
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
WhatsApp sender (worker) – Meta Cloud API v18.
|
||||
|
||||
Copia del sender backend: i due container sono separati.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
|
||||
META_GRAPH_API_URL = "https://graph.facebook.com/v18.0"
|
||||
DEFAULT_TIMEOUT = 10.0
|
||||
|
||||
|
||||
class WhatsAppError(Exception):
|
||||
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.
|
||||
|
||||
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}
|
||||
Reference in New Issue
Block a user