feat: persist alert results to SQLite history table
Matched results were announced then discarded. Add a dedicated SQLite database (data/alert_history.db) to store every announced result with channel, alert name, backend, title, URL, date, and timestamp. Add !alert history <name> [n] subcommand to query recent results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
100
plugins/alert.py
100
plugins/alert.py
@@ -6,9 +6,11 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sqlite3
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
|
||||
from derp.http import urlopen as _urlopen
|
||||
from derp.plugin import command, event
|
||||
@@ -37,6 +39,58 @@ _pollers: dict[str, asyncio.Task] = {}
|
||||
_subscriptions: dict[str, dict] = {}
|
||||
_errors: dict[str, int] = {}
|
||||
|
||||
# -- History database --------------------------------------------------------
|
||||
|
||||
_DB_PATH = Path("data/alert_history.db")
|
||||
_conn: sqlite3.Connection | None = None
|
||||
|
||||
|
||||
def _db() -> sqlite3.Connection:
|
||||
"""Lazy-init the history database connection and schema."""
|
||||
global _conn
|
||||
if _conn is not None:
|
||||
return _conn
|
||||
_DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
_conn = sqlite3.connect(str(_DB_PATH))
|
||||
_conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS results (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
channel TEXT NOT NULL,
|
||||
alert TEXT NOT NULL,
|
||||
backend TEXT NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
date TEXT NOT NULL DEFAULT '',
|
||||
found_at TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
_conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_results_alert ON results(channel, alert)"
|
||||
)
|
||||
_conn.commit()
|
||||
return _conn
|
||||
|
||||
|
||||
def _save_result(channel: str, alert: str, backend: str, item: dict) -> None:
|
||||
"""Persist a matched result to the history database."""
|
||||
db = _db()
|
||||
db.execute(
|
||||
"INSERT INTO results (channel, alert, backend, item_id, title, url, date, found_at)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
channel,
|
||||
alert,
|
||||
backend,
|
||||
item.get("id", ""),
|
||||
item.get("title", ""),
|
||||
item.get("url", ""),
|
||||
item.get("date", ""),
|
||||
datetime.now(timezone.utc).isoformat(),
|
||||
),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
# -- Pure helpers ------------------------------------------------------------
|
||||
|
||||
@@ -405,6 +459,7 @@ async def _poll_once(bot, key: str, announce: bool = True) -> None:
|
||||
if url:
|
||||
line += f" -- {url}"
|
||||
await bot.send(channel, line)
|
||||
_save_result(channel, name, tag, item)
|
||||
|
||||
for item in new_items:
|
||||
seen_list.append(item["id"])
|
||||
@@ -480,7 +535,7 @@ async def on_connect(bot, message):
|
||||
|
||||
# -- Command handler ---------------------------------------------------------
|
||||
|
||||
@command("alert", help="Alert: !alert add|del|list|check")
|
||||
@command("alert", help="Alert: !alert add|del|list|check|history")
|
||||
async def cmd_alert(bot, message):
|
||||
"""Per-channel keyword alert subscriptions across platforms.
|
||||
|
||||
@@ -489,10 +544,11 @@ async def cmd_alert(bot, message):
|
||||
!alert del <name> Remove alert (admin)
|
||||
!alert list List alerts
|
||||
!alert check <name> Force-poll now
|
||||
!alert history <name> [n] Show recent results (default 5)
|
||||
"""
|
||||
parts = message.text.split(None, 3)
|
||||
if len(parts) < 2:
|
||||
await bot.reply(message, "Usage: !alert <add|del|list|check> [args]")
|
||||
await bot.reply(message, "Usage: !alert <add|del|list|check|history> [args]")
|
||||
return
|
||||
|
||||
sub = parts[1].lower()
|
||||
@@ -545,6 +601,44 @@ async def cmd_alert(bot, message):
|
||||
await bot.reply(message, f"{name}: checked")
|
||||
return
|
||||
|
||||
# -- history (any user, channel only) ------------------------------------
|
||||
if sub == "history":
|
||||
if not message.is_channel:
|
||||
await bot.reply(message, "Use this command in a channel")
|
||||
return
|
||||
if len(parts) < 3:
|
||||
await bot.reply(message, "Usage: !alert history <name> [n]")
|
||||
return
|
||||
name = parts[2].lower()
|
||||
channel = message.target
|
||||
key = _state_key(channel, name)
|
||||
if _load(bot, key) is None:
|
||||
await bot.reply(message, f"No alert '{name}' in this channel")
|
||||
return
|
||||
limit = 5
|
||||
if len(parts) >= 4:
|
||||
try:
|
||||
limit = max(1, min(int(parts[3]), 20))
|
||||
except ValueError:
|
||||
limit = 5
|
||||
db = _db()
|
||||
rows = db.execute(
|
||||
"SELECT backend, title, url, date, found_at FROM results"
|
||||
" WHERE channel = ? AND alert = ? ORDER BY id DESC LIMIT ?",
|
||||
(channel, name, limit),
|
||||
).fetchall()
|
||||
if not rows:
|
||||
await bot.reply(message, f"{name}: no history yet")
|
||||
return
|
||||
for backend, title, url, date, found_at in reversed(rows):
|
||||
ts = found_at[:10]
|
||||
title = _truncate(title) if title else "(no title)"
|
||||
line = f"[{name}/{backend}] ({date or ts}) {title}"
|
||||
if url:
|
||||
line += f" -- {url}"
|
||||
await bot.reply(message, line)
|
||||
return
|
||||
|
||||
# -- add (admin, channel only) -------------------------------------------
|
||||
if sub == "add":
|
||||
if not bot._is_admin(message):
|
||||
@@ -645,4 +739,4 @@ async def cmd_alert(bot, message):
|
||||
await bot.reply(message, f"Removed '{name}'")
|
||||
return
|
||||
|
||||
await bot.reply(message, "Usage: !alert <add|del|list|check> [args]")
|
||||
await bot.reply(message, "Usage: !alert <add|del|list|check|history> [args]")
|
||||
|
||||
Reference in New Issue
Block a user