feat: add short IDs to alert results with !alert info command
Each alert result gets a deterministic 8-char base36 ID derived from backend:item_id. IDs appear in announcements and history, and can be looked up with !alert info <id> for full details. Existing rows are backfilled on startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@@ -93,22 +94,42 @@ def _db() -> sqlite3.Connection:
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
date TEXT NOT NULL DEFAULT '',
|
||||
found_at TEXT NOT NULL
|
||||
found_at TEXT NOT NULL,
|
||||
short_id TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
""")
|
||||
try:
|
||||
_conn.execute(
|
||||
"ALTER TABLE results ADD COLUMN short_id TEXT NOT NULL DEFAULT ''"
|
||||
)
|
||||
except sqlite3.OperationalError:
|
||||
pass # column already exists
|
||||
_conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_results_alert ON results(channel, alert)"
|
||||
)
|
||||
_conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_results_short_id ON results(short_id)"
|
||||
)
|
||||
# Backfill short_id for rows that predate the column
|
||||
for row_id, backend, item_id in _conn.execute(
|
||||
"SELECT id, backend, item_id FROM results WHERE short_id = ''"
|
||||
).fetchall():
|
||||
_conn.execute(
|
||||
"UPDATE results SET short_id = ? WHERE id = ?",
|
||||
(_make_short_id(backend, item_id), row_id),
|
||||
)
|
||||
_conn.commit()
|
||||
return _conn
|
||||
|
||||
|
||||
def _save_result(channel: str, alert: str, backend: str, item: dict) -> None:
|
||||
"""Persist a matched result to the history database."""
|
||||
def _save_result(channel: str, alert: str, backend: str, item: dict) -> str:
|
||||
"""Persist a matched result to the history database. Returns short_id."""
|
||||
short_id = _make_short_id(backend, item.get("id", ""))
|
||||
db = _db()
|
||||
db.execute(
|
||||
"INSERT INTO results (channel, alert, backend, item_id, title, url, date, found_at)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO results"
|
||||
" (channel, alert, backend, item_id, title, url, date, found_at, short_id)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
channel,
|
||||
alert,
|
||||
@@ -118,9 +139,11 @@ def _save_result(channel: str, alert: str, backend: str, item: dict) -> None:
|
||||
item.get("url", ""),
|
||||
item.get("date", ""),
|
||||
datetime.now(timezone.utc).isoformat(),
|
||||
short_id,
|
||||
),
|
||||
)
|
||||
db.commit()
|
||||
return short_id
|
||||
|
||||
|
||||
# -- Pure helpers ------------------------------------------------------------
|
||||
@@ -209,6 +232,18 @@ class _DDGParser(HTMLParser):
|
||||
self.results.append((self._url, title))
|
||||
|
||||
|
||||
def _make_short_id(backend: str, item_id: str) -> str:
|
||||
"""Deterministic 8-char base36 hash from backend:item_id."""
|
||||
digest = hashlib.sha256(f"{backend}:{item_id}".encode()).digest()
|
||||
n = int.from_bytes(digest[:5], "big")
|
||||
chars = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
parts = []
|
||||
while n:
|
||||
n, r = divmod(n, 36)
|
||||
parts.append(chars[r])
|
||||
return "".join(reversed(parts)) or "0"
|
||||
|
||||
|
||||
def _parse_date(raw: str) -> str:
|
||||
"""Try to extract a YYYY-MM-DD date from a raw date string."""
|
||||
m = re.search(r"\d{4}-\d{2}-\d{2}", raw)
|
||||
@@ -1200,17 +1235,17 @@ async def _poll_once(bot, key: str, announce: bool = True) -> None:
|
||||
channel = data["channel"]
|
||||
name = data["name"]
|
||||
for item in matched:
|
||||
short_id = _save_result(channel, name, tag, item)
|
||||
title = _truncate(item["title"]) if item["title"] else "(no title)"
|
||||
url = item["url"]
|
||||
date = item.get("date", "")
|
||||
line = f"[{name}/{tag}]"
|
||||
line = f"[{name}/{tag}/{short_id}]"
|
||||
if date:
|
||||
line += f" ({date})"
|
||||
line += f" {title}"
|
||||
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"])
|
||||
@@ -1286,7 +1321,7 @@ async def on_connect(bot, message):
|
||||
|
||||
# -- Command handler ---------------------------------------------------------
|
||||
|
||||
@command("alert", help="Alert: !alert add|del|list|check|history")
|
||||
@command("alert", help="Alert: !alert add|del|list|check|info|history")
|
||||
async def cmd_alert(bot, message):
|
||||
"""Per-channel keyword alert subscriptions across platforms.
|
||||
|
||||
@@ -1295,11 +1330,12 @@ async def cmd_alert(bot, message):
|
||||
!alert del <name> Remove alert (admin)
|
||||
!alert list List alerts
|
||||
!alert check <name> Force-poll now
|
||||
!alert info <id> Show full details for a result
|
||||
!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|history> [args]")
|
||||
await bot.reply(message, "Usage: !alert <add|del|list|check|info|history> [args]")
|
||||
return
|
||||
|
||||
sub = parts[1].lower()
|
||||
@@ -1374,22 +1410,51 @@ async def cmd_alert(bot, message):
|
||||
limit = 5
|
||||
db = _db()
|
||||
rows = db.execute(
|
||||
"SELECT backend, title, url, date, found_at FROM results"
|
||||
"SELECT backend, title, url, date, found_at, short_id 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):
|
||||
for backend, title, url, date, found_at, short_id in reversed(rows):
|
||||
ts = found_at[:10]
|
||||
title = _truncate(title) if title else "(no title)"
|
||||
line = f"[{name}/{backend}] ({date or ts}) {title}"
|
||||
line = f"[{name}/{backend}/{short_id}] ({date or ts}) {title}"
|
||||
if url:
|
||||
line += f" -- {url}"
|
||||
await bot.reply(message, line)
|
||||
return
|
||||
|
||||
# -- info (any user, channel only) ---------------------------------------
|
||||
if sub == "info":
|
||||
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 info <id>")
|
||||
return
|
||||
short_id = parts[2].lower()
|
||||
channel = message.target
|
||||
db = _db()
|
||||
row = db.execute(
|
||||
"SELECT alert, backend, title, url, date, found_at, short_id"
|
||||
" FROM results WHERE short_id = ? AND channel = ? LIMIT 1",
|
||||
(short_id, channel),
|
||||
).fetchone()
|
||||
if not row:
|
||||
await bot.reply(message, f"No result with id '{short_id}'")
|
||||
return
|
||||
alert, backend, title, url, date, found_at, sid = row
|
||||
await bot.reply(message, f"[{alert}/{backend}/{sid}] {title or '(no title)'}")
|
||||
if url:
|
||||
await bot.reply(message, url)
|
||||
await bot.reply(
|
||||
message,
|
||||
f"Date: {date or 'n/a'} | Found: {found_at[:19]}",
|
||||
)
|
||||
return
|
||||
|
||||
# -- add (admin, channel only) -------------------------------------------
|
||||
if sub == "add":
|
||||
if not bot._is_admin(message):
|
||||
@@ -1490,4 +1555,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|history> [args]")
|
||||
await bot.reply(message, "Usage: !alert <add|del|list|check|info|history> [args]")
|
||||
|
||||
Reference in New Issue
Block a user