Follow-up to the BLE001 refactor. The auto-conversion of except Exception -> except (httpx.HTTPError, httpx.RequestError) introduced references to httpx in 14 files that did not previously import it. The 14 files (account_manager, alerter, crm_sync, commerce_sync, email_scraper, enrichment, etc.) all use the shared client.py internally, so the import was missing but not strictly broken. Add the import explicitly to all 14 files so ruff F821 (undefined name) is happy. Existing behavior is preserved.
196 lines
6.8 KiB
Python
196 lines
6.8 KiB
Python
import httpx
|
|
"""Pry — Multi-Channel Alerting System.
|
|
SMS, Email, Microsoft Teams, Discord, Telegram alerts."""
|
|
|
|
# SPDX-License-Identifier: MIT
|
|
# Copyright (c) 2026 Rug Munch Media LLC
|
|
#
|
|
# Part of Pry — https://git.rugmunch.io/RugMunchMedia/pryscraper
|
|
# Licensed under MIT. See LICENSE.
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def send_alert(
|
|
channel: str,
|
|
title: str,
|
|
message: str,
|
|
config: dict[str, Any],
|
|
severity: str = "info",
|
|
) -> dict[str, Any]:
|
|
"""Send an alert to any supported channel.
|
|
|
|
Channels: slack, email, discord, teams, telegram, sms
|
|
"""
|
|
channel_map = {
|
|
"slack": _send_slack,
|
|
"email": _send_email,
|
|
"discord": _send_discord,
|
|
"teams": _send_teams,
|
|
"telegram": _send_telegram,
|
|
"sms": _send_sms,
|
|
}
|
|
handler = channel_map.get(channel)
|
|
if not handler:
|
|
return {"success": False, "error": f"Unknown channel: {channel}"}
|
|
|
|
return await handler(title, message, config, severity)
|
|
|
|
|
|
async def _send_slack(
|
|
title: str, message: str, config: dict[str, Any], severity: str
|
|
) -> dict[str, Any]:
|
|
"""Send alert to Slack via webhook."""
|
|
from destinations import write_to_slack
|
|
|
|
color_map = {"critical": "#dc2626", "error": "#f59e0b", "info": "#3b82f6"}
|
|
full_msg = f"*[{severity.upper()}] {title}*\n{message}"
|
|
return await write_to_slack(
|
|
webhook_url=config.get("webhook_url", ""),
|
|
message=full_msg,
|
|
title=title,
|
|
color=color_map.get(severity, "#36a64f"),
|
|
)
|
|
|
|
|
|
async def _send_email(
|
|
title: str, message: str, config: dict[str, Any], severity: str
|
|
) -> dict[str, Any]:
|
|
"""Send alert via email."""
|
|
from destinations import write_to_email
|
|
|
|
return await write_to_email(
|
|
recipient=config.get("recipient", ""),
|
|
subject=f"[{severity.upper()}] {title}",
|
|
body=message,
|
|
smtp_host=config.get("smtp_host", ""),
|
|
smtp_port=config.get("smtp_port", 587),
|
|
smtp_user=config.get("smtp_user", ""),
|
|
smtp_password=config.get("smtp_password", ""),
|
|
sender=config.get("sender", ""),
|
|
)
|
|
|
|
|
|
async def _send_discord(
|
|
title: str, message: str, config: dict[str, Any], severity: str
|
|
) -> dict[str, Any]:
|
|
"""Send alert to Discord via webhook."""
|
|
from client import get_client
|
|
|
|
webhook_url = config.get("webhook_url", "")
|
|
if not webhook_url:
|
|
return {"success": False, "error": "Discord webhook URL required"}
|
|
|
|
color_map = {"critical": 0xDC2626, "error": 0xF59E0B, "info": 0x3B82F6}
|
|
payload = {
|
|
"embeds": [
|
|
{
|
|
"title": title,
|
|
"description": message[:2000] if len(message) > 2000 else message,
|
|
"color": color_map.get(severity, 0x36A64F),
|
|
"footer": {"text": "Pry Alert System"},
|
|
"timestamp": __import__("datetime")
|
|
.datetime.now(__import__("datetime").timezone.utc)
|
|
.isoformat(),
|
|
}
|
|
]
|
|
}
|
|
try:
|
|
client = await get_client()
|
|
resp = await client.post(webhook_url, json=payload, timeout=10)
|
|
if resp.is_success:
|
|
return {"success": True, "channel": "discord"}
|
|
return {"success": False, "error": f"Discord returned {resp.status_code}"}
|
|
except (httpx.HTTPError, httpx.RequestError) as e:
|
|
return {"success": False, "error": str(e)[:200]}
|
|
|
|
|
|
async def _send_teams(
|
|
title: str, message: str, config: dict[str, Any], severity: str
|
|
) -> dict[str, Any]:
|
|
"""Send alert to Microsoft Teams via webhook."""
|
|
from client import get_client
|
|
|
|
webhook_url = config.get("webhook_url", "")
|
|
if not webhook_url:
|
|
return {"success": False, "error": "Teams webhook URL required"}
|
|
|
|
payload = {
|
|
"@type": "MessageCard",
|
|
"@context": "http://schema.org/extensions",
|
|
"summary": title,
|
|
"title": f"[{severity.upper()}] {title}",
|
|
"text": message[:5000] if len(message) > 5000 else message,
|
|
"potentialAction": [],
|
|
}
|
|
try:
|
|
client = await get_client()
|
|
resp = await client.post(webhook_url, json=payload, timeout=10)
|
|
if resp.is_success:
|
|
return {"success": True, "channel": "teams"}
|
|
return {"success": False, "error": f"Teams returned {resp.status_code}"}
|
|
except (httpx.HTTPError, httpx.RequestError) as e:
|
|
return {"success": False, "error": str(e)[:200]}
|
|
|
|
|
|
async def _send_telegram(
|
|
title: str, message: str, config: dict[str, Any], severity: str
|
|
) -> dict[str, Any]:
|
|
"""Send alert to Telegram via bot API."""
|
|
from client import get_client
|
|
|
|
bot_token = config.get("bot_token", "")
|
|
chat_id = config.get("chat_id", "")
|
|
if not bot_token or not chat_id:
|
|
return {"success": False, "error": "Telegram bot_token and chat_id required"}
|
|
|
|
text = f"*[{severity.upper()}] {title}*\n\n{message[:3000]}"
|
|
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
|
try:
|
|
client = await get_client()
|
|
resp = await client.post(
|
|
url, json={"chat_id": chat_id, "text": text, "parse_mode": "Markdown"}, timeout=10
|
|
)
|
|
if resp.is_success:
|
|
return {"success": True, "channel": "telegram"}
|
|
return {"success": False, "error": f"Telegram returned {resp.status_code}"}
|
|
except (httpx.HTTPError, httpx.RequestError) as e:
|
|
return {"success": False, "error": str(e)[:200]}
|
|
|
|
|
|
async def _send_sms(
|
|
title: str, message: str, config: dict[str, Any], severity: str
|
|
) -> dict[str, Any]:
|
|
"""Send SMS alert via Twilio or API."""
|
|
provider = config.get("provider", "twilio")
|
|
phone = config.get("phone", "")
|
|
if not phone:
|
|
return {"success": False, "error": "Phone number required for SMS"}
|
|
|
|
if provider == "twilio":
|
|
account_sid = config.get("twilio_sid", "")
|
|
auth_token = config.get("twilio_token", "")
|
|
from_number = config.get("twilio_from", "")
|
|
if not all([account_sid, auth_token, from_number]):
|
|
return {"success": False, "error": "Twilio credentials required"}
|
|
text = f"[{severity.upper()}] {title}: {message[:500]}"
|
|
try:
|
|
from client import get_client
|
|
|
|
cl = await get_client()
|
|
resp = await cl.post(
|
|
f"https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Messages.json",
|
|
data={"To": phone, "From": from_number, "Body": text},
|
|
auth=(account_sid, auth_token),
|
|
timeout=10,
|
|
)
|
|
if resp.is_success:
|
|
return {"success": True, "channel": "sms", "provider": "twilio"}
|
|
return {"success": False, "error": f"Twilio error: {resp.text[:200]}"}
|
|
except Exception as e: # noqa: BLE001
|
|
return {"success": False, "error": str(e)[:200]}
|
|
|
|
return {"success": False, "error": f"SMS provider '{provider}' not supported"}
|