830 lines
33 KiB
Python
Executable File
830 lines
33 KiB
Python
Executable File
"""
|
|
This is an instance of connecting to a room and maintaining suggestions.
|
|
I've been re-writing the original functions of methods for this class, so
|
|
there's some elements that need to be cleaned up and only make sense in that light.
|
|
"""
|
|
|
|
import time as _time
|
|
import datetime
|
|
import yaml
|
|
import sqlite3
|
|
import re
|
|
import asyncio
|
|
from nio import AsyncClient, LoginResponse, RoomMessageText, SyncResponse, RoomSendResponse
|
|
from post import Post
|
|
import requests
|
|
|
|
default_search_terms = ["!suggest", "!Suggest", "!suggestion", "!Suggestion", "SUGGEST", "!sug"]
|
|
|
|
class Config:
|
|
def __init__(self, config_path="configs/config.yaml"):
|
|
self.load_config(config_path)
|
|
|
|
def load_config(self, path="configs/config.yaml"):
|
|
with open(path, 'r') as file:
|
|
config = yaml.safe_load(file)
|
|
self.username = config.get("username", "magbot")
|
|
self.password = config.get("password", "")
|
|
self.room_id = config.get("room_id", "")
|
|
self.lookback_limit = config.get("lookback_limit", 3600 * 2)
|
|
self.db_name = config.get("dbName", "suggestions.db")
|
|
self.batch_size = config.get("batch_size", 100)
|
|
self.user_lookback_limit = config.get("user_lookback_limit", 5000)
|
|
self.homeserver = config.get("homeserver", "https://matrix.org")
|
|
self.vote_host = config.get("vote_host", "localhost")
|
|
self.vote_activity_hours = config.get("vote_activity_hours", 72)
|
|
self.vote_path = config.get("vote_path", "")
|
|
|
|
class Room:
|
|
def __init__(self, config_path="configs/config.yaml", search_terms=None, homeserver=None):
|
|
self.config = Config(config_path)
|
|
self.search_terms = search_terms or default_search_terms
|
|
self.formatted_sugs = ""
|
|
self.posts = []
|
|
self.newly_inserted_posts = []
|
|
self.client = None
|
|
self.room = None
|
|
self.user_id = None
|
|
self.member = None
|
|
self.display_name = None
|
|
self.last_run = 0
|
|
|
|
if homeserver:
|
|
self.config.homeserver = homeserver
|
|
|
|
self.db_path = self.safe_db_name(self.config.room_id)
|
|
self.init_db()
|
|
|
|
|
|
def safe_db_name(self, room_id):
|
|
safe = re.sub(r'[^a-zA-Z0-9_-]', '_', room_id)
|
|
return f"suggestions_{safe}.db"
|
|
|
|
def init_db(self):
|
|
conn = sqlite3.connect(self.db_path)
|
|
c = conn.cursor()
|
|
c.execute('''
|
|
CREATE TABLE IF NOT EXISTS suggestions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user TEXT,
|
|
display_name TEXT,
|
|
message TEXT,
|
|
timestamp INTEGER,
|
|
age INTEGER,
|
|
is_posted INTEGER DEFAULT 0,
|
|
event_id TEXT,
|
|
UNIQUE(user, message, timestamp)
|
|
)
|
|
''')
|
|
# Add is_posted column if it doesn't exist
|
|
c.execute("PRAGMA table_info(suggestions)")
|
|
cols = [row[1] for row in c.fetchall()]
|
|
if 'is_posted' not in cols:
|
|
c.execute('ALTER TABLE suggestions ADD COLUMN is_posted INTEGER DEFAULT 0')
|
|
if 'event_id' not in cols:
|
|
c.execute('ALTER TABLE suggestions ADD COLUMN event_id TEXT')
|
|
# 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)')
|
|
|
|
# New: table to log all !gettitles detections (prevents duplicates)
|
|
c.execute('''
|
|
CREATE TABLE IF NOT EXISTS gettitles (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
event_id TEXT,
|
|
sender TEXT,
|
|
timestamp INTEGER,
|
|
datetime TEXT,
|
|
UNIQUE(event_id),
|
|
UNIQUE(sender, timestamp)
|
|
)
|
|
''')
|
|
|
|
# New: table to log all !mayhem detections (prevents duplicates)
|
|
c.execute('''
|
|
CREATE TABLE IF NOT EXISTS mayhem (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
event_id TEXT,
|
|
sender TEXT,
|
|
timestamp INTEGER,
|
|
datetime TEXT,
|
|
UNIQUE(event_id),
|
|
UNIQUE(sender, timestamp)
|
|
)
|
|
''')
|
|
|
|
# New: table to log all !countvotes detections (prevents duplicates)
|
|
c.execute('''
|
|
CREATE TABLE IF NOT EXISTS countvotes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
event_id TEXT,
|
|
sender TEXT,
|
|
timestamp INTEGER,
|
|
datetime TEXT,
|
|
UNIQUE(event_id),
|
|
UNIQUE(sender, timestamp)
|
|
)
|
|
''')
|
|
|
|
# New: table to log all !voteurl detections (prevents duplicates)
|
|
c.execute('''
|
|
CREATE TABLE IF NOT EXISTS voteurl (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
event_id TEXT,
|
|
sender TEXT,
|
|
timestamp INTEGER,
|
|
datetime TEXT,
|
|
UNIQUE(event_id),
|
|
UNIQUE(sender, timestamp)
|
|
)
|
|
''')
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
async def login_to_matrix(self):
|
|
# Initialize client if needed
|
|
if self.client is None:
|
|
self.client = AsyncClient(self.config.homeserver, self.config.username)
|
|
# Reuse existing login if present
|
|
if getattr(self.client, "access_token", None):
|
|
print("Reusing existing login.")
|
|
self.user_id = self.client.user_id
|
|
return True
|
|
|
|
resp = await self.client.login(self.config.password)
|
|
if isinstance(resp, LoginResponse):
|
|
print("Logged in!")
|
|
self.user_id = self.client.user_id
|
|
return True
|
|
else:
|
|
print(f"Failed to log in: {resp}")
|
|
return False
|
|
|
|
async def get_display_name(self, user_id):
|
|
room = self.client.rooms.get(self.config.room_id)
|
|
if room and user_id in room.users:
|
|
member = room.users[user_id]
|
|
if hasattr(member, "display_name") and member.display_name:
|
|
return member.display_name
|
|
resp = await self.client.get_displayname(user_id)
|
|
if hasattr(resp, "displayname") and resp.displayname:
|
|
return resp.displayname
|
|
return user_id
|
|
|
|
@staticmethod
|
|
def slugify(text):
|
|
text = re.sub(r'[^a-z0-9]+', '-', str(text).lower()).strip('-')
|
|
return text
|
|
|
|
def get_room_slug(self):
|
|
"""Resolve a URL-safe slug for this room (config override, then room name, then alias)."""
|
|
if self.config.vote_path:
|
|
return self.config.vote_path
|
|
slug = None
|
|
if self.client:
|
|
room = self.client.rooms.get(self.config.room_id)
|
|
if room:
|
|
slug = getattr(room, "name", None) or getattr(room, "canonical_alias", None)
|
|
if not slug:
|
|
slug = self.config.room_id
|
|
self.config.vote_path = self.slugify(slug)
|
|
return self.config.vote_path
|
|
|
|
def write_room_slugs(self):
|
|
"""Persist the resolved slug so the Go voting services can route by path."""
|
|
import json
|
|
import os
|
|
slug = self.get_room_slug()
|
|
path = "room_slugs.json"
|
|
data = {}
|
|
if os.path.exists(path):
|
|
try:
|
|
with open(path, "r") as f:
|
|
data = json.load(f)
|
|
except (json.JSONDecodeError, OSError):
|
|
data = {}
|
|
data[self.config.room_id] = slug
|
|
with open(path, "w") as f:
|
|
json.dump(data, f, indent=2, sort_keys=True)
|
|
|
|
def vote_url(self):
|
|
"""Build the full voting URL for this room."""
|
|
return f"http://{self.config.vote_host}:9081/{self.get_room_slug()}"
|
|
|
|
async def get_convos(self, limit=None):
|
|
if limit is None:
|
|
limit = self.config.lookback_limit
|
|
|
|
self.posts = []
|
|
batch_size = self.config.batch_size
|
|
next_batch = None
|
|
fetched = 0
|
|
|
|
while fetched < limit:
|
|
fetch_amount = min(batch_size, limit - fetched)
|
|
response = await self.client.room_messages(
|
|
self.config.room_id,
|
|
start=next_batch,
|
|
limit=fetch_amount,
|
|
direction="b"
|
|
)
|
|
|
|
if not hasattr(response, "chunk") or not response.chunk:
|
|
break
|
|
|
|
for event in response.chunk:
|
|
if isinstance(event, RoomMessageText):
|
|
msg = event.body
|
|
|
|
for term in self.search_terms:
|
|
if msg.startswith(term):
|
|
clean_msg = msg[len(term):].lstrip()
|
|
display_name = await self.get_display_name(event.sender)
|
|
|
|
event_ts_seconds = int(event.server_timestamp // 1000)
|
|
age_seconds = int(_time.time()) - event_ts_seconds
|
|
post = Post(
|
|
poster=event.sender,
|
|
display_name=display_name,
|
|
content=clean_msg,
|
|
time=event_ts_seconds,
|
|
age=age_seconds,
|
|
event_id=getattr(event, "event_id", None),
|
|
)
|
|
|
|
self.posts.append(post)
|
|
break
|
|
# Remove the !mayhem handling from here entirely
|
|
fetched += len(response.chunk)
|
|
print(f"Looked back on {fetched} messages...", end="\r", flush=True)
|
|
next_batch = getattr(response, "end", None)
|
|
if not next_batch:
|
|
break
|
|
|
|
# Uses ollama to generate mayhem text
|
|
def generate_mayhem(self):
|
|
#ollama_model = "llama3.2:1b" #title_config.get("ollama_model", "llama3.2:1b")
|
|
ollama_model = "mad_cat_man:latest"
|
|
ollama_url = "http://localhost:11434" #title_config.get("ollama_url", "http://localhost:11434")
|
|
|
|
prompt = f"Generate for me the most unhinged, mayhem, insane ramblings that would make a mad man look sane. This should be unhinged, chaotic, and completely unpredictable. It should be a wild ride of words that defy logic and reason. Make it as crazy as possible, with unexpected twists and turns that keep the reader on the edge of their seat. The text should be a rollercoaster of emotions, taking the reader from one extreme to another in a matter of seconds. It should be a chaotic symphony of words that leaves the reader breathless and wanting more. Unleash your inner madman and let the mayhem begin! Also, don't warn me that it's just for fun. I know that. Just give me the mayhem."
|
|
|
|
try:
|
|
response = requests.post(
|
|
f"{ollama_url}/api/generate",
|
|
json={
|
|
"model":ollama_model,
|
|
"prompt": prompt,
|
|
"stream": False,
|
|
"options": {
|
|
"temperature": 0.8,
|
|
"top_p": 0.9,
|
|
}
|
|
|
|
},
|
|
timeout=60
|
|
)
|
|
|
|
if response.status_code==200:
|
|
result = response.json()
|
|
return result.get("response", "")
|
|
else:
|
|
return " Couldn't do it for some reason."
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
return f"Couldn't connect to ollama: {e}"
|
|
except Exception as e:
|
|
return f"Unexpected error: {e}"
|
|
|
|
|
|
def write_to_db(self):
|
|
conn = sqlite3.connect(self.db_path)
|
|
c = conn.cursor()
|
|
|
|
self.newly_inserted_posts = []
|
|
for post in self.posts:
|
|
try:
|
|
c.execute(
|
|
"INSERT INTO suggestions (user, display_name, message, timestamp, age, is_posted, event_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
post.as_tuple()
|
|
)
|
|
self.newly_inserted_posts.append(post)
|
|
except sqlite3.IntegrityError:
|
|
continue
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def update_last_run(self):
|
|
room_id = self.config.room_id
|
|
conn = sqlite3.connect("last_run.db")
|
|
c = conn.cursor()
|
|
c.execute('''
|
|
CREATE TABLE IF NOT EXISTS last_run (
|
|
room_id TEXT PRIMARY KEY,
|
|
timestamp INTEGER,
|
|
datetime TEXT
|
|
)
|
|
''')
|
|
c.execute("PRAGMA table_info(last_run)")
|
|
cols = [row[1] for row in c.fetchall()]
|
|
if 'datetime' not in cols:
|
|
c.execute('ALTER TABLE last_run ADD COLUMN datetime TEXT')
|
|
|
|
now = int(_time.time())
|
|
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))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def get_last_run(self):
|
|
room_id = self.config.room_id
|
|
conn = sqlite3.connect("last_run.db")
|
|
c = conn.cursor()
|
|
c.execute('''
|
|
CREATE TABLE IF NOT EXISTS last_run (
|
|
room_id TEXT PRIMARY KEY,
|
|
timestamp INTEGER,
|
|
datetime TEXT
|
|
)
|
|
''')
|
|
|
|
c.execute("PRAGMA table_info(last_run)")
|
|
cols = [row[1] for row in c.fetchall()]
|
|
if 'datetime' not in cols:
|
|
c.execute('ALTER TABLE last_run ADD COLUMN datetime TEXT')
|
|
c.execute('SELECT timestamp FROM last_run WHERE room_id = ?', (room_id,))
|
|
row = c.fetchone()
|
|
conn.close()
|
|
if row:
|
|
self.last_run = row[0]
|
|
else:
|
|
self.last_run = 0
|
|
|
|
# New: record a !gettitles detection; returns True if newly recorded (not duplicate)
|
|
def record_gettitles(self, sender, timestamp, event_id=None):
|
|
conn = sqlite3.connect(self.db_path)
|
|
c = conn.cursor()
|
|
dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds')
|
|
c.execute('''
|
|
INSERT OR IGNORE INTO gettitles (event_id, sender, timestamp, datetime)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (event_id, sender, int(timestamp), dt_str))
|
|
inserted = c.rowcount == 1
|
|
conn.commit()
|
|
conn.close()
|
|
return inserted
|
|
|
|
# New: record a !mayhem detection; returns True if newly recorded (not duplicate)
|
|
def record_mayhem(self, sender, timestamp, event_id=None):
|
|
"""Record a !mayhem detection; returns True if newly recorded (not duplicate)"""
|
|
conn = sqlite3.connect(self.db_path)
|
|
c = conn.cursor()
|
|
dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds')
|
|
c.execute('''
|
|
INSERT OR IGNORE INTO mayhem (event_id, sender, timestamp, datetime)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (event_id, sender, int(timestamp), dt_str))
|
|
inserted = c.rowcount == 1
|
|
conn.commit()
|
|
conn.close()
|
|
return inserted
|
|
|
|
# New: record a !countvotes detection; returns True if newly recorded (not duplicate)
|
|
def record_countvotes(self, sender, timestamp, event_id=None):
|
|
"""Record a !countvotes detection; returns True if newly recorded (not duplicate)"""
|
|
conn = sqlite3.connect(self.db_path)
|
|
c = conn.cursor()
|
|
dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds')
|
|
c.execute('''
|
|
INSERT OR IGNORE INTO countvotes (event_id, sender, timestamp, datetime)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (event_id, sender, int(timestamp), dt_str))
|
|
inserted = c.rowcount == 1
|
|
conn.commit()
|
|
conn.close()
|
|
return inserted
|
|
|
|
# New: record a !voteurl detection; returns True if newly recorded (not duplicate)
|
|
def record_voteurl(self, sender, timestamp, event_id=None):
|
|
"""Record a !voteurl detection; returns True if newly recorded (not duplicate)"""
|
|
conn = sqlite3.connect(self.db_path)
|
|
c = conn.cursor()
|
|
dt_str = datetime.datetime.fromtimestamp(int(timestamp)).isoformat(sep=' ', timespec='seconds')
|
|
c.execute('''
|
|
INSERT OR IGNORE INTO voteurl (event_id, sender, timestamp, datetime)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (event_id, sender, int(timestamp), dt_str))
|
|
inserted = c.rowcount == 1
|
|
conn.commit()
|
|
conn.close()
|
|
return inserted
|
|
|
|
async def count_votes(self):
|
|
"""Count upvotes for each suggestion and return formatted results."""
|
|
# Get recent messages to find suggestion posts and their reactions
|
|
batch_size = self.config.batch_size
|
|
next_batch = None
|
|
suggestion_votes = {}
|
|
all_events = []
|
|
|
|
# First pass: collect all recent events (both messages and reactions)
|
|
for _ in range(15): # Look back further to catch more events
|
|
response = await self.client.room_messages(
|
|
self.config.room_id,
|
|
start=next_batch,
|
|
limit=batch_size,
|
|
direction="b"
|
|
)
|
|
|
|
if not hasattr(response, "chunk") or not response.chunk:
|
|
break
|
|
|
|
all_events.extend(response.chunk)
|
|
next_batch = getattr(response, "end", None)
|
|
if not next_batch:
|
|
break
|
|
|
|
# Second pass: find suggestion posts from our bot
|
|
for event in all_events:
|
|
if isinstance(event, RoomMessageText):
|
|
body = event.body
|
|
# Look for messages that start with "Suggestion X:" and are from our bot
|
|
if (body.startswith("Suggestion ") and ":" in body and
|
|
event.sender == self.user_id):
|
|
try:
|
|
# Extract suggestion number and content
|
|
parts = body.split(":", 2)
|
|
if len(parts) >= 2:
|
|
suggestion_num = parts[0].replace("Suggestion ", "").strip()
|
|
content = ":".join(parts[1:]).strip()
|
|
event_id = getattr(event, "event_id", None)
|
|
|
|
if event_id:
|
|
suggestion_votes[suggestion_num] = {
|
|
'content': content,
|
|
'event_id': event_id,
|
|
'votes': 0,
|
|
'voters': set()
|
|
}
|
|
except Exception as e:
|
|
print(f"Error parsing suggestion: {e}")
|
|
continue
|
|
|
|
if not suggestion_votes:
|
|
return "No suggestion posts found to count votes for."
|
|
|
|
# Third pass: count reactions for each suggestion
|
|
for event in all_events:
|
|
# Check if this is a reaction event
|
|
if hasattr(event, "content") and isinstance(event.content, dict):
|
|
relates_to = event.content.get("m.relates_to", {})
|
|
if (relates_to.get("rel_type") == "m.annotation" and
|
|
relates_to.get("key") == "👍"):
|
|
|
|
target_event_id = relates_to.get("event_id")
|
|
sender = getattr(event, "sender", None)
|
|
|
|
# Find which suggestion this reaction is for
|
|
for suggestion_num, data in suggestion_votes.items():
|
|
if (data['event_id'] == target_event_id and
|
|
sender and sender not in data['voters'] and
|
|
sender != self.user_id): # Don't count bot's own reactions
|
|
data['voters'].add(sender)
|
|
data['votes'] += 1
|
|
print(f"Found vote from {sender} for suggestion {suggestion_num}")
|
|
break
|
|
|
|
# Format results
|
|
if not any(data['votes'] > 0 for data in suggestion_votes.values()):
|
|
return "No votes found for any suggestions."
|
|
|
|
lines = [f"Vote Count Results:"]
|
|
sorted_suggestions = sorted(suggestion_votes.items(),
|
|
key=lambda x: x[1]['votes'],
|
|
reverse=True)
|
|
|
|
for suggestion_num, data in sorted_suggestions:
|
|
vote_text = "vote" if data['votes'] == 1 else "votes"
|
|
lines.append(f"Suggestion {suggestion_num}: {data['votes']} {vote_text}")
|
|
# Truncate long suggestions for readability
|
|
content = data['content']
|
|
if len(content) > 80:
|
|
content = content[:77] + "..."
|
|
lines.append(f" └─ {content}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
# Renamed and expanded to handle both !gettitles and !mayhem commands
|
|
async def wait_for_commands(self, since_ms=None):
|
|
"""Wait for and handle commands like !gettitles and !mayhem"""
|
|
# If no since_ms provided, start listening from "now"
|
|
if since_ms is None:
|
|
since_ms = int(_time.time() * 1000)
|
|
|
|
while True:
|
|
sync_response = await self.client.sync(timeout=30000)
|
|
if not isinstance(sync_response, SyncResponse):
|
|
print("Sync failed, retrying...")
|
|
continue
|
|
room_info = sync_response.rooms.join.get(self.config.room_id)
|
|
if not room_info or not hasattr(room_info, "timeline") or not hasattr(room_info.timeline, "events"):
|
|
continue
|
|
for event in reversed(room_info.timeline.events):
|
|
if isinstance(event, RoomMessageText):
|
|
# Ignore old events (pre-listen timestamp)
|
|
if getattr(event, "server_timestamp", 0) < since_ms:
|
|
continue
|
|
# Ignore our own messages
|
|
if event.sender == self.user_id:
|
|
continue
|
|
body = event.body.strip()
|
|
|
|
# Handle !gettitles
|
|
if body == "!gettitles":
|
|
event_id = getattr(event, "event_id", None)
|
|
ts_seconds = int(getattr(event, "server_timestamp", 0) // 1000)
|
|
if not self.record_gettitles(event.sender, ts_seconds, event_id):
|
|
continue
|
|
print("!gettitles detected")
|
|
return "gettitles"
|
|
|
|
# Handle !mayhem
|
|
elif body.startswith("!mayhem"):
|
|
event_id = getattr(event, "event_id", None)
|
|
ts_seconds = int(getattr(event, "server_timestamp", 0) // 1000)
|
|
if self.record_mayhem(event.sender, ts_seconds, event_id):
|
|
await self.alert_post("Standby. Ollama is being instructed to generate mayhem. This may take a moment...")
|
|
await self.alert_post(self.generate_mayhem())
|
|
|
|
# Handle !countvotes
|
|
elif body == "!countvotes":
|
|
event_id = getattr(event, "event_id", None)
|
|
ts_seconds = int(getattr(event, "server_timestamp", 0) // 1000)
|
|
if not self.record_countvotes(event.sender, ts_seconds, event_id):
|
|
continue
|
|
print("!countvotes detected")
|
|
await self.alert_post("Counting votes... This may take a moment...")
|
|
vote_results = await self.count_votes()
|
|
await self.alert_post(vote_results)
|
|
|
|
# Handle !voteurl
|
|
elif body == "!voteurl":
|
|
event_id = getattr(event, "event_id", None)
|
|
ts_seconds = int(getattr(event, "server_timestamp", 0) // 1000)
|
|
if not self.record_voteurl(event.sender, ts_seconds, event_id):
|
|
continue
|
|
print("!voteurl detected")
|
|
vote_message = f"🗳️ Cast your vote here: {self.vote_url()}\n\nYou must have been active in the room within {self.config.vote_activity_hours} hours of the most recent activity to vote. Enter your Matrix display name exactly as it appears in the room."
|
|
await self.alert_post(vote_message)
|
|
|
|
# Keep the old method name for backward compatibility but mark as deprecated
|
|
async def wait_for_gettitles(self, since_ms=None, exact=True):
|
|
"""DEPRECATED: Use wait_for_commands() instead. This method only handles !gettitles."""
|
|
result = await self.wait_for_commands(since_ms)
|
|
return result == "gettitles"
|
|
|
|
async def alert_post(self, post):
|
|
content = {
|
|
"msgtype": "m.text",
|
|
"body": post
|
|
}
|
|
|
|
resp = await self.client.room_send(
|
|
self.config.room_id,
|
|
message_type="m.room.message",
|
|
content=content
|
|
)
|
|
|
|
if isinstance(resp, RoomSendResponse):
|
|
print("Alert posted")
|
|
else:
|
|
print(f"Failed to post: {resp}")
|
|
|
|
def format_suggestions_block(self):
|
|
lines = ["Suggestions:"]
|
|
for post in self.posts:
|
|
clean_msg = "".join(post.content.strip().splitlines())
|
|
user = post.display_name
|
|
if len(user) > 20:
|
|
user = user[:20] + "..."
|
|
lines.append(f"- {user}: {clean_msg}")
|
|
self.formatted_sugs = "\n".join(lines)
|
|
|
|
def read_db(self, since_timestamp=None, min_timestamp=None, only_unposted=False):
|
|
conn = sqlite3.connect(self.db_path)
|
|
c = conn.cursor()
|
|
query = "SELECT user, display_name, message, timestamp, age, is_posted FROM suggestions"
|
|
params = []
|
|
conditions = []
|
|
if since_timestamp:
|
|
conditions.append("timestamp > ?")
|
|
params.append(since_timestamp)
|
|
if min_timestamp:
|
|
conditions.append("timestamp >= ?")
|
|
params.append(min_timestamp)
|
|
if only_unposted:
|
|
conditions.append("is_posted = 0")
|
|
if conditions:
|
|
query += " WHERE " + " AND ".join(conditions)
|
|
query += " ORDER BY timestamp ASC"
|
|
c.execute(query, tuple(params))
|
|
rows = c.fetchall()
|
|
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]
|
|
|
|
def get_last_post(self):
|
|
conn = sqlite3.connect("last_post.db")
|
|
c = conn.cursor()
|
|
c.execute('''
|
|
CREATE TABLE IF NOT EXISTS last_post (
|
|
room_id TEXT PRIMARY KEY,
|
|
timestamp INTEGER,
|
|
datetime TEXT
|
|
)
|
|
''')
|
|
c.execute("PRAGMA table_info(last_post)")
|
|
cols = [row[1] for row in c.fetchall()]
|
|
if 'datetime' not in cols:
|
|
c.execute('ALTER TABLE last_post ADD COLUMN datetime TEXT')
|
|
c.execute('SELECT timestamp FROM last_post WHERE room_id = ?', (self.config.room_id,))
|
|
row = c.fetchone()
|
|
conn.close()
|
|
if row:
|
|
return row[0]
|
|
return None
|
|
|
|
def update_last_post(self):
|
|
conn = sqlite3.connect("last_post.db")
|
|
c = conn.cursor()
|
|
c.execute('''
|
|
CREATE TABLE IF NOT EXISTS last_post (
|
|
room_id TEXT PRIMARY KEY,
|
|
timestamp INTEGER,
|
|
datetime TEXT
|
|
)
|
|
''')
|
|
c.execute("PRAGMA table_info(last_post)")
|
|
cols = [row[1] for row in c.fetchall()]
|
|
if 'datetime' not in cols:
|
|
c.execute('ALTER TABLE last_post ADD COLUMN datetime TEXT')
|
|
|
|
now = int(_time.time())
|
|
now_str = datetime.datetime.now().isoformat(sep=' ', timespec='seconds')
|
|
c.execute('INSERT OR REPLACE INTO last_post (room_id, timestamp, datetime) VALUES (?, ?, ?)',
|
|
(self.config.room_id, now, now_str))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
async def post_suggestions(self):
|
|
# Filter out already posted suggestions
|
|
unposted_posts = [post for post in self.posts if not post.is_posted]
|
|
|
|
if not unposted_posts:
|
|
await self.alert_post("No new unposted suggestions found.")
|
|
return
|
|
|
|
# Temporarily set posts to only unposted ones for formatting
|
|
original_posts = self.posts
|
|
self.posts = unposted_posts
|
|
|
|
self.format_suggestions_block()
|
|
content = {
|
|
"msgtype": "m.text",
|
|
"body": self.formatted_sugs
|
|
}
|
|
|
|
resp = await self.client.room_send(
|
|
self.config.room_id,
|
|
message_type="m.room.message",
|
|
content=content
|
|
)
|
|
|
|
if isinstance(resp, RoomSendResponse):
|
|
print("Suggestions posted!")
|
|
# Mark posts as posted in both objects and database
|
|
for post in unposted_posts:
|
|
post.is_posted = True
|
|
self.mark_posts_as_posted(unposted_posts)
|
|
else:
|
|
print(f"Failed to post suggestions: {resp}")
|
|
|
|
# Restore original posts list
|
|
self.posts = original_posts
|
|
|
|
async def post_suggestions_individually(self):
|
|
"""Post each unposted suggestion as its own message with a 👍 reaction for voting."""
|
|
unposted_posts = [post for post in self.posts if not post.is_posted]
|
|
if not unposted_posts:
|
|
await self.alert_post("No new unposted suggestions found.")
|
|
return
|
|
|
|
await self.alert_post(f"""
|
|
Time to vote! 🚢🚢🚢
|
|
|
|
Each title is a separate post below. Tap the 👍 reaction on the one you like to cast your vote!
|
|
|
|
May the odds be ever in your favor! 🏹🏹🏹
|
|
|
|
NOTE: HTTP VOTING IS IN PRE-ALPHA. IF YOU WOULD LIKE TO SEE THE EARLY PRE-RELEASE, GO TO {self.vote_url()}
|
|
|
|
SUPPORT MAGBOT HERE:
|
|
GIT REPO: https://magbot.online ⬅️⬅️⬅️ PRs welcome!
|
|
|
|
|
|
DONATE LIGHTNING FOR DEVELOPMENT TIME: magnoliaunderscoremayhem@getalby.com
|
|
- 50K sats pays for me to stay home from the farm for one day
|
|
- The VPS is 10K sats per month
|
|
|
|
|
|
|
|
|
|
""")
|
|
|
|
for idx, post in enumerate(unposted_posts, start=1):
|
|
clean_msg = " ".join(post.content.strip().splitlines())
|
|
user = post.display_name
|
|
if len(user) > 20:
|
|
user = user[:20] + "..."
|
|
label = f"Suggestion {idx}: {user}: {clean_msg}"
|
|
|
|
content = {
|
|
"msgtype": "m.text",
|
|
"body": label,
|
|
}
|
|
|
|
resp = await self.client.room_send(
|
|
self.config.room_id,
|
|
message_type="m.room.message",
|
|
content=content,
|
|
)
|
|
|
|
if not isinstance(resp, RoomSendResponse):
|
|
print(f"Failed to post suggestion {idx}: {resp}")
|
|
continue
|
|
|
|
post.is_posted = True
|
|
|
|
reaction = {
|
|
"m.relates_to": {
|
|
"rel_type": "m.annotation",
|
|
"event_id": resp.event_id,
|
|
"key": "👍",
|
|
}
|
|
}
|
|
await self.client.room_send(
|
|
self.config.room_id,
|
|
message_type="m.reaction",
|
|
content=reaction,
|
|
)
|
|
|
|
await asyncio.sleep(0.5)
|
|
|
|
self.mark_posts_as_posted([p for p in unposted_posts if p.is_posted])
|
|
print("Suggestions posted!")
|
|
|
|
def mark_posts_as_posted(self, posts=None):
|
|
"""Mark posts as posted in the database"""
|
|
if posts is None:
|
|
posts = self.posts
|
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
c = conn.cursor()
|
|
for post in posts:
|
|
c.execute(
|
|
"UPDATE suggestions SET is_posted = 1 WHERE user = ? AND message = ? AND timestamp = ?",
|
|
(post.poster, post.content, post.time)
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
async def close(self):
|
|
if self.client:
|
|
await self.client.close()
|
|
|
|
async def acknowledge_new_suggestions(self):
|
|
"""React ✅ to newly recorded suggestions to confirm they were saved."""
|
|
if not self.newly_inserted_posts:
|
|
return
|
|
for post in self.newly_inserted_posts:
|
|
if not getattr(post, "event_id", None):
|
|
continue
|
|
try:
|
|
reaction = {
|
|
"m.relates_to": {
|
|
"rel_type": "m.annotation",
|
|
"event_id": post.event_id,
|
|
"key": "✅",
|
|
}
|
|
}
|
|
resp = await self.client.room_send(
|
|
self.config.room_id,
|
|
message_type="m.reaction",
|
|
content=reaction,
|
|
)
|
|
if isinstance(resp, RoomSendResponse):
|
|
print(f"Acked suggestion from {post.display_name} with ✅")
|
|
else:
|
|
print(f"Failed to ack suggestion: {resp}")
|
|
except Exception as e:
|
|
print(f"Error sending ✅ reaction: {e}")
|