When MATRIX_USE_THREADS=true, deals are posted into dedicated threads instead of the room timeline: - 🎮 Game Deals — CheapShark + ITAD type=game - 🧩 DLC Deals — ITAD type=dlc - 🆓 Epic Free Games — current and upcoming Epic free games - 📦 Non-Game Deals — ITAD non-game content (software, courses, etc.) Thread roots are created on first use and persisted in the database. When threads are disabled (default), behaviour is unchanged. Key changes: - New threads.py module with ThreadCategory enum and routing logic - ITADDeal now carries deal_type; type filter moved from fetcher to bot - MatrixDealsClient gains send_deal_in_thread() and create_thread_root() - Database gets thread_roots table for storing root event IDs - Bot routes each deal to the appropriate thread via _send_to_thread_or_room() https://claude.ai/code/session_01EfPjktyrF24DBNHWsY1KBP
116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
import logging
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import aiosqlite
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS posted_deals (
|
|
id TEXT PRIMARY KEY,
|
|
source TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS config (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS thread_roots (
|
|
category TEXT PRIMARY KEY,
|
|
event_id TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
"""
|
|
|
|
|
|
class Database:
|
|
def __init__(self, path: str):
|
|
self.path = path
|
|
self._db: aiosqlite.Connection | None = None
|
|
|
|
async def connect(self):
|
|
self._db = await aiosqlite.connect(self.path)
|
|
await self._db.executescript(SCHEMA)
|
|
await self._db.commit()
|
|
logger.info("Database initialized at %s", self.path)
|
|
|
|
async def close(self):
|
|
if self._db:
|
|
await self._db.close()
|
|
|
|
async def has_been_posted(self, deal_id: str) -> bool:
|
|
assert self._db is not None
|
|
cursor = await self._db.execute(
|
|
"SELECT 1 FROM posted_deals WHERE id = ?", (deal_id,)
|
|
)
|
|
row = await cursor.fetchone()
|
|
return row is not None
|
|
|
|
async def mark_posted(self, deal_id: str, source: str, title: str):
|
|
assert self._db is not None
|
|
await self._db.execute(
|
|
"INSERT OR IGNORE INTO posted_deals (id, source, title) VALUES (?, ?, ?)",
|
|
(deal_id, source, title),
|
|
)
|
|
await self._db.commit()
|
|
|
|
async def prune_old(self, days: int = 30):
|
|
assert self._db is not None
|
|
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
|
result = await self._db.execute(
|
|
"DELETE FROM posted_deals WHERE posted_at < ?",
|
|
(cutoff.isoformat(),),
|
|
)
|
|
await self._db.commit()
|
|
if result.rowcount:
|
|
logger.info("Pruned %d old deal records", result.rowcount)
|
|
|
|
async def get_config(self, key: str) -> str | None:
|
|
assert self._db is not None
|
|
cursor = await self._db.execute(
|
|
"SELECT value FROM config WHERE key = ?", (key,)
|
|
)
|
|
row = await cursor.fetchone()
|
|
return row[0] if row else None
|
|
|
|
async def set_config(self, key: str, value: str):
|
|
assert self._db is not None
|
|
await self._db.execute(
|
|
"INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)",
|
|
(key, value),
|
|
)
|
|
await self._db.commit()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Thread root management
|
|
# ------------------------------------------------------------------
|
|
|
|
async def get_thread_root(self, category: str) -> str | None:
|
|
"""Return the Matrix event ID for a thread root, or None."""
|
|
assert self._db is not None
|
|
cursor = await self._db.execute(
|
|
"SELECT event_id FROM thread_roots WHERE category = ?", (category,)
|
|
)
|
|
row = await cursor.fetchone()
|
|
return row[0] if row else None
|
|
|
|
async def set_thread_root(self, category: str, event_id: str):
|
|
"""Store the Matrix event ID for a thread root."""
|
|
assert self._db is not None
|
|
await self._db.execute(
|
|
"INSERT OR REPLACE INTO thread_roots (category, event_id) VALUES (?, ?)",
|
|
(category, event_id),
|
|
)
|
|
await self._db.commit()
|
|
|
|
async def clear_thread_root(self, category: str):
|
|
"""Remove a stored thread root (e.g. if the event was redacted)."""
|
|
assert self._db is not None
|
|
await self._db.execute(
|
|
"DELETE FROM thread_roots WHERE category = ?", (category,)
|
|
)
|
|
await self._db.commit()
|