diff --git a/chat.db b/chat.db index bf0161f..23b1d02 100644 Binary files a/chat.db and b/chat.db differ diff --git a/index.html b/index.html index a1850b9..11a8663 100644 --- a/index.html +++ b/index.html @@ -523,7 +523,6 @@ let pollInterval = null; let selectedGifUrl = null; let votedPolls = new Set(); - let pollsData = []; function loadVotedPolls() { try { @@ -711,7 +710,7 @@ if (data.success) { showToast('Poll created!', 'success'); pollModal.classList.remove('open'); - loadPolls(); + loadMessages(); } else { showToast(data.error || 'Failed to create poll', 'error'); } @@ -745,57 +744,6 @@ renderChannels(); messagesEl.innerHTML = '
Loading...
'; loadMessages(); - loadPolls(); - } - - async function loadPolls() { - try { - const r = await fetch(`/api/polls?channel=${currentChannel}`); - const data = await r.json(); - if (data.success) pollsData = data.polls || []; - else pollsData = []; - } catch (e) { - pollsData = []; - } - } - - function renderPolls() { - if (!pollsData.length) return ''; - let html = ''; - pollsData.forEach(poll => { - const isClosed = poll.status === 'closed'; - const isExpired = poll.expires_at && Date.now() > poll.expires_at; - const hasVoted = votedPolls.has(poll.id) || poll.options.some(o => o.votes > 0 && false); - const total = poll.total_votes || 0; - let optionsHtml = ''; - poll.options.forEach(opt => { - const pct = total > 0 ? Math.round((opt.votes / total) * 100) : 0; - const votedClass = hasVoted ? ' voted' : ''; - if (isClosed || isExpired || hasVoted) { - optionsHtml += `
-
- ${escapeHtml(opt.label)} - ${pct}% - ${opt.votes} -
`; - } else { - optionsHtml += `
- ${escapeHtml(opt.label)} - ${opt.votes} votes -
`; - } - }); - const statusLabel = isClosed || poll.status === 'closed' ? 'Closed' : - isExpired ? 'Expired' : 'Active'; - const statusClass = isClosed || isExpired ? 'closed' : 'active'; - const creatorLabel = poll.is_anonymous ? 'Anonymous' : escapeHtml(poll.creator_ip); - html += `
-
๐Ÿ“Š ${escapeHtml(poll.question)}
-
by ${creatorLabel} ยท ${total} vote${total !== 1 ? 's' : ''} ยท ${statusLabel}
-
${optionsHtml}
-
`; - }); - return html; } async function loadMessages() { @@ -813,28 +761,61 @@ } function renderMessages(msgs) { - const pollsHtml = renderPolls(); - if (msgs.length === 0 && !pollsHtml) { + if (msgs.length === 0) { messagesEl.innerHTML = '
๐Ÿ’ฌ No messages yet. Be the first!
'; return; } - let html = pollsHtml; + let html = ''; msgs.forEach(msg => { - const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); - let contentHtml = escapeHtml(msg.content || ''); - let gifToShow = msg.gif_url || null; - if (!gifToShow) { - const gifMatch = msg.content.match(/https?:\/\/[^\s]+\.(gif|webp)(\?[^\s]*)?/i); - if (gifMatch) gifToShow = gifMatch[0]; + if (msg.poll) { + const poll = msg.poll; + const isClosed = poll.status === 'closed'; + const isExpired = poll.expires_at && Date.now() > poll.expires_at; + const hasVoted = votedPolls.has(poll.id); + const total = poll.total_votes || 0; + let optionsHtml = ''; + poll.options.forEach(opt => { + const pct = total > 0 ? Math.round((opt.votes / total) * 100) : 0; + const votedClass = hasVoted ? ' voted' : ''; + if (isClosed || isExpired || hasVoted) { + optionsHtml += `
+
+ ${escapeHtml(opt.label)} + ${pct}% + ${opt.votes} +
`; + } else { + optionsHtml += `
+ ${escapeHtml(opt.label)} + ${opt.votes} votes +
`; + } + }); + const statusLabel = isClosed ? 'Closed' : isExpired ? 'Expired' : 'Active'; + const statusClass = isClosed || isExpired ? 'closed' : 'active'; + const creatorLabel = poll.is_anonymous ? 'Anonymous' : escapeHtml(poll.creator_ip); + html += `
+
๐Ÿ“Š ${escapeHtml(poll.question)}
+
by ${creatorLabel} ยท ${total} vote${total !== 1 ? 's' : ''} ยท ${statusLabel}
+
${optionsHtml}
+
`; + } else { + const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); + let contentHtml = escapeHtml(msg.content || ''); + let gifToShow = msg.gif_url || null; + if (!gifToShow) { + const gifMatch = msg.content.match(/https?:\/\/[^\s]+\.(gif|webp)(\?[^\s]*)?/i); + if (gifMatch) gifToShow = gifMatch[0]; + } + if (gifToShow) { + contentHtml += `gif`; + } + html += `
+
๐Ÿ”‘ ${escapeHtml(msg.ip)}
+
${contentHtml}
+
${time}
+
`; } - if (gifToShow) { - contentHtml += `gif`; - } - html += `
-
๐Ÿ”‘ ${escapeHtml(msg.ip)}
-
${contentHtml}
-
${time}
-
`; }); messagesEl.innerHTML = html; attachPollHandlers(); @@ -859,10 +840,10 @@ votedPolls.add(pollId); saveVotedPolls(); showToast('Vote recorded!', 'success'); - loadPolls().then(() => renderMessages(window._lastMessages || [])); + loadMessages(); } else { showToast(data.error || 'Failed to vote', 'error'); - loadPolls().then(() => renderMessages(window._lastMessages || [])); + loadMessages(); } } catch (e) { showToast('Network error', 'error'); @@ -904,17 +885,13 @@ loadVotedPolls(); renderChannels(); loadMessages(); - loadPolls(); sendBtn.addEventListener('click', sendMessage); inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); }); - // Auto-poll every 3 seconds + // Auto-refresh every 3 seconds setInterval(() => { loadMessages(); - loadPolls().then(() => { - if (window._lastMessages) renderMessages(window._lastMessages); - }); }, 3000); diff --git a/models/__pycache__/base.cpython-314.pyc b/models/__pycache__/base.cpython-314.pyc index 7d3d6db..584c546 100644 Binary files a/models/__pycache__/base.cpython-314.pyc and b/models/__pycache__/base.cpython-314.pyc differ diff --git a/models/__pycache__/db.cpython-314.pyc b/models/__pycache__/db.cpython-314.pyc index dc611d9..b16e7d3 100644 Binary files a/models/__pycache__/db.cpython-314.pyc and b/models/__pycache__/db.cpython-314.pyc differ diff --git a/models/base.py b/models/base.py index f4f9248..c5907b2 100644 --- a/models/base.py +++ b/models/base.py @@ -22,6 +22,7 @@ class Messages(Base): ip: Mapped[str] = mapped_column(String(64), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False) gif_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True, default=None) + poll_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, default=None) timestamp: Mapped[int] = mapped_column(Integer, nullable=False) class Poll(Base): diff --git a/models/db.py b/models/db.py index 08bd35a..0b9d355 100644 --- a/models/db.py +++ b/models/db.py @@ -18,6 +18,16 @@ class DBfrontend: self.engine = make_engine(db_cfg) self.Session = sessionmaker(bind=self.engine) self.cipher = self._get_cipher(secret, salt) + self._migrate() + + def _migrate(self): + with self.Session() as session: + from sqlalchemy import inspect, text + inspector = inspect(session.connection()) + columns = [c["name"] for c in inspector.get_columns("messages")] + if "poll_id" not in columns: + session.execute(text("ALTER TABLE messages ADD COLUMN poll_id INTEGER")) + session.commit() @staticmethod def _get_cipher(secret: bytes, salt: bytes) -> Fernet: @@ -34,13 +44,14 @@ class DBfrontend: except Exception: return "[Decryption failed]" - def add_message(self, channel: str, ip: str, content: str, gif_url: Optional[str] = None) -> int: + def add_message(self, channel: str, ip: str, content: str, gif_url: Optional[str] = None, poll_id: Optional[int] = None) -> int: with self.Session() as session: msg = Messages( channel=channel, ip=ip, content=self._encrypt(content), gif_url=gif_url, + poll_id=poll_id, timestamp=int(time.time() * 1000), ) session.add(msg) @@ -57,6 +68,11 @@ class DBfrontend: ) result = [] for msg in reversed(q.all()): + poll_data = None + if msg.poll_id: + poll = session.get(Poll, msg.poll_id) + if poll and poll.status != "deleted": + poll_data = self._serialize_poll(session, poll) result.append( { "id": msg.id, @@ -64,6 +80,8 @@ class DBfrontend: "ip": msg.ip, "content": self._decrypt(msg.content), "gif_url": msg.gif_url, + "poll_id": msg.poll_id, + "poll": poll_data, "timestamp": msg.timestamp, } ) @@ -106,6 +124,15 @@ class DBfrontend: opt = PollOption(poll_id=poll.id, label=self._encrypt(label), position=i) session.add(opt) + msg = Messages( + channel=channel, + ip=creator_ip, + content=self._encrypt(question), + poll_id=poll.id, + timestamp=int(time.time() * 1000), + ) + session.add(msg) + session.commit() return poll.id