5 Commits
Author SHA1 Message Date
nolan ca44cf73df Add nostr login and periodic vote-beta announcements 2026-09-04 11:56:39 -05:00
nolan 5dde44e98e Tweaked donation request 2026-09-04 11:19:37 -05:00
nolan f6f6721f3a Tweaked voting logic 2026-09-04 10:43:53 -05:00
nolan c71fec44d6 Fixed annoying delay issue. 2026-09-04 10:21:24 -05:00
nolan fffb9c50b4 ??? 2026-08-16 12:02:37 -04:00
42 changed files with 2119 additions and 4047001 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
config/*
.venv/
__pycache__/
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+17 -2
View File
@@ -1,5 +1,7 @@
username: "magbot"
password: "zC1b9qCKT8Cv0D"
#password: "zC1b9qCKT8Cv0D"
password: cathandle
homeserver: "https://matrix.dinguscloud.com"
room_id: "!ajyOBbqoCIbBhEqNPU:matrix.org" # Magbot Testing Space
#room_id: "!HXiyYVEZOMRtkyvarN:jupiterbroadcasting.com" # Da Lunch
#room_id: "!gJYEKNllaubNlNkFIj:jupiterbroadcasting.com" # The Main Chat
@@ -9,6 +11,19 @@ lookback_limit: 400
dump_mode: True
dbName: ""
# vote_host: "agmninex.online" # Hostname where voting interface is accessible (e.g., "localhost", "example.com", "vote.mysite.xyz")
vote_host: "agmninex.online"
vote_host: "workstation"
vote_path: "random" # URL slug for this room's voting page (http://<vote_host>:9081/<vote_path>)
vote_activity_hours: 6672 # Hours a user must have been active within to be eligible to vote
# --- Wait times (seconds) ---
sync_timeout: 3000 # Initial sync timeout for scraper/user-list (ms)
poll_interval: 10 # How often the scraper polls for new messages (s)
command_sync_timeout: 30000 # Sync timeout when listening for chat commands (ms)
post_interval: 0.2 # Delay between posting each suggestion (s)
initial_sync_timeout: 300 # Initial sync timeout for post_results (ms)
post_loop_interval: 1 # Delay between post_results loop iterations (s)
user_list_interval: 300 # How often the user-list service refreshes (s)
user_list_retry_interval: 60 # Retry delay on user-list error (s)
# --- Feature toggles ---
suggestion_announce_interval: 0 # Post a vote-beta announcement every N suggestions received (0 = disabled)
+16 -2
View File
@@ -2,8 +2,9 @@
username: "magbot"
password: "Dont tek psswrd"
# room_id: "!ajyOBbqoCIbBhEqNPU:matrix.org" # Magbot Testing Space
room_id: "!HXiyYVEZOMRtkyvarN:jupiterbroadcasting.com" # The Lunch
homeserver: "https://matrix.dinguscloud.com" # Matrix homeserver the bot connects to
#room_id: "!ajyOBbqoCIbBhEqNPU:matrix.org" # Magbot Testing Space
#room_id: "!HXiyYVEZOMRtkyvarN:jupiterbroadcasting.com" # The Lunch
lookback_seconds: 86400 # 24 hours
lookback_limit: 400 # 400 messages
dump_mode: False
@@ -11,3 +12,16 @@ dbName: "my_db_name"
vote_host: "localhost" # Hostname where voting interface is accessible (e.g., "localhost", "example.com", "vote.mysite.xyz")
# vote_path: "my-room-slug" # Optional URL path for this room's voting page (auto-derived from the room name if unset)
vote_activity_hours: 72 # Hours a user must have been active within to be eligible to vote
# --- Wait times (seconds) ---
sync_timeout: 3000 # Initial sync timeout for scraper/user-list (ms)
poll_interval: 30 # How often the scraper polls for new messages (s)
command_sync_timeout: 30000 # Sync timeout when listening for chat commands (ms)
post_interval: 0.5 # Delay between posting each suggestion (s)
initial_sync_timeout: 300 # Initial sync timeout for post_results (ms)
post_loop_interval: 1 # Delay between post_results loop iterations (s)
user_list_interval: 300 # How often the user-list service refreshes (s)
user_list_retry_interval: 60 # Retry delay on user-list error (s)
# --- Feature toggles ---
suggestion_announce_interval: 0 # Post a vote-beta announcement every N suggestions received (0 = disabled)
@@ -1,5 +1,6 @@
username: "magbot"
password: "zC1b9qCKT8Cv0D"
homeserver: "https://jupiterbroadcasting.com"
#room_id: "!ajyOBbqoCIbBhEqNPU:matrix.org" # Magbot Testing Space
room_id: "!HXiyYVEZOMRtkyvarN:jupiterbroadcasting.com" # Da Lunch
#room_id: "!gJYEKNllaubNlNkFIj:jupiterbroadcasting.com" # The Main Chat
@@ -11,3 +12,4 @@ dbName: ""
vote_host: "agmninex.online"
vote_path: "lunch" # URL slug for this room's voting page (http://<vote_host>:9081/<vote_path>)
vote_activity_hours: 6672 # Hours a user must have been active within to be eligible to vote
suggestion_announce_interval: 0 # Post a vote-beta announcement every N suggestions received (0 = disabled)
@@ -1,5 +1,6 @@
username: "magbot"
password: "zC1b9qCKT8Cv0D"
homeserver: "https://jupiterbroadcasting.com"
#room_id: "!ajyOBbqoCIbBhEqNPU:matrix.org" # Magbot Testing Space
#room_id: "!HXiyYVEZOMRtkyvarN:jupiterbroadcasting.com" # Da Lunch
room_id: "!gJYEKNllaubNlNkFIj:jupiterbroadcasting.com" # The Main Chat
@@ -11,3 +12,4 @@ dbName: ""
vote_host: "agmninex.online"
vote_path: "jb-chat" # URL slug for this room's voting page (http://<vote_host>:9081/<vote_path>)
vote_activity_hours: 6672 # Hours a user must have been active within to be eligible to vote
suggestion_announce_interval: 0 # Post a vote-beta announcement every N suggestions received (0 = disabled)
+13 -12
View File
@@ -9,7 +9,7 @@ import asyncio
import sqlite3
import time
import sys
from nio import AsyncClient, LoginResponse, RoomMessagesResponse, SyncResponse
from nio import AsyncClient, LoginResponse, RoomMessageText, RoomMessagesResponse, SyncResponse
from room import Room
import dbutil
@@ -79,7 +79,8 @@ class UserListManager:
return []
# Sync to get room state
sync_resp = await self.room.client.sync(timeout=3000)
assert self.room.client is not None
sync_resp = await self.room.client.sync(timeout=self.room.config.sync_timeout)
if not isinstance(sync_resp, SyncResponse):
print(f"Sync failed: {sync_resp}")
@@ -119,6 +120,7 @@ class UserListManager:
fetched = 0
# Fetch messages in batches to count posts per user
assert self.room.client is not None
while fetched < lookback_limit:
try:
response = await self.room.client.room_messages(
@@ -133,18 +135,17 @@ class UserListManager:
fetched += len(response.chunk)
for event in response.chunk:
if hasattr(event, 'sender') and hasattr(event, 'body'):
if isinstance(event, RoomMessageText):
sender = event.sender
post_counts[sender] = post_counts.get(sender, 0) + 1
# Track the most recent message timestamp for each user
if hasattr(event, 'server_timestamp'):
event_time = int(event.server_timestamp // 1000)
if sender not in last_activity or event_time > last_activity[sender]:
last_activity[sender] = event_time
# Track the absolute most recent post in the room
if event_time > most_recent_post:
most_recent_post = event_time
event_time = int(event.server_timestamp // 1000)
if sender not in last_activity or event_time > last_activity[sender]:
last_activity[sender] = event_time
# Track the absolute most recent post in the room
if event_time > most_recent_post:
most_recent_post = event_time
next_batch = response.end
@@ -268,12 +269,12 @@ async def main():
# Wait 5 minutes before next update
print("\nWaiting 5 minutes before next update...")
await asyncio.sleep(300) # 5 minutes
await asyncio.sleep(manager.room.config.user_list_interval)
except Exception as e:
print(f"Error during user list update: {e}")
print("Retrying in 1 minute...")
await asyncio.sleep(60) # Wait 1 minute on error before retry
await asyncio.sleep(manager.room.config.user_list_retry_interval) # Wait 1 minute on error before retry
except KeyboardInterrupt:
print("\nShutting down user list service...")
+5 -2
View File
@@ -39,7 +39,10 @@ async def main():
if command:
print("!getvotes command detected, posting vote results...")
# Clear the web poll by marking all pending suggestions as posted
room.mark_submissions_posted()
# Read and format vote results
vote_message = format_vote_results(room)
@@ -111,7 +114,7 @@ async def wait_for_getvotes(room, since_ms):
"""Wait for !getvotes command in the chat."""
from nio import SyncResponse, RoomMessageText
sync_response = await room.client.sync(timeout=30000)
sync_response = await room.client.sync(timeout=room.config.command_sync_timeout)
if not isinstance(sync_response, SyncResponse):
return None
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
-1318672
View File
File diff suppressed because it is too large Load Diff
+18 -30363
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+38 -7663
View File
File diff suppressed because it is too large Load Diff
+18 -30363
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -35,7 +35,8 @@ async def main():
return
# Initial sync establishes the next-batch; after this, listen from "now"
await room.client.sync(timeout=300)
assert room.client is not None
await room.client.sync(timeout=room.config.initial_sync_timeout)
room.write_room_slugs()
try:
@@ -59,7 +60,7 @@ async def main():
room.update_last_post()
else:
await room.alert_post("No new unposted suggestions found.")
await asyncio.sleep(1)
await asyncio.sleep(room.config.post_loop_interval)
finally:
await room.close()
+6
View File
@@ -0,0 +1,6 @@
{
"venvPath": ".",
"venv": ".venv",
"pythonVersion": "3.14",
"typeCheckingMode": "basic"
}
+179 -49
View File
@@ -10,7 +10,7 @@ import yaml
import sqlite3
import re
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
import requests
import dbutil
@@ -35,6 +35,15 @@ class Config:
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", "")
self.sync_timeout = config.get("sync_timeout", 3000)
self.poll_interval = config.get("poll_interval", 30)
self.command_sync_timeout = config.get("command_sync_timeout", 30000)
self.post_interval = config.get("post_interval", 0.5)
self.initial_sync_timeout = config.get("initial_sync_timeout", 300)
self.post_loop_interval = config.get("post_loop_interval", 1)
self.user_list_interval = config.get("user_list_interval", 300)
self.user_list_retry_interval = config.get("user_list_retry_interval", 60)
self.suggestion_announce_interval = config.get("suggestion_announce_interval", 0)
class Room:
def __init__(self, config_path="configs/config.yaml", search_terms=None, homeserver=None):
@@ -43,18 +52,23 @@ class Room:
self.formatted_sugs = ""
self.posts = []
self.newly_inserted_posts = []
self.rejected_banned_posts = []
self.client = None
self.room = None
self.user_id = None
self.member = None
self.display_name = None
self.last_run = 0
self._display_name_cache = {}
if homeserver:
self.config.homeserver = homeserver
self.db_path = self.safe_db_name(self.config.room_id)
self.banned_db_path = "banned_users.db"
self.init_db()
self.init_banned_db()
self.init_suggestion_counter_db()
def safe_db_name(self, room_id):
@@ -64,7 +78,7 @@ class Room:
def init_db(self):
conn = dbutil.connect(self.db_path)
c = conn.cursor()
c.execute('''
_ = c.execute('''
CREATE TABLE IF NOT EXISTS suggestions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user TEXT,
@@ -78,17 +92,17 @@ class Room:
)
''')
# 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()]
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:
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
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)
c.execute('''
_ = c.execute('''
CREATE TABLE IF NOT EXISTS gettitles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT,
@@ -101,7 +115,7 @@ class Room:
''')
# New: table to log all !mayhem detections (prevents duplicates)
c.execute('''
_ = c.execute('''
CREATE TABLE IF NOT EXISTS mayhem (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT,
@@ -114,7 +128,7 @@ class Room:
''')
# New: table to log all !countvotes detections (prevents duplicates)
c.execute('''
_ = c.execute('''
CREATE TABLE IF NOT EXISTS countvotes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT,
@@ -127,7 +141,7 @@ class Room:
''')
# New: table to log all !voteurl detections (prevents duplicates)
c.execute('''
_ = c.execute('''
CREATE TABLE IF NOT EXISTS voteurl (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT,
@@ -141,6 +155,91 @@ class Room:
dbutil.commit(conn)
conn.close()
def init_banned_db(self):
"""Initialize the shared banned-users database (primary key + user string)."""
conn = dbutil.connect(self.banned_db_path)
c = conn.cursor()
_ = c.execute('''
CREATE TABLE IF NOT EXISTS banned_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user TEXT UNIQUE
)
''')
dbutil.commit(conn)
conn.close()
def is_banned(self, user):
"""Return True if the given Matrix account/homeserver string is banned."""
if not user:
return False
conn = dbutil.connect(self.banned_db_path)
c = conn.cursor()
_ = c.execute("SELECT 1 FROM banned_users WHERE user = ?", (user,))
row = c.fetchone()
conn.close()
return row is not None
def ban_user(self, user):
"""Add a Matrix account/homeserver string to the banned list."""
conn = dbutil.connect(self.banned_db_path)
c = conn.cursor()
_ = c.execute("INSERT OR IGNORE INTO banned_users (user) VALUES (?)", (user,))
dbutil.commit(conn)
conn.close()
def unban_user(self, user):
"""Remove a Matrix account/homeserver string from the banned list."""
conn = dbutil.connect(self.banned_db_path)
c = conn.cursor()
_ = c.execute("DELETE FROM banned_users WHERE user = ?", (user,))
dbutil.commit(conn)
conn.close()
def init_suggestion_counter_db(self):
"""Initialize the suggestion counter table for periodic announcements."""
conn = dbutil.connect(self.db_path)
c = conn.cursor()
_ = c.execute('''
CREATE TABLE IF NOT EXISTS suggestion_counter (
id INTEGER PRIMARY KEY CHECK (id = 1),
count INTEGER DEFAULT 0
)
''')
_ = c.execute('INSERT OR IGNORE INTO suggestion_counter (id, count) VALUES (1, 0)')
dbutil.commit(conn)
conn.close()
def get_suggestion_counter(self):
conn = dbutil.connect(self.db_path)
c = conn.cursor()
_ = c.execute('SELECT count FROM suggestion_counter WHERE id = 1')
row = c.fetchone()
conn.close()
return row[0] if row else 0
def increment_suggestion_counter(self, amount=1):
conn = dbutil.connect(self.db_path)
c = conn.cursor()
_ = c.execute('UPDATE suggestion_counter SET count = count + ? WHERE id = 1', (amount,))
dbutil.commit(conn)
conn.close()
return self.get_suggestion_counter()
async def check_and_announce_vote_beta(self, new_count):
"""Post a beta announcement if the suggestion count hits the configured interval."""
interval = self.config.suggestion_announce_interval
if interval <= 0:
return
if new_count > 0 and new_count % interval == 0:
url = self.vote_url()
await self.alert_post(
f"Psst! HTTP voting is now in beta! You can vote on titles at:\n{url}"
)
async def reject_banned_suggestion(self, post):
"""Reply to a banned user's suggestion and do not insert it."""
await self.alert_post(f"{post.display_name}: this user has been banned from submitting titles")
async def login_to_matrix(self):
# Initialize client if needed
if self.client is None:
@@ -161,15 +260,21 @@ class Room:
return False
async def get_display_name(self, user_id):
if user_id in self._display_name_cache:
return self._display_name_cache[user_id]
assert self.client is not None
name = 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
name = member.display_name
if name == user_id:
resp = await self.client.get_displayname(user_id)
if isinstance(resp, ProfileGetDisplayNameResponse) and resp.displayname:
name = resp.displayname
self._display_name_cache[user_id] = name
return name
@staticmethod
def slugify(text):
@@ -219,6 +324,8 @@ class Room:
batch_size = self.config.batch_size
next_batch = None
fetched = 0
self.get_last_run()
assert self.client is not None
while fetched < limit:
fetch_amount = min(batch_size, limit - fetched)
@@ -226,13 +333,17 @@ class Room:
self.config.room_id,
start=next_batch,
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
for event in response.chunk:
# If we've reached messages older than the last run, stop (incremental resume)
if self.last_run and getattr(event, "server_timestamp", 0) < self.last_run:
next_batch = None
return
if isinstance(event, RoomMessageText):
msg = event.body
@@ -303,8 +414,11 @@ class Room:
self.newly_inserted_posts = []
for post in self.posts:
if self.is_banned(post.poster):
self.rejected_banned_posts.append(post)
continue
try:
c.execute(
_ = c.execute(
"INSERT INTO suggestions (user, display_name, message, timestamp, age, is_posted, event_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
post.as_tuple()
)
@@ -319,21 +433,21 @@ class Room:
room_id = self.config.room_id
conn = dbutil.connect("last_run.db")
c = conn.cursor()
c.execute('''
_ = 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)")
_ = 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('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))
_ = c.execute('INSERT OR REPLACE INTO last_run (room_id, timestamp, datetime) VALUES (?, ?, ?)', (room_id, now, now_str))
dbutil.commit(conn)
conn.close()
@@ -341,7 +455,7 @@ class Room:
room_id = self.config.room_id
conn = dbutil.connect("last_run.db")
c = conn.cursor()
c.execute('''
_ = c.execute('''
CREATE TABLE IF NOT EXISTS last_run (
room_id TEXT PRIMARY KEY,
timestamp INTEGER,
@@ -349,11 +463,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()]
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,))
_ = 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:
@@ -366,7 +480,7 @@ class Room:
conn = dbutil.connect(self.db_path)
c = conn.cursor()
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)
VALUES (?, ?, ?, ?)
''', (event_id, sender, int(timestamp), dt_str))
@@ -381,7 +495,7 @@ class Room:
conn = dbutil.connect(self.db_path)
c = conn.cursor()
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)
VALUES (?, ?, ?, ?)
''', (event_id, sender, int(timestamp), dt_str))
@@ -396,7 +510,7 @@ class Room:
conn = dbutil.connect(self.db_path)
c = conn.cursor()
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)
VALUES (?, ?, ?, ?)
''', (event_id, sender, int(timestamp), dt_str))
@@ -411,7 +525,7 @@ class Room:
conn = dbutil.connect(self.db_path)
c = conn.cursor()
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)
VALUES (?, ?, ?, ?)
''', (event_id, sender, int(timestamp), dt_str))
@@ -427,6 +541,7 @@ class Room:
next_batch = None
suggestion_votes = {}
all_events = []
assert self.client is not None
# First pass: collect all recent events (both messages and reactions)
for _ in range(15): # Look back further to catch more events
@@ -434,10 +549,10 @@ class Room:
self.config.room_id,
start=next_batch,
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
all_events.extend(response.chunk)
@@ -522,8 +637,9 @@ class Room:
if since_ms is None:
since_ms = int(_time.time() * 1000)
assert self.client is not None
while True:
sync_response = await self.client.sync(timeout=30000)
sync_response = await self.client.sync(timeout=self.config.command_sync_timeout)
if not isinstance(sync_response, SyncResponse):
print("Sync failed, retrying...")
continue
@@ -585,6 +701,7 @@ class Room:
return result == "gettitles"
async def alert_post(self, post):
assert self.client is not None
content = {
"msgtype": "m.text",
"body": post
@@ -628,7 +745,7 @@ class Room:
if conditions:
query += " WHERE " + " AND ".join(conditions)
query += " ORDER BY timestamp ASC"
c.execute(query, tuple(params))
_ = 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]
@@ -636,18 +753,18 @@ class Room:
def get_last_post(self):
conn = dbutil.connect("last_post.db")
c = conn.cursor()
c.execute('''
_ = 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)")
_ = 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,))
_ = 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:
@@ -657,29 +774,29 @@ class Room:
def update_last_post(self):
conn = dbutil.connect("last_post.db")
c = conn.cursor()
c.execute('''
_ = 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)")
_ = 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('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 (?, ?, ?)',
_ = c.execute('INSERT OR REPLACE INTO last_post (room_id, timestamp, datetime) VALUES (?, ?, ?)',
(self.config.room_id, now, now_str))
dbutil.commit(conn)
conn.close()
async def post_suggestions(self):
assert self.client is not None
# 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
@@ -714,6 +831,7 @@ class Room:
async def post_suggestions_individually(self):
"""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]
if not unposted_posts:
await self.alert_post("No new unposted suggestions found.")
@@ -728,7 +846,7 @@ class Room:
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:
MAKE MAGBOT BETTER:
GIT REPO: https://magbot.online ⬅️⬅️⬅️ PRs welcome!
@@ -763,8 +881,6 @@ class Room:
print(f"Failed to post suggestion {idx}: {resp}")
continue
post.is_posted = True
reaction = {
"m.relates_to": {
"rel_type": "m.annotation",
@@ -778,9 +894,8 @@ class Room:
content=reaction,
)
await asyncio.sleep(0.5)
await asyncio.sleep(self.config.post_interval)
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):
@@ -791,19 +906,34 @@ class Room:
conn = dbutil.connect(self.db_path)
c = conn.cursor()
for post in posts:
c.execute(
_ = c.execute(
"UPDATE suggestions SET is_posted = 1 WHERE user = ? AND message = ? AND timestamp = ?",
(post.poster, post.content, post.time)
)
dbutil.commit(conn)
conn.close()
def mark_submissions_posted(self):
conn = dbutil.connect(self.db_path)
c = conn.cursor()
_ = c.execute("UPDATE suggestions SET is_posted = 1 WHERE is_posted = 0")
dbutil.commit(conn)
conn.close()
async def close(self):
if self.client:
await self.client.close()
async def acknowledge_rejected_suggestions(self):
"""Reply to banned users that they cannot submit titles."""
assert self.client is not None
for post in self.rejected_banned_posts:
await self.reject_banned_suggestion(post)
self.rejected_banned_posts = []
async def acknowledge_new_suggestions(self):
"""React ✅ to newly recorded suggestions to confirm they were saved."""
assert self.client is not None
if not self.newly_inserted_posts:
return
for post in self.newly_inserted_posts:
+8 -2
View File
@@ -21,7 +21,8 @@ async def main():
# await room.alert_post("SCRAPER LOGGED IN")
# Initial sync
await room.client.sync(timeout=3000)
assert room.client is not None
await room.client.sync(timeout=room.config.sync_timeout)
room.write_room_slugs()
try:
@@ -34,11 +35,16 @@ async def main():
# Scrape and write to database
await room.get_convos()
room.write_to_db()
if room.newly_inserted_posts:
count = room.increment_suggestion_counter(len(room.newly_inserted_posts))
await room.check_and_announce_vote_beta(count)
# Reply to banned users that they cannot submit titles
await room.acknowledge_rejected_suggestions()
# Ack newly recorded suggestions with a ✅ reaction
await room.acknowledge_new_suggestions()
room.update_last_run()
print(f"Stored {len(room.posts)} suggestions in database.")
await asyncio.sleep(30)
await asyncio.sleep(room.config.poll_interval)
finally:
# Close connection
await room.close()
+7
View File
@@ -1,3 +1,4 @@
Forgot I had a suggestion simulator
simulate suggestion 1
simulated suggestion 2
:wq
@@ -13,3 +14,9 @@ I like trains
freaking foamer
WHERE DID MY BOOTS GO
I have a booty problem
super pooper
!double bang!
!suggest double suggestion
they think they can stop my brilliance!
They said I couldn't teach a llama to drive
voom voom
Binary file not shown.
Binary file not shown.
+30 -2
View File
@@ -3,6 +3,34 @@ module vs
go 1.24.9
require (
github.com/mattn/go-sqlite3 v1.14.32 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
github.com/mattn/go-sqlite3 v1.14.32
github.com/nbd-wtf/go-nostr v0.52.3
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/bytedance/sonic v1.13.1 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/sys v0.31.0 // indirect
)
+168
View File
@@ -1,5 +1,173 @@
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nbd-wtf/go-nostr v0.52.3 h1:Xd87pXfJEJRXHpM+fLjQQln8dBNNaoPA10V7BbyP4KI=
github.com/nbd-wtf/go-nostr v0.52.3/go.mod h1:4avYoc9mDGZ9wHsvCOhHH9vPzKucCfuYBtJUSpHTfNk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+163 -5
View File
@@ -16,13 +16,21 @@ body {
padding: 20px;
}
#display_name {
input[type="text"] {
width: 100%;
padding: 10px;
margin-bottom: 20px;
margin-bottom: 15px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
#suggestion_list {
@@ -37,7 +45,7 @@ body {
border-radius: 4px;
}
#submit_vote {
button {
padding: 10px 20px;
font-size: 16px;
background-color: #4CAF50;
@@ -45,9 +53,159 @@ body {
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
margin-right: 5px;
}
button:hover {
background-color: #45a049;
}
button:disabled {
background-color: #999;
cursor: not-allowed;
}
#submit_vote {
margin-top: 20px;
font-size: 20px;
}
#auth_section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: #fafafa;
}
#vote_section {
margin-top: 20px;
}
#submit_vote:hover {
background-color: #45a049;
#logged_in_as {
padding: 10px;
background: #e8f5e9;
border-radius: 4px;
margin-bottom: 15px;
font-weight: bold;
color: #2e7d32;
}
.info-box {
padding: 8px 12px;
margin-bottom: 15px;
background: #e3f2fd;
border-radius: 4px;
color: #1565c0;
font-size: 14px;
}
.success {
padding: 8px 12px;
margin-top: 10px;
background: #e8f5e9;
border-radius: 4px;
color: #2e7d32;
}
.error {
padding: 8px 12px;
margin-top: 10px;
background: #ffebee;
border-radius: 4px;
color: #c62828;
}
#connect_btn {
background-color: #f57c00;
margin-bottom: 15px;
}
#connect_btn:hover {
background-color: #ef6c00;
}
#auth_btn {
background-color: #1976d2;
}
#auth_btn:hover {
background-color: #1565c0;
}
#link_link {
display: inline-block;
margin-top: 10px;
color: #666;
font-size: 14px;
}
#link_link:hover {
color: #333;
}
#auth_method_select {
text-align: center;
margin-bottom: 10px;
}
#auth_method_select p {
margin-bottom: 15px;
font-size: 16px;
color: #555;
}
#matrix_auth_btn {
background-color: #0dbd8b;
margin-right: 10px;
}
#matrix_auth_btn:hover {
background-color: #0aa37a;
}
#nostr_auth_btn {
background-color: #f57c00;
}
#nostr_auth_btn:hover {
background-color: #ef6c00;
}
#login_form {
max-width: 400px;
}
#login_form input[type="password"] {
width: 100%;
padding: 10px;
margin-bottom: 15px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
#back_link {
display: inline-block;
margin-top: 20px;
color: #666;
font-size: 14px;
}
#back_link:hover {
color: #333;
}
#sign_out_btn {
background-color: #999;
font-size: 12px;
padding: 4px 12px;
margin-top: 5px;
margin-bottom: 15px;
}
#sign_out_btn:hover {
background-color: #777;
}
+255 -57
View File
@@ -1,19 +1,185 @@
// Fetch submissions from the JSON endpoint and populate the suggestion list
let currentUser = null;
let currentChallenge = null;
let authMethod = null;
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// --- Matrix auth ---
function goToMatrixLogin() {
const slug = window.location.pathname.split('/').filter(Boolean)[0];
window.location.href = '/' + slug + '/matrix-login';
}
function signOut() {
sessionStorage.removeItem('matrix_session_token');
currentUser = null;
authMethod = null;
window.location.reload();
}
function showNostrAuth() {
document.getElementById('auth_method_select').style.display = 'none';
document.getElementById('nostr_auth_section').style.display = 'block';
}
async function checkMatrixSession() {
const sessionToken = sessionStorage.getItem('matrix_session_token');
if (!sessionToken) return false;
try {
const slug = window.location.pathname.split('/').filter(Boolean)[0];
const resp = await fetch('/' + slug + '/matrixVerify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_token: sessionToken })
});
if (resp.ok) {
const data = await resp.json();
currentUser = { displayName: data.display_name, sessionToken: sessionToken };
authMethod = 'matrix';
document.getElementById('auth_section').style.display = 'none';
document.getElementById('vote_section').style.display = 'block';
document.getElementById('logged_in_as').textContent = 'Signed in as: ' + data.display_name;
loadSubmissions();
return true;
} else {
sessionStorage.removeItem('matrix_session_token');
}
} catch (e) {
sessionStorage.removeItem('matrix_session_token');
}
return false;
}
// --- Nostr auth ---
function hasNip07() {
return typeof window.nostr !== 'undefined' && window.nostr !== null;
}
async function connectNip07() {
if (!hasNip07()) {
setAuthStatus('No Nostr browser extension detected. Enter your npub manually.', true);
return;
}
try {
const hexKey = await window.nostr.getPublicKey();
const resp = await fetch('encode_npub', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hex: hexKey })
});
if (resp.ok) {
const data = await resp.json();
document.getElementById('npub_input').value = data.npub;
document.getElementById('nip07_badge').style.display = 'block';
setAuthStatus('Connected! Click "Sign In to Vote" to continue.');
} else {
document.getElementById('npub_input').value = hexKey;
setAuthStatus('Connected (using hex key). Click "Sign In to Vote".');
}
} catch (err) {
setAuthStatus('Error: ' + err.message, true);
}
}
async function authenticate() {
const npub = document.getElementById('npub_input').value.trim();
if (!npub) {
setAuthStatus('Enter your npub or connect AlbyHub first.', true);
return;
}
setAuthStatus('Requesting challenge...');
try {
const resp = await fetch('challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ npub: npub })
});
if (resp.status === 404) {
setAuthStatus('Npub not linked. Please <a href="link">link your identity</a> first.', true);
return;
}
if (!resp.ok) {
const text = await resp.text();
setAuthStatus('Error: ' + text, true);
return;
}
const data = await resp.json();
currentChallenge = data.challenge;
if (!hasNip07()) {
setAuthStatus('No Nostr extension. Please install AlbyHub to sign.', true);
return;
}
setAuthStatus('Please sign the challenge in your extension...');
const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: currentChallenge
});
currentUser = {
npub: npub,
displayName: data.display_name,
signedEvent: signedEvent
};
authMethod = 'nostr';
document.getElementById('auth_section').style.display = 'none';
document.getElementById('vote_section').style.display = 'block';
document.getElementById('logged_in_as').textContent = 'Signed in as: ' + data.display_name;
loadSubmissions();
} catch (err) {
if (err.message && err.message.includes('denied')) {
setAuthStatus('Signature request denied.', true);
} else {
setAuthStatus('Error: ' + err.message, true);
}
}
}
function setAuthStatus(msg, isError) {
const el = document.getElementById('auth_status');
el.innerHTML = msg;
el.className = isError ? 'error' : 'success';
}
// --- Submissions ---
async function loadSubmissions() {
try {
const response = await fetch('submissions');
const data = await response.json();
const submissionList = document.getElementById('suggestion_list');
submissionList.innerHTML = ''; // Clear existing items
submissionList.innerHTML = '';
if (data.Submissions && data.Submissions.length > 0) {
data.Submissions.forEach((sub, index) => {
data.Submissions.forEach((sub) => {
const li = document.createElement('li');
li.innerHTML = `
<span class="suggestor">${sub.submitter} SUGGESTED</span> :
<span class="suggestion">${sub.submission}</span>
<input type="radio" name="vote" value="${sub.submission}" data-submitter="${sub.submitter}" data-time="${sub.submission_time}">
<span class="suggestor">${escapeHtml(sub.submitter)} SUGGESTED</span> :
<span class="suggestion">${escapeHtml(sub.submission)}</span>
<input type="radio" name="vote" value="${escapeHtml(sub.submission)}" data-submitter="${escapeHtml(sub.submitter)}" data-time="${sub.submission_time}">
`;
submissionList.appendChild(li);
});
@@ -26,71 +192,103 @@ async function loadSubmissions() {
}
}
// Iterate over all radio inputs and collect selected vote
function getVotes() {
const displayName = document.getElementById('display_name').value.trim();
if (!displayName) {
alert('Please enter your display name');
// --- Voting ---
async function submitVote() {
if (!currentUser) {
alert('Please sign in first');
return;
}
// Get the selected radio button
const selectedRadio = document.querySelector('input[name="vote"]:checked');
if (!selectedRadio) {
alert('Please select a title to vote for');
return;
}
// Collect vote data
const voteData = {
voter_display_name: displayName,
selected_submission: selectedRadio.value,
submitter: selectedRadio.dataset.submitter,
submission_time: parseInt(selectedRadio.dataset.time),
vote_timestamp: Math.floor(Date.now() / 1000)
submission_time: parseInt(selectedRadio.dataset.time)
};
// Send to the /getVotes endpoint
fetch('getVotes', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(voteData)
})
.then(response => {
if (response.ok) {
alert('Vote submitted successfully!');
// Clear the selection
selectedRadio.checked = false;
document.getElementById('display_name').value = '';
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to submit vote');
try {
if (authMethod === 'matrix') {
const resp = await fetch('matrixVote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_token: currentUser.sessionToken,
...voteData
})
});
if (resp.ok) {
alert('Vote submitted successfully!');
selectedRadio.checked = false;
} else {
const text = await resp.text();
alert('Error: ' + text);
}
} else if (authMethod === 'nostr') {
const challengeResp = await fetch('challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ npub: currentUser.npub })
});
if (!challengeResp.ok) {
const text = await challengeResp.text();
alert('Error getting challenge: ' + text);
return;
}
const challengeData = await challengeResp.json();
const challengeMsg = challengeData.challenge;
const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: challengeMsg
});
const voteResp = await fetch('verifyVote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
npub: currentUser.npub,
challenge: challengeMsg,
...voteData,
signed_event: signedEvent
})
});
if (voteResp.ok) {
alert('Vote submitted successfully!');
selectedRadio.checked = false;
} else {
const text = await voteResp.text();
alert('Error: ' + text);
}
}
})
.catch(error => {
} catch (error) {
console.error('Error submitting vote:', error);
alert('Error submitting vote: ' + error.message);
});
}
}
// Load submissions when page loads
document.addEventListener('DOMContentLoaded', () => {
loadSubmissions();
// Add submit button if it doesn't exist
if (!document.getElementById('submit_vote')) {
const submitButton = document.createElement('button');
submitButton.id = 'submit_vote';
submitButton.textContent = '⛵⛵⛵⛵⛵⛵⛵⛵⛵';
submitButton.onclick = getVotes;
document.body.appendChild(submitButton);
// --- Init ---
document.addEventListener('DOMContentLoaded', async () => {
setInterval(() => {
if (currentUser) loadSubmissions();
}, 30000);
const matrixAuthed = await checkMatrixSession();
if (matrixAuthed) return;
if (hasNip07()) {
document.getElementById('nip07_badge').style.display = 'block';
}
// Optionally refresh submissions periodically
setInterval(loadSubmissions, 30000); // Refresh every 30 seconds
});
+143
View File
@@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Link Nostr Identity</title>
<link rel="stylesheet" href="css.css">
</head>
<body>
<h1>Link Your Nostr Identity</h1>
<p>Connect your npub to your Matrix display name so you can vote.</p>
<div id="auth_section">
<div id="nip07_status" class="info-box" style="display:none;">
Nostr browser extension detected.
</div>
<label for="display_name">Matrix Display Name:</label>
<input type="text" id="display_name" placeholder="Enter your Matrix display name exactly">
<label for="npub_input">Nostr Public Key (npub):</label>
<input type="text" id="npub_input" placeholder="npub1...">
<button id="connect_btn" onclick="connectNip07()">Connect AlbyHub</button>
<button id="link_btn" onclick="linkNpub()">Link & Sign</button>
<div id="status"></div>
</div>
<script>
let currentNpub = null;
async function connectNip07() {
if (!window.nostr) {
setStatus('No Nostr browser extension detected. Please enter your npub manually.', true);
return;
}
try {
const pubkey = await window.nostr.getPublicKey();
const resp = await fetch('encode_npub', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({hex: pubkey})
});
if (resp.ok) {
const data = await resp.json();
document.getElementById('npub_input').value = data.npub;
currentNpub = data.npub;
setStatus('Connected! Npub loaded from extension.');
} else {
document.getElementById('npub_input').value = pubkey;
currentNpub = pubkey;
setStatus('Connected! Paste this as npub if needed: ' + pubkey);
}
} catch (err) {
setStatus('Error connecting to extension: ' + err.message, true);
}
}
async function linkNpub() {
const displayName = document.getElementById('display_name').value.trim();
const npub = document.getElementById('npub_input').value.trim();
if (!displayName) {
setStatus('Please enter your Matrix display name.', true);
return;
}
if (!npub) {
setStatus('Please enter your npub or connect AlbyHub.', true);
return;
}
setStatus('Requesting signature...');
try {
let npubHex = npub;
const decodeResp = await fetch('decode_npub', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({npub: npub})
});
if (!decodeResp.ok) {
if (!window.nostr) {
setStatus('Could not decode npub. Please install a Nostr extension.', true);
return;
}
setStatus('Could not decode npub via server.', true);
return;
}
const decodeData = await decodeResp.json();
npubHex = decodeData.hex;
const slug = window.location.pathname.split('/')[1];
const linkMsg = `Link npub ${npubHex} to Matrix user ${displayName} for room ${slug}`;
let signedEvent;
if (window.nostr) {
signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: linkMsg
});
} else {
setStatus('No Nostr extension available. Please install AlbyHub or another NIP-07 signer.', true);
return;
}
setStatus('Sending link request...');
const resp = await fetch('linkNpub', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
display_name: displayName,
npub: npub,
signed_event: signedEvent
})
});
if (resp.ok) {
setStatus('Successfully linked! You can now <a href="/">go vote</a>.');
document.getElementById('link_btn').disabled = true;
} else {
const text = await resp.text();
setStatus('Error: ' + text, true);
}
} catch (err) {
setStatus('Error: ' + err.message, true);
}
}
function setStatus(msg, isError) {
const el = document.getElementById('status');
el.innerHTML = msg;
el.className = isError ? 'error' : 'success';
}
</script>
</body>
</html>
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign in with Matrix</title>
<link rel="stylesheet" href="css.css">
</head>
<body>
<h1>Sign in with Matrix</h1>
<p>Log in with your Matrix account to vote.</p>
<div id="login_form">
<label for="homeserver">Homeserver:</label>
<input type="text" id="homeserver" placeholder="https://matrix.org">
<label for="username">Username:</label>
<input type="text" id="username" placeholder="@username:matrix.org">
<label for="password">Password:</label>
<input type="password" id="password" placeholder="Password">
<button id="login_btn" onclick="matrixLogin()">Sign In</button>
<div id="status"></div>
</div>
<a id="back_link" href="../">Back to voting page</a>
<script>
async function init() {
const parts = window.location.pathname.split('/').filter(Boolean);
const slug = parts[0];
if (!slug) {
setStatus('Invalid URL. Go to the voting page first.', true);
return;
}
try {
const resp = await fetch('/' + slug + '/roomConfig');
if (resp.ok) {
const config = await resp.json();
document.getElementById('homeserver').value = config.homeserver || 'https://matrix.org';
}
} catch (e) {
document.getElementById('homeserver').value = 'https://matrix.org';
}
const sessionToken = sessionStorage.getItem('matrix_session_token');
if (sessionToken) {
window.location.href = '/' + slug + '/';
}
}
async function matrixLogin() {
const homeserver = document.getElementById('homeserver').value.trim().replace(/\/+$/, '');
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
if (!homeserver || !username || !password) {
setStatus('All fields are required.', true);
return;
}
setStatus('Checking homeserver...');
document.getElementById('login_btn').disabled = true;
try {
const loginTypesResp = await fetch(homeserver + '/_matrix/client/v3/login');
if (!loginTypesResp.ok) {
setStatus('Could not reach homeserver. Check the URL.', true);
document.getElementById('login_btn').disabled = false;
return;
}
const loginTypes = await loginTypesResp.json();
const supportsPassword = loginTypes.flows && loginTypes.flows.some(f => f.type === 'm.login.password');
if (!supportsPassword) {
setStatus('This homeserver does not support password login. Try the Nostr option instead.', true);
document.getElementById('login_btn').disabled = false;
return;
}
setStatus('Logging in...');
const loginResp = await fetch(homeserver + '/_matrix/client/v3/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'm.login.password',
identifier: { type: 'm.id.user', user: username },
password: password
})
});
if (!loginResp.ok) {
setStatus('Login failed: ' + (loginResp.status === 403 ? 'Invalid credentials' : 'Homeserver error'), true);
document.getElementById('login_btn').disabled = false;
return;
}
const loginData = await loginResp.json();
const accessToken = loginData.access_token;
setStatus('Verifying identity...');
const parts = window.location.pathname.split('/').filter(Boolean);
const slug = parts[0];
const authResp = await fetch('/' + slug + '/matrixAuth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
access_token: accessToken,
homeserver: homeserver
})
});
if (!authResp.ok) {
const errText = await authResp.text();
setStatus('Verification failed: ' + errText, true);
document.getElementById('login_btn').disabled = false;
return;
}
const authData = await authResp.json();
sessionStorage.setItem('matrix_session_token', authData.session_token);
window.location.href = '/' + slug + '/';
} catch (err) {
setStatus('Error: ' + err.message, true);
document.getElementById('login_btn').disabled = false;
}
}
function setStatus(msg, isError) {
const el = document.getElementById('status');
el.innerHTML = msg;
el.className = isError ? 'error' : 'success';
}
init();
</script>
</body>
</html>
+38 -9
View File
@@ -8,15 +8,44 @@
</head>
<body>
<h1>Choose Your Favorite Title</h1>
<label for="display_name">Matrix Display Name (Not Matrix Base Username):</label>
<input type="text" id="display_name" placeholder="Enter your display name">
<h2>Suggestions:</h2>
<ul id="suggestion_list">
<!-- Populated Dynamically Via JS -->
</ul>
<div id="auth_section">
<div id="auth_method_select">
<p>How would you like to sign in?</p>
<button id="matrix_auth_btn" onclick="goToMatrixLogin()">Sign in with Matrix</button>
<button id="nostr_auth_btn" onclick="showNostrAuth()">Sign in with Nostr</button>
</div>
<div id="nostr_auth_section" style="display:none;">
<div id="nip07_badge" class="info-box" style="display:none;">
Nostr extension connected
</div>
<div id="auth_npub_row">
<label for="npub_input">Nostr Public Key:</label>
<input type="text" id="npub_input" placeholder="npub1... or connect extension">
<button id="connect_btn" onclick="connectNip07()">Connect AlbyHub</button>
</div>
<button id="auth_btn" onclick="authenticate()">Sign In to Vote</button>
<a id="link_link" href="link">First time? Link your npub</a>
<div id="auth_status"></div>
</div>
</div>
<div id="vote_section" style="display:none;">
<div id="logged_in_as"></div>
<button id="sign_out_btn" onclick="signOut()">Sign Out</button>
<h2>Suggestions:</h2>
<ul id="suggestion_list">
<!-- Populated Dynamically Via JS -->
</ul>
<button id="submit_vote" onclick="submitVote()">&#x26F5;&#x26F5;&#x26F5;&#x26F5;&#x26F5;&#x26F5;&#x26F5;&#x26F5;&#x26F5;</button>
</div>
<script src="js.js"></script>
</body>
</html>
</html>
+775 -35
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.