📄 notifications.py 5,012 bytes Apr 22, 2026 📋 Raw

"""Telegram notification module for Content Brief v2 approval flow.

Sends DMs to Matt when a brief is submitted for approval.
Uses the same Telegram bot token as the family assistant (Hermes).
"""

import os
import json
from typing import Optional

import requests

TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
MATT_CHAT_ID = os.getenv("TELEGRAM_MATT_CHAT_ID", "8386527252")
API_BASE = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}"

def _send_message(chat_id: str, text: str, reply_markup: Optional[dict] = None) -> bool:
"""Send a Telegram message. Returns True on success."""
if not TELEGRAM_BOT_TOKEN:
print("[telegram] No bot token configured, skipping notification")
return False

payload = {
    "chat_id": chat_id,
    "text": text,
    "parse_mode": "HTML",
}
if reply_markup:
    payload["reply_markup"] = json.dumps(reply_markup)

try:
    resp = requests.post(f"{API_BASE}/sendMessage", json=payload, timeout=10)
    resp.raise_for_status()
    result = resp.json()
    if result.get("ok"):
        print(f"[telegram] Sent notification to {chat_id}")
        return True
    else:
        print(f"[telegram] Failed: {result}")
        return False
except Exception as e:
    print(f"[telegram] Error sending message: {e}")
    return False

def notify_brief_submitted(brief_id: str, title: str, created_by: str, base_url: str = "https://notes.hoffdesk.com") -> bool:
"""Notify Matt that a new brief is pending approval.

Sends an inline keyboard with Approve / Reject / Edit buttons.
"""
text = (
    f"📝 <b>New Content Brief Pending Approval</b>\n\n"
    f"<b>Title:</b> {title}\n"
    f"<b>Author:</b> {created_by}\n"
    f"<b>ID:</b> <code>{brief_id}</code>\n\n"
    f"Review and approve to trigger content generation."
)

reply_markup = {
    "inline_keyboard": [
        [
            {"text": "✅ Approve", "callback_data": f"brief_approve:{brief_id}"},
            {"text": "❌ Reject", "callback_data": f"brief_reject:{brief_id}"},
        ],
        [
            {"text": "✏️ Edit in Admin", "url": f"{base_url}/admin/content/briefs/{brief_id}"},
        ],
    ]
}

return _send_message(MATT_CHAT_ID, text, reply_markup)

def notify_brief_approved(brief_id: str, title: str, generation_job_id: str) -> bool:
"""Notify that a brief has been approved and generation started."""
text = (
f"✅ Brief Approved — Generation Started\n\n"
f"Title: {title}\n"
f"Job ID: {generation_job_id}\n\n"
f"Content is being generated on the local GPU. You'll be notified when it's ready."
)
return _send_message(MATT_CHAT_ID, text)

def notify_generation_complete(brief_id: str, title: str, struggle_score: float, output_url: str) -> bool:
"""Notify that content generation is complete."""
score_emoji = "🟢" if struggle_score >= 75 else "🟡" if struggle_score >= 50 else "🔴"
text = (
f"✨ Content Ready for Review\n\n"
f"Title: {title}\n"
f"Struggle Score: {score_emoji} {struggle_score:.1f}/100\n\n"
f"Preview Content"
)

reply_markup = {
    "inline_keyboard": [
        [
            {"text": "📄 Preview", "url": output_url},
            {"text": "🚀 Publish", "callback_data": f"brief_publish:{brief_id}"},
        ],
        [
            {"text": "🔄 Regenerate", "callback_data": f"brief_regenerate:{brief_id}"},
        ],
    ]
}

return _send_message(MATT_CHAT_ID, text, reply_markup)

def notify_brief_rejected(brief_id: str, title: str, feedback: str) -> bool:
"""Notify that a brief was rejected."""
text = (
f"❌ Brief Rejected\n\n"
f"Title: {title}\n"
f"Feedback: {feedback or 'No feedback provided'}\n\n"
f"The brief has been returned to draft status. Edit and resubmit when ready."
)
return _send_message(MATT_CHAT_ID, text)

─── Hook into brief lifecycle ────────────────────────────────────────────

def on_brief_submitted(brief_id: str, title: str, created_by: str) -> bool:
"""Called when a brief is submitted for approval."""
return notify_brief_submitted(brief_id, title, created_by)

def on_brief_approved(brief_id: str, title: str, generation_job_id: str) -> bool:
"""Called when a brief is approved."""
return notify_brief_approved(brief_id, title, generation_job_id)

def on_generation_complete(brief_id: str, title: str, struggle_score: float, output_url: str) -> bool:
"""Called when generation completes."""
return notify_generation_complete(brief_id, title, struggle_score, output_url)