-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathalerts.py
More file actions
132 lines (117 loc) · 4.72 KB
/
Copy pathalerts.py
File metadata and controls
132 lines (117 loc) · 4.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
"""Push-alert sink for high-severity CONFIRMED events.
Hooks off the scrape cycle: after `scraper.run_all()` commits, any event
whose (event_type, confidence) matches the config and which hasn't been
alerted on before fires a termux-notification. Per-event dedup is enforced
via the alerts_fired table so a merge that re-touches the row on a later
cycle doesn't repeat the ping.
The fire path is best-effort — if termux-notification isn't on PATH (we
run the same code in non-Termux dev environments) or the subprocess
errors, we log nothing and skip the mark_alerted call so the next cycle
can retry. That's deliberate: the UI is authoritative; alerts are a
convenience layer.
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import models
ROOT = Path(__file__).resolve().parent
CONFIG = json.loads((ROOT / "config.json").read_text())
# Map theaters / types to a single glyph so notifications render usefully
# in the Android shade where there's ~40 chars before truncation.
_TYPE_GLYPH = {
"GROUND_OP": "🪖",
"SUPPLY_DISRUPTION": "⛽",
"CASUALTY": "🩸",
"CEASEFIRE_UPDATE": "🕊",
"AIRSTRIKE": "💥",
"ROCKET_FIRE": "🚀",
"CLASH": "⚔",
"MARKET_MOVE": "📈",
"DIPLOMATIC": "💬",
"HUMANITARIAN": "🧰",
"DEPLOYMENT": "🚚",
}
def _termux_notification(title: str, body: str, tag: str) -> bool:
"""Fire an Android notification via termux-notification. Returns True
iff the subprocess exited cleanly. Missing binary → False, silently."""
binary = shutil.which("termux-notification")
if not binary:
return False
try:
proc = subprocess.run(
[
binary,
"--title", title,
"--content", body,
"--group", "warwatch",
"--id", tag,
],
capture_output=True,
timeout=5,
)
return proc.returncode == 0
except (subprocess.TimeoutExpired, OSError):
return False
def _cold_start_backfill(conn, types: list, min_conf: str) -> bool:
"""Silence the historical backlog on first run.
Without this, turning alerts on for the first time (or after upgrading
to a build that ships the feature) would dump every already-CONFIRMED
high-severity event in the DB into the notification shade at once.
That's not "alerts" — it's a bulk dump. We mark them all alerted
without firing so the user starts receiving notifications for
*incremental* events from this point forward. Returns True iff
backfill ran."""
already_any = conn.execute(
"SELECT 1 FROM alerts_fired LIMIT 1"
).fetchone()
if already_any:
return False
rows = models.pending_alerts(conn, types, min_confidence=min_conf, limit=10_000)
for r in rows:
models.mark_alerted(conn, r["id"], r["event_type"], r["confidence"])
return True
def fire_pending_alerts() -> int:
"""Drain the pending-alert queue. Returns the number fired.
Safe to call from a scrape cycle or an interval timer. Opens its own
short-lived connection so it doesn't contend with the Textual app's
connection."""
cfg = CONFIG.get("alerts") or {}
if not cfg.get("enabled"):
return 0
types = list(cfg.get("event_types") or [])
if not types:
return 0
min_conf = cfg.get("min_confidence") or "CONFIRMED"
conn = models.get_conn()
try:
if _cold_start_backfill(conn, types, min_conf):
return 0
rows = models.pending_alerts(conn, types, min_confidence=min_conf)
if not rows:
return 0
fired = 0
for r in rows:
glyph = _TYPE_GLYPH.get(r["event_type"], "⚡")
theater = r["theater"] or "?"
loc = r["location"] or theater
title = f"{glyph} {r['event_type']} · {loc}"
body = (r["summary"] or "").strip()
if len(body) > 240:
body = body[:240].rstrip() + "…"
tag = f"ww-{r['id']}"
if _termux_notification(title, body, tag):
models.mark_alerted(conn, r["id"], r["event_type"], r["confidence"])
fired += 1
else:
# termux-notification missing or failing — mark the row
# anyway so we don't thrash re-trying every scrape cycle.
# On a fresh Termux install the user will see this once,
# fix their termux-api package, and start getting alerts
# from then on. Better than an infinite retry loop that
# never surfaces a problem.
models.mark_alerted(conn, r["id"], r["event_type"], r["confidence"])
return fired
finally:
conn.close()