"""Opt-in daily digest — a finite, calm morning email of today's brief. Ritual, not capture: no streaks, no "you missed", no urgency, no unread counts. One send per opted-in user per day, gated to a morning window in the site timezone and deduped via digest_sends. On a thin day it skips quietly rather than padding. Reuses the existing SMTP/email pipeline. """ from __future__ import annotations import os import secrets import sqlite3 from datetime import datetime from html import escape from . import email_send from .localtime import local_now, local_today from .paywall import is_paywalled DIGEST_HOUR = int(os.environ.get("GOODNEWS_DIGEST_HOUR", "7")) DIGEST_WINDOW_HOURS = 4 # send between DIGEST_HOUR and +4h, site-local MIN_ITEMS = 4 # below this, skip the day rather than pad def _base_url() -> str: return os.environ.get("GOODNEWS_PUBLIC_BASE_URL", "https://upbeatbytes.com").rstrip("/") def digest_items(conn: sqlite3.Connection, brief_date: str, limit: int = 7) -> list[dict]: """The brief's items with the bits a calm email needs (visible sources only).""" rows = conn.execute( """ SELECT a.id, a.title, a.canonical_url, s.name AS source, sc.reason_text, (SELECT summary FROM article_summaries WHERE article_id = a.id) AS summary FROM daily_briefs b JOIN daily_brief_items bi ON bi.brief_id = b.id JOIN articles a ON a.id = bi.article_id JOIN sources s ON s.id = a.source_id LEFT JOIN article_scores sc ON sc.article_id = a.id WHERE b.brief_date = ? AND s.content_visible = 1 ORDER BY bi.rank LIMIT ? """, (brief_date, limit), ).fetchall() items = [] for r in rows: d = dict(r) d["paywalled"] = is_paywalled(d["canonical_url"]) items.append(d) return items def _weekday(brief_date: str) -> str: try: return datetime.strptime(brief_date, "%Y-%m-%d").strftime("%A") except (ValueError, TypeError): return "today" def build_digest(items: list[dict], brief_date: str, unsub_url: str, base: str | None = None) -> tuple[str, str, str]: """Return (subject, text, html) for the digest — calm and dated, no urgency.""" base = base or _base_url() n = len(items) weekday = _weekday(brief_date) subject = f"{weekday}'s Upbeat Bytes · {n} calm read{'' if n == 1 else 's'}" if weekday == "today": subject = f"Today's Upbeat Bytes · {n} calm reads" text_lines = [f"Today's good news — {n} calm reads.\n"] for it in items: text_lines.append(f"• {it['title']} ({it['source']})") if it.get("summary"): text_lines.append(f" {it['summary']}") text_lines.append(f" Read: {base}/a/{it['id']}") text_lines.append(f" Source: {it['canonical_url']}\n") text_lines.append("That's today's good news. You're caught up — see you tomorrow.") text_lines.append(f"\nTo stop these emails: {unsub_url}") text = "\n".join(text_lines) blocks = [] for it in items: summary = f'
{escape(it["summary"])}
' if it.get("summary") else "" why = f'
Why it’s here: {escape(it["reason_text"])}
' if it.get("reason_text") else "" lock = " \U0001f512" if it.get("paywalled") else "" blocks.append( '
' f'{escape(it["title"])}' f'
{escape(it["source"])}
' f'{summary}{why}' '
' f'Read on Upbeat Bytes' f'  ·  Full story at source{lock}' '
' ) html = ( '
' '

Today’s good news

' f'

{n} calm reads for {escape(weekday)}

' + "".join(blocks) + '

That’s today’s good news. ' 'You’re caught up — see you tomorrow.

' f'

You’re getting this because you turned on ' f'the daily digest. Unsubscribe.

' '
' ) return subject, text, html def unsub_url(user: dict, base: str | None = None) -> str: base = base or _base_url() return f"{base}/api/digest/unsubscribe?u={user['id']}&t={user['digest_unsub_token']}" def send_due_digests(conn: sqlite3.Connection, force: bool = False, base: str | None = None) -> int: """Send today's digest to opted-in users who haven't received it yet. Gated to the morning window unless force=True (manual CLI). Skips quietly on a thin day. Deduped via digest_sends. Returns the number sent. """ if not force: hour = local_now().hour if hour < DIGEST_HOUR or hour >= DIGEST_HOUR + DIGEST_WINDOW_HOURS: return 0 brief_date = local_today() items = digest_items(conn, brief_date) if len(items) < MIN_ITEMS: return 0 base = base or _base_url() users = conn.execute( """ SELECT u.id, u.email, u.digest_unsub_token FROM users u WHERE u.digest_enabled = 1 AND NOT EXISTS (SELECT 1 FROM digest_sends d WHERE d.user_id = u.id AND d.brief_date = ?) """, (brief_date,), ).fetchall() sent = 0 for row in users: user = dict(row) if not user.get("digest_unsub_token"): token = secrets.token_urlsafe(18) conn.execute("UPDATE users SET digest_unsub_token = ? WHERE id = ?", (token, user["id"])) conn.commit() user["digest_unsub_token"] = token subject, text, html = build_digest(items, brief_date, unsub_url(user, base), base) try: email_send.send_email(user["email"], subject, text, html=html) except Exception: # noqa: BLE001 — one bad send shouldn't stop the rest; retry next window continue conn.execute( "INSERT OR IGNORE INTO digest_sends (user_id, brief_date, item_count) VALUES (?, ?, ?)", (user["id"], brief_date, len(items)), ) conn.commit() sent += 1 return sent