indie-status-page/app/services/notifier.py
IndieStatusBot 902133edd3 feat: indie status page MVP -- FastAPI + SQLite
- 8 DB models (services, incidents, monitors, subscribers, etc.)
- Full CRUD API for services, incidents, monitors
- Public status page with live data
- Incident detail page with timeline
- API key authentication
- Uptime monitoring scheduler
- 13 tests passing
- TECHNICAL_DESIGN.md with full spec
2026-04-25 05:00:00 +00:00

110 lines
No EOL
3.1 KiB
Python

"""Notification service — email (SMTP) and webhook dispatch."""
import json
import smtplib
from email.mime.text import MIMEText
from datetime import datetime
import httpx
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.models.models import Incident, NotificationLog, Subscriber
async def send_email_notification(
to_email: str,
subject: str,
body: str,
) -> bool:
"""Send an email notification via SMTP. Returns True if successful."""
if not settings.smtp_host:
return False
msg = MIMEText(body, "html")
msg["Subject"] = subject
msg["From"] = settings.smtp_from
msg["To"] = to_email
try:
with smtplib.SMTP(settings.smtp_host, settings.smtp_port) as server:
if settings.smtp_user:
server.starttls()
server.login(settings.smtp_user, settings.smtp_pass)
server.send_message(msg)
return True
except Exception:
return False
async def send_webhook_notification(
payload: dict,
) -> bool:
"""Send a webhook POST notification. Returns True if successful."""
if not settings.webhook_notify_url:
return False
try:
async with httpx.AsyncClient() as client:
response = await client.post(
settings.webhook_notify_url,
json=payload,
timeout=10.0,
)
return response.status_code < 400
except Exception:
return False
async def notify_subscribers(
incident: Incident,
db: AsyncSession,
) -> int:
"""Notify all confirmed subscribers about an incident update. Returns count notified."""
result = await db.execute(
select(Subscriber).where(Subscriber.is_confirmed == True) # noqa: E712
)
subscribers = result.scalars().all()
notified = 0
subject = f"[{incident.severity.upper()}] {incident.title}"
for subscriber in subscribers:
# Email notification
email_sent = await send_email_notification(
to_email=subscriber.email,
subject=subject,
body=f"<p>{incident.title}</p><p>Status: {incident.status}</p>",
)
if email_sent:
log = NotificationLog(
incident_id=incident.id,
subscriber_id=subscriber.id,
channel="email",
status="sent",
)
db.add(log)
notified += 1
# Webhook notification
webhook_sent = await send_webhook_notification(
payload={
"incident_id": incident.id,
"title": incident.title,
"status": incident.status,
"severity": incident.severity,
"started_at": incident.started_at.isoformat() if incident.started_at else None,
}
)
if webhook_sent:
log = NotificationLog(
incident_id=incident.id,
subscriber_id=subscriber.id,
channel="webhook",
status="sent",
)
db.add(log)
await db.flush()
return notified