thermograph/discord.py
Emi Griffith 28b10a0783 Remove em-dashes from site copy and tighten the prose (#206)
Replace em-dashes in user-facing copy across the server-rendered pages,
static frontend views, and the strings the app injects at runtime, using
colons, commas, parentheses or full stops as the context wants. Data
placeholder glyphs (a lone "—" for a missing reading) are left alone,
since a hyphen there reads as a minus sign in temperature columns.

Also tighten the high-visibility surfaces (home hero and meta, about,
privacy, city and records ledes, glossary blurbs) toward a plainer,
more direct voice while keeping every factual claim intact.

Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
2026-07-20 01:48:33 +00:00

113 lines
4 KiB
Python

"""Discord delivery — currently just the daily "most unusual right now" post.
A once-daily message to a Discord channel via an **incoming webhook** (a single
secret URL; no bot, no gateway, no persistent process). It rides the notifier
daemon like the homepage feed refresh does, so it inherits the single-leader
invariant and costs no new infrastructure. Entirely optional: with no webhook
configured, everything here no-ops.
The message is built from the same `data/homepage.json` feed the homepage renders
(homepage.build/refresh), so the Discord post and the site always agree.
"""
from __future__ import annotations
import datetime
import os
import httpx
import homepage
# The webhook URL IS the credential — env only, never the repo. Unset => disabled.
WEBHOOK_URL = os.environ.get("THERMOGRAPH_DISCORD_WEBHOOK", "").strip()
# How many cities the daily post lists.
POST_LIMIT = 8
# Embed accent: warm when the top anomaly is hot, cool when it's cold.
_HOT = 0xE8663B
_COLD = 0x4393C3
_TIMEOUT = httpx.Timeout(10.0)
def enabled() -> bool:
return bool(WEBHOOK_URL)
def _f_c(value) -> str:
"""A Fahrenheit reading as '104°F (40°C)' — Discord's audience is global, so
the post carries both, even though the site picks one per city."""
if value is None:
return ""
f = round(value)
c = round((value - 32) * 5 / 9)
return f"{f}°F ({c}°C)"
def _line(card: dict) -> str:
"""One city's line in the post."""
display = card.get("display") or card.get("name") or ""
metric = (card.get("metric_label") or "").lower() or "reading"
val = _f_c(card.get("value"))
normal = _f_c(card.get("normal")) if card.get("normal") is not None else None
pct = card.get("pct_label") or ""
grade = card.get("grade_label") or card.get("grade") or ""
tag = " · ".join(p for p in (pct, grade) if p)
when = f" ({card['date_label']})" if card.get("date_label") else ""
normal_txt = f", normal {normal}" if normal else ""
return f"**{display}**: {metric} {val}{normal_txt}{when}" + (f" · {tag}" if tag else "")
def build_embed(feed: dict) -> dict:
"""A Discord rich embed for the day's most unusual cities, or raise ValueError
if the feed has nothing to show."""
ranked = (feed or {}).get("ranked") or []
if not ranked:
raise ValueError("empty feed")
top = ranked[:POST_LIMIT]
color = _COLD if top[0].get("tail") == "cold" else _HOT
date_s = feed.get("date") or datetime.date.today().isoformat()
description = "\n".join(_line(c) for c in top)
embed = {
"title": "🌡️ Most unusual weather right now",
"description": description,
"url": "https://thermograph.org/",
"color": color,
"footer": {"text": "thermograph.org · every day graded against ~45 years of local history"},
}
# ISO8601 timestamp from the feed's build time, when available.
gen = feed.get("generated_at")
if gen:
embed["timestamp"] = datetime.datetime.fromtimestamp(
float(gen), tz=datetime.timezone.utc
).isoformat()
else:
embed["footer"]["text"] += f" · {date_s}"
return embed
def post_daily_feed(feed: dict | None = None) -> bool:
"""Post the daily feed to the configured webhook. Best-effort: returns True on
a 2xx, False otherwise (disabled, empty/stale feed, or any HTTP error) and
never raises — a Discord failure must not disturb the notifier pass."""
if not enabled():
return False
if feed is None:
feed = homepage.load()
if not feed or homepage.is_stale(feed):
return False
try:
embed = build_embed(feed)
except ValueError:
return False
payload = {
"username": "Thermograph",
"avatar_url": "https://thermograph.org/logo.png?v=3",
"embeds": [embed],
}
try:
resp = httpx.post(WEBHOOK_URL, json=payload, timeout=_TIMEOUT)
return 200 <= resp.status_code < 300
except Exception: # noqa: BLE001 - best-effort side channel
return False