Add nostr login and periodic vote-beta announcements
This commit is contained in:
@@ -24,3 +24,6 @@ initial_sync_timeout: 300 # Initial sync timeout for post_results (ms)
|
|||||||
post_loop_interval: 1 # Delay between post_results loop iterations (s)
|
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_interval: 300 # How often the user-list service refreshes (s)
|
||||||
user_list_retry_interval: 60 # Retry delay on user-list error (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)
|
||||||
|
|||||||
@@ -22,3 +22,6 @@ initial_sync_timeout: 300 # Initial sync timeout for post_results (ms)
|
|||||||
post_loop_interval: 1 # Delay between post_results loop iterations (s)
|
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_interval: 300 # How often the user-list service refreshes (s)
|
||||||
user_list_retry_interval: 60 # Retry delay on user-list error (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)
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ dbName: ""
|
|||||||
vote_host: "agmninex.online"
|
vote_host: "agmninex.online"
|
||||||
vote_path: "lunch" # URL slug for this room's voting page (http://<vote_host>:9081/<vote_path>)
|
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
|
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)
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ dbName: ""
|
|||||||
vote_host: "agmninex.online"
|
vote_host: "agmninex.online"
|
||||||
vote_path: "jb-chat" # URL slug for this room's voting page (http://<vote_host>:9081/<vote_path>)
|
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
|
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)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class Config:
|
|||||||
self.post_loop_interval = config.get("post_loop_interval", 1)
|
self.post_loop_interval = config.get("post_loop_interval", 1)
|
||||||
self.user_list_interval = config.get("user_list_interval", 300)
|
self.user_list_interval = config.get("user_list_interval", 300)
|
||||||
self.user_list_retry_interval = config.get("user_list_retry_interval", 60)
|
self.user_list_retry_interval = config.get("user_list_retry_interval", 60)
|
||||||
|
self.suggestion_announce_interval = config.get("suggestion_announce_interval", 0)
|
||||||
|
|
||||||
class Room:
|
class Room:
|
||||||
def __init__(self, config_path="configs/config.yaml", search_terms=None, homeserver=None):
|
def __init__(self, config_path="configs/config.yaml", search_terms=None, homeserver=None):
|
||||||
@@ -67,6 +68,7 @@ class Room:
|
|||||||
self.banned_db_path = "banned_users.db"
|
self.banned_db_path = "banned_users.db"
|
||||||
self.init_db()
|
self.init_db()
|
||||||
self.init_banned_db()
|
self.init_banned_db()
|
||||||
|
self.init_suggestion_counter_db()
|
||||||
|
|
||||||
|
|
||||||
def safe_db_name(self, room_id):
|
def safe_db_name(self, room_id):
|
||||||
@@ -193,6 +195,47 @@ class Room:
|
|||||||
dbutil.commit(conn)
|
dbutil.commit(conn)
|
||||||
conn.close()
|
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):
|
async def reject_banned_suggestion(self, post):
|
||||||
"""Reply to a banned user's suggestion and do not insert it."""
|
"""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")
|
await self.alert_post(f"{post.display_name}: this user has been banned from submitting titles")
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ async def main():
|
|||||||
# Scrape and write to database
|
# Scrape and write to database
|
||||||
await room.get_convos()
|
await room.get_convos()
|
||||||
room.write_to_db()
|
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
|
# Reply to banned users that they cannot submit titles
|
||||||
await room.acknowledge_rejected_suggestions()
|
await room.acknowledge_rejected_suggestions()
|
||||||
# Ack newly recorded suggestions with a ✅ reaction
|
# Ack newly recorded suggestions with a ✅ reaction
|
||||||
|
|||||||
+30
-2
@@ -3,6 +3,34 @@ module vs
|
|||||||
go 1.24.9
|
go 1.24.9
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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/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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
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
@@ -16,13 +16,21 @@ body {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#display_name {
|
input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
#suggestion_list {
|
#suggestion_list {
|
||||||
@@ -37,7 +45,7 @@ body {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#submit_vote {
|
button {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background-color: #4CAF50;
|
background-color: #4CAF50;
|
||||||
@@ -45,9 +53,159 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
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;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#submit_vote:hover {
|
#logged_in_as {
|
||||||
background-color: #45a049;
|
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
@@ -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() {
|
async function loadSubmissions() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('submissions');
|
const response = await fetch('submissions');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const submissionList = document.getElementById('suggestion_list');
|
const submissionList = document.getElementById('suggestion_list');
|
||||||
submissionList.innerHTML = ''; // Clear existing items
|
submissionList.innerHTML = '';
|
||||||
|
|
||||||
if (data.Submissions && data.Submissions.length > 0) {
|
if (data.Submissions && data.Submissions.length > 0) {
|
||||||
data.Submissions.forEach((sub, index) => {
|
data.Submissions.forEach((sub) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<span class="suggestor">${sub.submitter} SUGGESTED</span> :
|
<span class="suggestor">${escapeHtml(sub.submitter)} SUGGESTED</span> :
|
||||||
<span class="suggestion">${sub.submission}</span>
|
<span class="suggestion">${escapeHtml(sub.submission)}</span>
|
||||||
<input type="radio" name="vote" value="${sub.submission}" data-submitter="${sub.submitter}" data-time="${sub.submission_time}">
|
<input type="radio" name="vote" value="${escapeHtml(sub.submission)}" data-submitter="${escapeHtml(sub.submitter)}" data-time="${sub.submission_time}">
|
||||||
`;
|
`;
|
||||||
submissionList.appendChild(li);
|
submissionList.appendChild(li);
|
||||||
});
|
});
|
||||||
@@ -26,71 +192,103 @@ async function loadSubmissions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over all radio inputs and collect selected vote
|
// --- Voting ---
|
||||||
function getVotes() {
|
|
||||||
const displayName = document.getElementById('display_name').value.trim();
|
async function submitVote() {
|
||||||
|
if (!currentUser) {
|
||||||
if (!displayName) {
|
alert('Please sign in first');
|
||||||
alert('Please enter your display name');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the selected radio button
|
|
||||||
const selectedRadio = document.querySelector('input[name="vote"]:checked');
|
const selectedRadio = document.querySelector('input[name="vote"]:checked');
|
||||||
|
|
||||||
if (!selectedRadio) {
|
if (!selectedRadio) {
|
||||||
alert('Please select a title to vote for');
|
alert('Please select a title to vote for');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect vote data
|
|
||||||
const voteData = {
|
const voteData = {
|
||||||
voter_display_name: displayName,
|
|
||||||
selected_submission: selectedRadio.value,
|
selected_submission: selectedRadio.value,
|
||||||
submitter: selectedRadio.dataset.submitter,
|
submitter: selectedRadio.dataset.submitter,
|
||||||
submission_time: parseInt(selectedRadio.dataset.time),
|
submission_time: parseInt(selectedRadio.dataset.time)
|
||||||
vote_timestamp: Math.floor(Date.now() / 1000)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send to the /getVotes endpoint
|
try {
|
||||||
fetch('getVotes', {
|
if (authMethod === 'matrix') {
|
||||||
method: 'POST',
|
const resp = await fetch('matrixVote', {
|
||||||
headers: {
|
method: 'POST',
|
||||||
'Content-Type': 'application/json'
|
headers: { 'Content-Type': 'application/json' },
|
||||||
},
|
body: JSON.stringify({
|
||||||
body: JSON.stringify(voteData)
|
session_token: currentUser.sessionToken,
|
||||||
})
|
...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');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
console.error('Error submitting vote:', error);
|
||||||
alert('Error submitting vote: ' + error.message);
|
alert('Error submitting vote: ' + error.message);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load submissions when page loads
|
// --- Init ---
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
loadSubmissions();
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
setInterval(() => {
|
||||||
// Add submit button if it doesn't exist
|
if (currentUser) loadSubmissions();
|
||||||
if (!document.getElementById('submit_vote')) {
|
}, 30000);
|
||||||
const submitButton = document.createElement('button');
|
|
||||||
submitButton.id = 'submit_vote';
|
const matrixAuthed = await checkMatrixSession();
|
||||||
submitButton.textContent = '⛵⛵⛵⛵⛵⛵⛵⛵⛵';
|
if (matrixAuthed) return;
|
||||||
submitButton.onclick = getVotes;
|
|
||||||
document.body.appendChild(submitButton);
|
if (hasNip07()) {
|
||||||
|
document.getElementById('nip07_badge').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally refresh submissions periodically
|
|
||||||
setInterval(loadSubmissions, 30000); // Refresh every 30 seconds
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
@@ -8,15 +8,44 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Choose Your Favorite Title</h1>
|
<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>
|
<div id="auth_section">
|
||||||
<ul id="suggestion_list">
|
<div id="auth_method_select">
|
||||||
<!-- Populated Dynamically Via JS -->
|
<p>How would you like to sign in?</p>
|
||||||
</ul>
|
<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()">⛵⛵⛵⛵⛵⛵⛵⛵⛵</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="js.js"></script>
|
<script src="js.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+775
-35
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user