Add DM support, friend identification, and target pruning

React in 1:1 DMs alongside servers/groups, surface the resolved friend
username at startup and in the stats panel, verify reactions actually
stick after adding them (with jump links for manual spot-checking),
and prune stale targets from stats on each run so only the currently
mutual set is shown.
This commit is contained in:
2026-08-01 18:03:02 +02:00
parent 3118f7b9d5
commit c8359ef4bb
7 changed files with 116 additions and 18 deletions

View File

@@ -6,7 +6,7 @@ from typing import Optional
SCHEMA = """
CREATE TABLE IF NOT EXISTS targets (
target_id TEXT PRIMARY KEY,
target_type TEXT NOT NULL CHECK(target_type IN ('guild','group_dm')),
target_type TEXT NOT NULL CHECK(target_type IN ('guild','group_dm','dm')),
name TEXT,
first_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
reaction_count INTEGER NOT NULL DEFAULT 0
@@ -55,6 +55,28 @@ class Storage:
if "message_created_at" not in columns:
self._conn.execute("ALTER TABLE reactions_log ADD COLUMN message_created_at TEXT")
# Older DBs created before 'dm' targets (1:1 DMs) existed have a CHECK
# constraint that only allows 'guild'/'group_dm' — rebuild the table
# with the wider constraint, preserving all existing rows.
targets_sql = self._conn.execute(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='targets'"
).fetchone()
if targets_sql and "'dm'" not in targets_sql["sql"]:
self._conn.executescript(
"""
ALTER TABLE targets RENAME TO targets_old;
CREATE TABLE targets (
target_id TEXT PRIMARY KEY,
target_type TEXT NOT NULL CHECK(target_type IN ('guild','group_dm','dm')),
name TEXT,
first_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
reaction_count INTEGER NOT NULL DEFAULT 0
);
INSERT INTO targets SELECT * FROM targets_old;
DROP TABLE targets_old;
"""
)
def upsert_target(self, target_id, target_type: str, name: str) -> None:
with self._lock, self._conn:
self._conn.execute(
@@ -66,6 +88,21 @@ class Storage:
(str(target_id), target_type, name),
)
def prune_targets(self, keep_target_ids: list) -> int:
"""Remove targets not present in the latest discovery run (e.g. a server
that's no longer mutual, or a stale entry from an earlier bug/test run).
reactions_log/channel_scan_state rows are left alone for history/undo."""
keep = [str(t) for t in keep_target_ids]
with self._lock, self._conn:
placeholders = ",".join("?" * len(keep)) if keep else None
if placeholders:
cur = self._conn.execute(
f"DELETE FROM targets WHERE target_id NOT IN ({placeholders})", keep
)
else:
cur = self._conn.execute("DELETE FROM targets")
return cur.rowcount
def has_reacted(self, message_id, emoji: str) -> bool:
with self._lock:
cur = self._conn.execute(