This commit is contained in:
2026-08-16 12:02:37 -04:00
parent 268938abb1
commit fffb9c50b4
6 changed files with 66 additions and 49 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
config/* config/*
.venv/
__pycache__/
+10 -9
View File
@@ -9,7 +9,7 @@ import asyncio
import sqlite3 import sqlite3
import time import time
import sys import sys
from nio import AsyncClient, LoginResponse, RoomMessagesResponse, SyncResponse from nio import AsyncClient, LoginResponse, RoomMessageText, RoomMessagesResponse, SyncResponse
from room import Room from room import Room
import dbutil import dbutil
@@ -79,6 +79,7 @@ class UserListManager:
return [] return []
# Sync to get room state # Sync to get room state
assert self.room.client is not None
sync_resp = await self.room.client.sync(timeout=3000) sync_resp = await self.room.client.sync(timeout=3000)
if not isinstance(sync_resp, SyncResponse): if not isinstance(sync_resp, SyncResponse):
print(f"Sync failed: {sync_resp}") print(f"Sync failed: {sync_resp}")
@@ -119,6 +120,7 @@ class UserListManager:
fetched = 0 fetched = 0
# Fetch messages in batches to count posts per user # Fetch messages in batches to count posts per user
assert self.room.client is not None
while fetched < lookback_limit: while fetched < lookback_limit:
try: try:
response = await self.room.client.room_messages( response = await self.room.client.room_messages(
@@ -133,18 +135,17 @@ class UserListManager:
fetched += len(response.chunk) fetched += len(response.chunk)
for event in response.chunk: for event in response.chunk:
if hasattr(event, 'sender') and hasattr(event, 'body'): if isinstance(event, RoomMessageText):
sender = event.sender sender = event.sender
post_counts[sender] = post_counts.get(sender, 0) + 1 post_counts[sender] = post_counts.get(sender, 0) + 1
# Track the most recent message timestamp for each user # Track the most recent message timestamp for each user
if hasattr(event, 'server_timestamp'): event_time = int(event.server_timestamp // 1000)
event_time = int(event.server_timestamp // 1000) if sender not in last_activity or event_time > last_activity[sender]:
if sender not in last_activity or event_time > last_activity[sender]: last_activity[sender] = event_time
last_activity[sender] = event_time # Track the absolute most recent post in the room
# Track the absolute most recent post in the room if event_time > most_recent_post:
if event_time > most_recent_post: most_recent_post = event_time
most_recent_post = event_time
next_batch = response.end next_batch = response.end
+1
View File
@@ -35,6 +35,7 @@ async def main():
return return
# Initial sync establishes the next-batch; after this, listen from "now" # Initial sync establishes the next-batch; after this, listen from "now"
assert room.client is not None
await room.client.sync(timeout=300) await room.client.sync(timeout=300)
room.write_room_slugs() room.write_room_slugs()
+6
View File
@@ -0,0 +1,6 @@
{
"venvPath": ".",
"venv": ".venv",
"pythonVersion": "3.14",
"typeCheckingMode": "basic"
}
+46 -39
View File
@@ -10,7 +10,7 @@ import yaml
import sqlite3 import sqlite3
import re import re
import asyncio import asyncio
from nio import AsyncClient, LoginResponse, RoomMessageText, SyncResponse, RoomSendResponse from nio import AsyncClient, LoginResponse, MessageDirection, ProfileGetDisplayNameResponse, RoomMessageText, RoomMessagesResponse, RoomSendResponse, SyncResponse
from post import Post from post import Post
import requests import requests
import dbutil import dbutil
@@ -64,7 +64,7 @@ class Room:
def init_db(self): def init_db(self):
conn = dbutil.connect(self.db_path) conn = dbutil.connect(self.db_path)
c = conn.cursor() c = conn.cursor()
c.execute(''' _ = c.execute('''
CREATE TABLE IF NOT EXISTS suggestions ( CREATE TABLE IF NOT EXISTS suggestions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user TEXT, user TEXT,
@@ -78,17 +78,17 @@ class Room:
) )
''') ''')
# Add is_posted column if it doesn't exist # Add is_posted column if it doesn't exist
c.execute("PRAGMA table_info(suggestions)") _ = c.execute("PRAGMA table_info(suggestions)")
cols = [row[1] for row in c.fetchall()] cols = [row[1] for row in c.fetchall()]
if 'is_posted' not in cols: if 'is_posted' not in cols:
c.execute('ALTER TABLE suggestions ADD COLUMN is_posted INTEGER DEFAULT 0') _ = c.execute('ALTER TABLE suggestions ADD COLUMN is_posted INTEGER DEFAULT 0')
if 'event_id' not in cols: if 'event_id' not in cols:
c.execute('ALTER TABLE suggestions ADD COLUMN event_id TEXT') _ = c.execute('ALTER TABLE suggestions ADD COLUMN event_id TEXT')
# Ensure a uniqueness index on event_id to avoid double-acking the same message # Ensure a uniqueness index on event_id to avoid double-acking the same message
c.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_suggestions_event_id ON suggestions(event_id)') _ = c.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_suggestions_event_id ON suggestions(event_id)')
# New: table to log all !gettitles detections (prevents duplicates) # New: table to log all !gettitles detections (prevents duplicates)
c.execute(''' _ = c.execute('''
CREATE TABLE IF NOT EXISTS gettitles ( CREATE TABLE IF NOT EXISTS gettitles (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT, event_id TEXT,
@@ -101,7 +101,7 @@ class Room:
''') ''')
# New: table to log all !mayhem detections (prevents duplicates) # New: table to log all !mayhem detections (prevents duplicates)
c.execute(''' _ = c.execute('''
CREATE TABLE IF NOT EXISTS mayhem ( CREATE TABLE IF NOT EXISTS mayhem (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT, event_id TEXT,
@@ -114,7 +114,7 @@ class Room:
''') ''')
# New: table to log all !countvotes detections (prevents duplicates) # New: table to log all !countvotes detections (prevents duplicates)
c.execute(''' _ = c.execute('''
CREATE TABLE IF NOT EXISTS countvotes ( CREATE TABLE IF NOT EXISTS countvotes (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT, event_id TEXT,
@@ -127,7 +127,7 @@ class Room:
''') ''')
# New: table to log all !voteurl detections (prevents duplicates) # New: table to log all !voteurl detections (prevents duplicates)
c.execute(''' _ = c.execute('''
CREATE TABLE IF NOT EXISTS voteurl ( CREATE TABLE IF NOT EXISTS voteurl (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT, event_id TEXT,
@@ -161,13 +161,14 @@ class Room:
return False return False
async def get_display_name(self, user_id): async def get_display_name(self, user_id):
assert self.client is not None
room = self.client.rooms.get(self.config.room_id) room = self.client.rooms.get(self.config.room_id)
if room and user_id in room.users: if room and user_id in room.users:
member = room.users[user_id] member = room.users[user_id]
if hasattr(member, "display_name") and member.display_name: if hasattr(member, "display_name") and member.display_name:
return member.display_name return member.display_name
resp = await self.client.get_displayname(user_id) resp = await self.client.get_displayname(user_id)
if hasattr(resp, "displayname") and resp.displayname: if isinstance(resp, ProfileGetDisplayNameResponse) and resp.displayname:
return resp.displayname return resp.displayname
return user_id return user_id
@@ -219,6 +220,7 @@ class Room:
batch_size = self.config.batch_size batch_size = self.config.batch_size
next_batch = None next_batch = None
fetched = 0 fetched = 0
assert self.client is not None
while fetched < limit: while fetched < limit:
fetch_amount = min(batch_size, limit - fetched) fetch_amount = min(batch_size, limit - fetched)
@@ -226,10 +228,10 @@ class Room:
self.config.room_id, self.config.room_id,
start=next_batch, start=next_batch,
limit=fetch_amount, limit=fetch_amount,
direction="b" direction=MessageDirection.back
) )
if not hasattr(response, "chunk") or not response.chunk: if not isinstance(response, RoomMessagesResponse) or not response.chunk:
break break
for event in response.chunk: for event in response.chunk:
@@ -304,7 +306,7 @@ class Room:
self.newly_inserted_posts = [] self.newly_inserted_posts = []
for post in self.posts: for post in self.posts:
try: try:
c.execute( _ = c.execute(
"INSERT INTO suggestions (user, display_name, message, timestamp, age, is_posted, event_id) VALUES (?, ?, ?, ?, ?, ?, ?)", "INSERT INTO suggestions (user, display_name, message, timestamp, age, is_posted, event_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
post.as_tuple() post.as_tuple()
) )
@@ -319,21 +321,21 @@ class Room:
room_id = self.config.room_id room_id = self.config.room_id
conn = dbutil.connect("last_run.db") conn = dbutil.connect("last_run.db")
c = conn.cursor() c = conn.cursor()
c.execute(''' _ = c.execute('''
CREATE TABLE IF NOT EXISTS last_run ( CREATE TABLE IF NOT EXISTS last_run (
room_id TEXT PRIMARY KEY, room_id TEXT PRIMARY KEY,
timestamp INTEGER, timestamp INTEGER,
datetime TEXT datetime TEXT
) )
''') ''')
c.execute("PRAGMA table_info(last_run)") _ = c.execute("PRAGMA table_info(last_run)")
cols = [row[1] for row in c.fetchall()] cols = [row[1] for row in c.fetchall()]
if 'datetime' not in cols: if 'datetime' not in cols:
c.execute('ALTER TABLE last_run ADD COLUMN datetime TEXT') _ = c.execute('ALTER TABLE last_run ADD COLUMN datetime TEXT')
now = int(_time.time()) now = int(_time.time())
now_str = datetime.datetime.now().isoformat(sep=' ', timespec='seconds') now_str = datetime.datetime.now().isoformat(sep=' ', timespec='seconds')
c.execute('INSERT OR REPLACE INTO last_run (room_id, timestamp, datetime) VALUES (?, ?, ?)', (room_id, now, now_str)) _ = c.execute('INSERT OR REPLACE INTO last_run (room_id, timestamp, datetime) VALUES (?, ?, ?)', (room_id, now, now_str))
dbutil.commit(conn) dbutil.commit(conn)
conn.close() conn.close()
@@ -341,7 +343,7 @@ class Room:
room_id = self.config.room_id room_id = self.config.room_id
conn = dbutil.connect("last_run.db") conn = dbutil.connect("last_run.db")
c = conn.cursor() c = conn.cursor()
c.execute(''' _ = c.execute('''
CREATE TABLE IF NOT EXISTS last_run ( CREATE TABLE IF NOT EXISTS last_run (
room_id TEXT PRIMARY KEY, room_id TEXT PRIMARY KEY,
timestamp INTEGER, timestamp INTEGER,
@@ -349,11 +351,11 @@ class Room:
) )
''') ''')
c.execute("PRAGMA table_info(last_run)") _ = c.execute("PRAGMA table_info(last_run)")
cols = [row[1] for row in c.fetchall()] cols = [row[1] for row in c.fetchall()]
if 'datetime' not in cols: if 'datetime' not in cols:
c.execute('ALTER TABLE last_run ADD COLUMN datetime TEXT') _ = c.execute('ALTER TABLE last_run ADD COLUMN datetime TEXT')
c.execute('SELECT timestamp FROM last_run WHERE room_id = ?', (room_id,)) _ = c.execute('SELECT timestamp FROM last_run WHERE room_id = ?', (room_id,))
row = c.fetchone() row = c.fetchone()
conn.close() conn.close()
if row: if row:
@@ -366,7 +368,7 @@ class Room:
conn = dbutil.connect(self.db_path) conn = dbutil.connect(self.db_path)
c = conn.cursor() c = conn.cursor()
dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds') dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds')
c.execute(''' _ = c.execute('''
INSERT OR IGNORE INTO gettitles (event_id, sender, timestamp, datetime) INSERT OR IGNORE INTO gettitles (event_id, sender, timestamp, datetime)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
''', (event_id, sender, int(timestamp), dt_str)) ''', (event_id, sender, int(timestamp), dt_str))
@@ -381,7 +383,7 @@ class Room:
conn = dbutil.connect(self.db_path) conn = dbutil.connect(self.db_path)
c = conn.cursor() c = conn.cursor()
dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds') dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds')
c.execute(''' _ = c.execute('''
INSERT OR IGNORE INTO mayhem (event_id, sender, timestamp, datetime) INSERT OR IGNORE INTO mayhem (event_id, sender, timestamp, datetime)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
''', (event_id, sender, int(timestamp), dt_str)) ''', (event_id, sender, int(timestamp), dt_str))
@@ -396,7 +398,7 @@ class Room:
conn = dbutil.connect(self.db_path) conn = dbutil.connect(self.db_path)
c = conn.cursor() c = conn.cursor()
dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds') dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds')
c.execute(''' _ = c.execute('''
INSERT OR IGNORE INTO countvotes (event_id, sender, timestamp, datetime) INSERT OR IGNORE INTO countvotes (event_id, sender, timestamp, datetime)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
''', (event_id, sender, int(timestamp), dt_str)) ''', (event_id, sender, int(timestamp), dt_str))
@@ -411,7 +413,7 @@ class Room:
conn = dbutil.connect(self.db_path) conn = dbutil.connect(self.db_path)
c = conn.cursor() c = conn.cursor()
dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds') dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds')
c.execute(''' _ = c.execute('''
INSERT OR IGNORE INTO voteurl (event_id, sender, timestamp, datetime) INSERT OR IGNORE INTO voteurl (event_id, sender, timestamp, datetime)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
''', (event_id, sender, int(timestamp), dt_str)) ''', (event_id, sender, int(timestamp), dt_str))
@@ -427,6 +429,7 @@ class Room:
next_batch = None next_batch = None
suggestion_votes = {} suggestion_votes = {}
all_events = [] all_events = []
assert self.client is not None
# First pass: collect all recent events (both messages and reactions) # First pass: collect all recent events (both messages and reactions)
for _ in range(15): # Look back further to catch more events for _ in range(15): # Look back further to catch more events
@@ -434,10 +437,10 @@ class Room:
self.config.room_id, self.config.room_id,
start=next_batch, start=next_batch,
limit=batch_size, limit=batch_size,
direction="b" direction=MessageDirection.back
) )
if not hasattr(response, "chunk") or not response.chunk: if not isinstance(response, RoomMessagesResponse) or not response.chunk:
break break
all_events.extend(response.chunk) all_events.extend(response.chunk)
@@ -522,6 +525,7 @@ class Room:
if since_ms is None: if since_ms is None:
since_ms = int(_time.time() * 1000) since_ms = int(_time.time() * 1000)
assert self.client is not None
while True: while True:
sync_response = await self.client.sync(timeout=30000) sync_response = await self.client.sync(timeout=30000)
if not isinstance(sync_response, SyncResponse): if not isinstance(sync_response, SyncResponse):
@@ -585,6 +589,7 @@ class Room:
return result == "gettitles" return result == "gettitles"
async def alert_post(self, post): async def alert_post(self, post):
assert self.client is not None
content = { content = {
"msgtype": "m.text", "msgtype": "m.text",
"body": post "body": post
@@ -628,7 +633,7 @@ class Room:
if conditions: if conditions:
query += " WHERE " + " AND ".join(conditions) query += " WHERE " + " AND ".join(conditions)
query += " ORDER BY timestamp ASC" query += " ORDER BY timestamp ASC"
c.execute(query, tuple(params)) _ = c.execute(query, tuple(params))
rows = c.fetchall() rows = c.fetchall()
conn.close() conn.close()
self.posts = [Post(poster=row[0], display_name=row[1], content=row[2], time=row[3], age=row[4], is_posted=bool(row[5])) for row in rows] self.posts = [Post(poster=row[0], display_name=row[1], content=row[2], time=row[3], age=row[4], is_posted=bool(row[5])) for row in rows]
@@ -636,18 +641,18 @@ class Room:
def get_last_post(self): def get_last_post(self):
conn = dbutil.connect("last_post.db") conn = dbutil.connect("last_post.db")
c = conn.cursor() c = conn.cursor()
c.execute(''' _ = c.execute('''
CREATE TABLE IF NOT EXISTS last_post ( CREATE TABLE IF NOT EXISTS last_post (
room_id TEXT PRIMARY KEY, room_id TEXT PRIMARY KEY,
timestamp INTEGER, timestamp INTEGER,
datetime TEXT datetime TEXT
) )
''') ''')
c.execute("PRAGMA table_info(last_post)") _ = c.execute("PRAGMA table_info(last_post)")
cols = [row[1] for row in c.fetchall()] cols = [row[1] for row in c.fetchall()]
if 'datetime' not in cols: if 'datetime' not in cols:
c.execute('ALTER TABLE last_post ADD COLUMN datetime TEXT') _ = c.execute('ALTER TABLE last_post ADD COLUMN datetime TEXT')
c.execute('SELECT timestamp FROM last_post WHERE room_id = ?', (self.config.room_id,)) _ = c.execute('SELECT timestamp FROM last_post WHERE room_id = ?', (self.config.room_id,))
row = c.fetchone() row = c.fetchone()
conn.close() conn.close()
if row: if row:
@@ -657,29 +662,29 @@ class Room:
def update_last_post(self): def update_last_post(self):
conn = dbutil.connect("last_post.db") conn = dbutil.connect("last_post.db")
c = conn.cursor() c = conn.cursor()
c.execute(''' _ = c.execute('''
CREATE TABLE IF NOT EXISTS last_post ( CREATE TABLE IF NOT EXISTS last_post (
room_id TEXT PRIMARY KEY, room_id TEXT PRIMARY KEY,
timestamp INTEGER, timestamp INTEGER,
datetime TEXT datetime TEXT
) )
''') ''')
c.execute("PRAGMA table_info(last_post)") _ = c.execute("PRAGMA table_info(last_post)")
cols = [row[1] for row in c.fetchall()] cols = [row[1] for row in c.fetchall()]
if 'datetime' not in cols: if 'datetime' not in cols:
c.execute('ALTER TABLE last_post ADD COLUMN datetime TEXT') _ = c.execute('ALTER TABLE last_post ADD COLUMN datetime TEXT')
now = int(_time.time()) now = int(_time.time())
now_str = datetime.datetime.now().isoformat(sep=' ', timespec='seconds') now_str = datetime.datetime.now().isoformat(sep=' ', timespec='seconds')
c.execute('INSERT OR REPLACE INTO last_post (room_id, timestamp, datetime) VALUES (?, ?, ?)', _ = c.execute('INSERT OR REPLACE INTO last_post (room_id, timestamp, datetime) VALUES (?, ?, ?)',
(self.config.room_id, now, now_str)) (self.config.room_id, now, now_str))
dbutil.commit(conn) dbutil.commit(conn)
conn.close() conn.close()
async def post_suggestions(self): async def post_suggestions(self):
assert self.client is not None
# Filter out already posted suggestions # Filter out already posted suggestions
unposted_posts = [post for post in self.posts if not post.is_posted] unposted_posts = [post for post in self.posts if not post.is_posted]
if not unposted_posts: if not unposted_posts:
await self.alert_post("No new unposted suggestions found.") await self.alert_post("No new unposted suggestions found.")
return return
@@ -714,6 +719,7 @@ class Room:
async def post_suggestions_individually(self): async def post_suggestions_individually(self):
"""Post each unposted suggestion as its own message with a 👍 reaction for voting.""" """Post each unposted suggestion as its own message with a 👍 reaction for voting."""
assert self.client is not None
unposted_posts = [post for post in self.posts if not post.is_posted] unposted_posts = [post for post in self.posts if not post.is_posted]
if not unposted_posts: if not unposted_posts:
await self.alert_post("No new unposted suggestions found.") await self.alert_post("No new unposted suggestions found.")
@@ -791,7 +797,7 @@ class Room:
conn = dbutil.connect(self.db_path) conn = dbutil.connect(self.db_path)
c = conn.cursor() c = conn.cursor()
for post in posts: for post in posts:
c.execute( _ = c.execute(
"UPDATE suggestions SET is_posted = 1 WHERE user = ? AND message = ? AND timestamp = ?", "UPDATE suggestions SET is_posted = 1 WHERE user = ? AND message = ? AND timestamp = ?",
(post.poster, post.content, post.time) (post.poster, post.content, post.time)
) )
@@ -804,6 +810,7 @@ class Room:
async def acknowledge_new_suggestions(self): async def acknowledge_new_suggestions(self):
"""React ✅ to newly recorded suggestions to confirm they were saved.""" """React ✅ to newly recorded suggestions to confirm they were saved."""
assert self.client is not None
if not self.newly_inserted_posts: if not self.newly_inserted_posts:
return return
for post in self.newly_inserted_posts: for post in self.newly_inserted_posts:
+1
View File
@@ -21,6 +21,7 @@ async def main():
# await room.alert_post("SCRAPER LOGGED IN") # await room.alert_post("SCRAPER LOGGED IN")
# Initial sync # Initial sync
assert room.client is not None
await room.client.sync(timeout=3000) await room.client.sync(timeout=3000)
room.write_room_slugs() room.write_room_slugs()