From c0391f8e694d0ce7787d107ef506dff3b7cbc866 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 22 Jul 2026 14:43:45 +0300 Subject: [PATCH] FEATURE: added pols --- chat.db | Bin 8192 -> 24576 bytes controllers/__pycache__/chat.cpython-314.pyc | Bin 7868 -> 14244 bytes controllers/__pycache__/tenor.cpython-314.pyc | Bin 8504 -> 8504 bytes controllers/chat.py | 122 +++++ index.html | 470 +++++++++++++++++- models/__pycache__/base.cpython-314.pyc | Bin 2638 -> 5009 bytes models/__pycache__/db.cpython-314.pyc | Bin 6280 -> 14896 bytes models/base.py | 55 +- models/db.py | 141 +++++- 9 files changed, 780 insertions(+), 8 deletions(-) diff --git a/chat.db b/chat.db index aa8949b08085dd808835492957911aeab53847fd..bf0161f3fe510a25144e2e4be9b3941074af9de4 100644 GIT binary patch delta 973 zcmah{-HOvd6i)1>KUrf|5P~c&(~CljMOYRE(Th#XYz=l3cQzIGDx0-)F_3MN&P3`} z>#8?ih|=fqqWA(nf^Q)B1U`bDY0@9N#Xw-@%y-WD&i9?s;pm6_W%qVblBE3f&pzdA z{lF+n4?f+|G(}RjJCeSq{n51T&dr~b_?P%4PZE6Xst8I0}QndlNED7bm5O zpy@*3nvFIDS$uq=-#Zi+(gl|7!WKlpc3fb0+U+_}OJT%F*z-2L@*aI}{8`@}@?+QY z8E9CoX`z)OFOYR?qBHOuo`EXq)_CEjW1qUnJt{;bAu8-9YRb&8>|!M*7p@>pu2b zn665kU@91`aTTzQYlH_8>0`QBGCm5S-enRn5(OAE9H$LUI}Q82u=n;dOz3i)GQYy6 qp_e9VJ~L5u|B+a$e+5x%-}m(2J9&*ex-*Ht-I|P!<^NpO74Tmr5F3L4 delta 54 zcmZoTz}Vn0L0XWNfq{Vuh+%+fqK+{?D}!E_2rp2GkzbC1Uv9IY!Un$0a{P}30M+aW Aa{vGU diff --git a/controllers/__pycache__/chat.cpython-314.pyc b/controllers/__pycache__/chat.cpython-314.pyc index e026b8278b54de4fc3164c2fc904a8e5004666f5..2ff94167e774cfe061f705c7489885144f0333e5 100644 GIT binary patch delta 5190 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 W>G*7t9}<2c1sWv7V>G!7OXT0mf`tbF delta 631 zcmYLGL2J}N6wV}@Y~$_*Bf7YvvQSJ3g{56-78DUI>`4|ZON&~KyJlR|?k4pmMGb1{ zLA2sQn4?#3qA2mGcDx7s37l-%GKbz?XU7``&xs%)E&|U$ADa+93tkhZncj z?%N-&+*G9^zL~#_rP{ROTKVfrpr6jn`F5u@hu?hojwuQ(Vo6u(93X~FoU)rn_zL+< zEZQxW39!BDx!&)?sOq^4`r?!QwX%c}vN>aa`$N`uYxuGG_bokvoQ*Y}+0m~sxRJRs!^ zSrPRU-_Q44O&Y^EO~)KAVhU6Q*O1y^V`GoFOzen&P61S(7zmEI**;m>!X4EmFBS2$ zy`%4{6}^*xIs1dLNbD^9oY}^tEc<>C-}ZgDfe~CMN{tg(pvWb7ES`DOY*oDVyc)HH YV+i=}+v1PcV9&)=N0?p4I#5UcABkO`=l}o! diff --git a/controllers/__pycache__/tenor.cpython-314.pyc b/controllers/__pycache__/tenor.cpython-314.pyc index 25527c13eafe55e8d0817fa06c256dc9aefcd1e6..5f49f25e869a544432fff035607cf6c2bb87b268 100644 GIT binary patch delta 22 ccmdntw8M#4n~#@^0SMHlBxH$f diff --git a/controllers/chat.py b/controllers/chat.py index 4a35de9..2b9329e 100644 --- a/controllers/chat.py +++ b/controllers/chat.py @@ -131,3 +131,125 @@ def register_routes(app): except Exception as e: response.status = 500 return {"success": False, "error": str(e)} + + @app.route("/api/polls", method="POST") + def create_poll(): + ip = request.environ.get("REMOTE_ADDR", "127.0.0.1") + if not _check_rate(ip): + response.status = 429 + return {"success": False, "error": "Rate limit exceeded. Slow down."} + + try: + data = request.json + if not data: + response.status = 400 + return {"success": False, "error": "No data"} + + channel = data.get("channel", "general") + question = data.get("question", "").strip() + options = data.get("options", []) + + if channel not in VALID_CHANNELS: + response.status = 400 + return {"success": False, "error": "Invalid channel"} + if not question: + response.status = 400 + return {"success": False, "error": "Question is required"} + if len(question) > 500: + response.status = 400 + return {"success": False, "error": "Question too long"} + if len(options) < 2: + response.status = 400 + return {"success": False, "error": "At least 2 options required"} + if len(options) > 20: + response.status = 400 + return {"success": False, "error": "Maximum 20 options allowed"} + for opt in options: + if not opt.strip(): + response.status = 400 + return {"success": False, "error": "Option cannot be empty"} + if len(opt) > 200: + response.status = 400 + return {"success": False, "error": "Option too long"} + + ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16] + expires_at = data.get("expires_at") + is_multiple = data.get("is_multiple", False) + is_anonymous = data.get("is_anonymous", False) + + poll_id = _db.add_poll( + channel=channel, + creator_ip=ip_hash, + question=question, + options=options, + expires_at=expires_at, + is_multiple=is_multiple, + is_anonymous=is_anonymous, + ) + return {"success": True, "poll_id": poll_id} + except Exception as e: + response.status = 500 + return {"success": False, "error": str(e)} + + @app.route("/api/polls", method="GET") + def list_polls(): + try: + channel = request.query.get("channel", "general") + if channel not in VALID_CHANNELS: + channel = "general" + polls = _db.get_polls(channel=channel) + return {"success": True, "polls": polls} + except Exception as e: + response.status = 500 + return {"success": False, "error": str(e)} + + @app.route("/api/polls/", method="GET") + def get_poll(poll_id): + try: + poll = _db.get_poll(poll_id) + if not poll: + response.status = 404 + return {"success": False, "error": "Poll not found"} + return {"success": True, "poll": poll} + except Exception as e: + response.status = 500 + return {"success": False, "error": str(e)} + + @app.route("/api/polls//vote", method="POST") + def vote_poll(poll_id): + ip = request.environ.get("REMOTE_ADDR", "127.0.0.1") + try: + data = request.json + if not data or "option_id" not in data: + response.status = 400 + return {"success": False, "error": "option_id required"} + + ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16] + ok, msg = _db.vote(poll_id, data["option_id"], ip_hash) + if not ok: + response.status = 400 + return {"success": False, "error": msg} + return {"success": True, "message": msg} + except Exception as e: + response.status = 500 + return {"success": False, "error": str(e)} + + @app.route("/api/polls//close", method="POST") + def close_poll(poll_id): + ip = request.environ.get("REMOTE_ADDR", "127.0.0.1") + ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16] + + auth = request.get_header("Authorization", "") + token = auth.replace("Bearer ", "") + if _admin_token and token == _admin_token: + ok, msg = _db.close_poll(poll_id, requester_ip=ip_hash) + if not ok: + response.status = 400 + return {"success": False, "error": msg} + return {"success": True, "message": msg} + + ok, msg = _db.close_poll(poll_id, requester_ip=ip_hash) + if not ok: + response.status = 400 + return {"success": False, "error": msg} + return {"success": True, "message": msg} diff --git a/index.html b/index.html index 586d38f..a1850b9 100644 --- a/index.html +++ b/index.html @@ -245,6 +245,220 @@ .toast.error { background: #c0392b; color: #fff; } .toast.success { background: #27ae60; color: #fff; } .toast.info { background: #2980b9; color: #fff; } + .poll-card { + background: #1a2d4a; + border-radius: 8px; + padding: 14px; + margin-bottom: 12px; + border-left: 3px solid #f39c12; + } + .poll-card .poll-question { + font-size: 15px; + font-weight: 600; + color: #f1c40f; + margin-bottom: 10px; + } + .poll-card .poll-meta { + font-size: 11px; + color: #6a8aaa; + margin-bottom: 8px; + } + .poll-card .poll-option { + display: flex; + align-items: center; + padding: 8px 10px; + margin-bottom: 4px; + background: #0f2040; + border-radius: 6px; + cursor: pointer; + transition: background 0.15s; + position: relative; + overflow: hidden; + } + .poll-card .poll-option:hover { background: #1a3a5a; } + .poll-card .poll-option .poll-bar { + position: absolute; + left: 0; top: 0; bottom: 0; + background: rgba(46, 204, 113, 0.15); + border-radius: 6px; + transition: width 0.3s; + } + .poll-card .poll-option .poll-label { + position: relative; + z-index: 1; + flex: 1; + font-size: 14px; + } + .poll-card .poll-option .poll-pct { + position: relative; + z-index: 1; + font-size: 12px; + color: #7ec8a3; + min-width: 36px; + text-align: right; + } + .poll-card .poll-option .poll-votes { + position: relative; + z-index: 1; + font-size: 11px; + color: #556677; + margin-left: 8px; + min-width: 20px; + text-align: right; + } + .poll-card .poll-option.voted { + border: 1px solid #2ecc71; + } + .poll-card .poll-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 8px; + font-size: 12px; + color: #556677; + } + .poll-card .poll-footer button { + padding: 4px 12px; + background: transparent; + border: 1px solid #6a8aaa; + border-radius: 4px; + color: #a0c4ff; + font-size: 11px; + cursor: pointer; + transition: all 0.15s; + } + .poll-card .poll-footer button:hover { + background: #e94560; + border-color: #e94560; + color: #fff; + } + .poll-card .poll-status { + font-size: 11px; + padding: 2px 8px; + border-radius: 4px; + } + .poll-card .poll-status.active { background: #27ae60; color: #fff; } + .poll-card .poll-status.closed { background: #7f8c8d; color: #fff; } + .poll-modal-overlay { + display: none; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); + z-index: 2000; + justify-content: center; + align-items: center; + } + .poll-modal-overlay.open { display: flex; } + .poll-modal { + background: #16213e; + border-radius: 12px; + padding: 24px; + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow-y: auto; + } + .poll-modal h2 { + color: #f1c40f; + margin-bottom: 16px; + font-size: 18px; + } + .poll-modal label { + display: block; + font-size: 13px; + color: #a0c4ff; + margin-bottom: 4px; + margin-top: 12px; + } + .poll-modal input[type="text"], + .poll-modal textarea { + width: 100%; + padding: 8px 12px; + border: 1px solid #1a4a7a; + border-radius: 6px; + background: #121e38; + color: #e0e0e0; + font-size: 14px; + outline: none; + } + .poll-modal input:focus, .poll-modal textarea:focus { border-color: #e94560; } + .poll-modal textarea { resize: vertical; min-height: 60px; font-family: inherit; } + .poll-modal .poll-opt-row { + display: flex; + gap: 8px; + margin-bottom: 6px; + align-items: center; + } + .poll-modal .poll-opt-row input { flex: 1; } + .poll-modal .poll-opt-row button { + padding: 6px 10px; + background: transparent; + border: 1px solid #e74c3c; + border-radius: 4px; + color: #e74c3c; + cursor: pointer; + font-size: 14px; + } + .poll-modal .poll-opt-row button:hover { background: #e74c3c; color: #fff; } + .poll-modal .add-opt-btn { + padding: 6px 14px; + background: transparent; + border: 1px dashed #2a4a7a; + border-radius: 6px; + color: #6a8aaa; + cursor: pointer; + font-size: 13px; + margin-top: 4px; + width: 100%; + } + .poll-modal .add-opt-btn:hover { + border-color: #e94560; + color: #e94560; + } + .poll-modal .modal-actions { + display: flex; + gap: 10px; + margin-top: 16px; + justify-content: flex-end; + } + .poll-modal .modal-actions button { + padding: 8px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + font-weight: 600; + } + .poll-modal .modal-actions .btn-cancel { + background: #2a3a5a; + color: #8899aa; + } + .poll-modal .modal-actions .btn-cancel:hover { background: #3a4a6a; } + .poll-modal .modal-actions .btn-create { + background: #e94560; + color: #fff; + } + .poll-modal .modal-actions .btn-create:hover { background: #d63850; } + .poll-modal .modal-actions .btn-create:disabled { + background: #4a5a7a; + cursor: not-allowed; + } + .poll-modal .poll-check-row { + display: flex; + gap: 16px; + margin-top: 12px; + } + .poll-modal .poll-check-row label { + display: flex; + align-items: center; + gap: 6px; + margin: 0; + cursor: pointer; + font-size: 13px; + } + .poll-modal .poll-check-row input[type="checkbox"] { + accent-color: #e94560; + } @@ -264,11 +478,39 @@
+
+
+
+

๐Ÿ“Š Create Poll

+ + + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
diff --git a/models/__pycache__/base.cpython-314.pyc b/models/__pycache__/base.cpython-314.pyc index 8afc256c4d24e5916c31f5fc9ca9e4958a6e0031..7d3d6db80203a62456b14b469eedd2e5f947a036 100644 GIT binary patch literal 5009 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 WHwAcXc7??=TnnF@@qifPHv1Q36L*3D delta 617 zcmX|-KX21O7{=e%b{scb>IK5-+vj3@k!ln}>dGBfp~3H~=qasxH<1M)_1V`^G%*N`Gbv zoPw}1Kuzr7m3r*QaWIayy=OjiFfn>NTpqnp)Rl`^5M5=6W${{BX_llnBT%e3C-DNl zXa<|hbn&*-5Tbi{YH-f)z}h$dm^Jqi6JKlDf5B;*dVg`<|Xiqid2ZW*g$ zFLzl!dz0HemwAqB#~II>qXI+d)#+N zlW5?&%>JhjLNAVo!GI6^FmzoRrz3A-l(@;NlC1V06d@i-{CNon@VVL)pLAExLe`ER zRu&X2iLvD@U!ysy|5Cf1StLd3)AggRAo5vP-ZaEJ>s6kfK5%BmrhR`u(fRI95N$E) zGb{Gw$285TgOg?{QDUrbkB5`c@3LF+NKxZ8i61H={!|pyej9-6$8h}{^p2tT70#wQ b;>_WV6y%Z`2HwU)o&v3Dt%x?8qqu(oDpG=X diff --git a/models/__pycache__/db.cpython-314.pyc b/models/__pycache__/db.cpython-314.pyc index 956a500eeaa06f00501ab2151bc8d7dcbc222b78..dc611d9b5c7ecb0478da02aa823408eae5890356 100644 GIT binary patch literal 14896 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 delta 1324 zcmah}O-NKx6n=O9-kWh8$N6#m!=J>Thm@M;Pn2b*MVa7;LQst3yqCRr<5H-i;sKmJ?DJqeD~b9$DZ{>IwR#F z1-`c%bC2GIHY3$!eX~7R)u<$uW+knqlytDqgAR(qqMJKUh=+HPRCUhXa?$(SINVeC zIXhf<2IrON=LHetbZjQ=F<o8i1(-nW))B(S;R%y{#(S?&;bea{j!!mY)<#m9t=g*gx6YRr3&Z>kCJ534(G{Z>obV2 z`{@WKxP~x_a2-S58=%kh6v;e`Mkx1$1-%)t@*T^ zsspzIeF<;gS#VUmDIAuzrY4vdBXTq@Cj}#g-?kv3l=Y-Tm(;eLk}Ymkz_=9mE1r60 zZezKp)Ix8-!K7X(I}`rp6_c*X21Ve3Wiyk?AfXkgVbaX}8pTAyXP8coPVt>s-GtO@ z7}Ng|O(y+MfGIqmz)-S`gYT@^jNSp;Q2}J@*nBY_ze_I5$qj%s8Ot3gxJtL_`9i@i zb+6kA7_8#QL?$hobsxX!7JluM}z~V-#|k486%yLmQ-nzZlY6F=|_J{qW@r?NYvA dtk~u-T?G?KoOAs9P+PS9C~$?;94Y|P{TCNH5jg+= diff --git a/models/base.py b/models/base.py index 28de686..f4f9248 100644 --- a/models/base.py +++ b/models/base.py @@ -3,9 +3,12 @@ models/base.py โ€” declarative base & the Messages model. """ from sqlalchemy import create_engine, Integer, String, Text -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker from typing import Optional - +from sqlalchemy import ( + Column, Integer, String, Text, Boolean, BigInteger, + ForeignKey, UniqueConstraint +) class Base(DeclarativeBase): pass @@ -21,6 +24,54 @@ class Messages(Base): gif_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True, default=None) timestamp: Mapped[int] = mapped_column(Integer, nullable=False) +class Poll(Base): + __tablename__ = "polls" + + id = Column(Integer, primary_key=True) + channel = Column(String(32), nullable=False, default="general") + creator_ip = Column(String(64), nullable=False) + question = Column(Text, nullable=False) # encrypted + created_at = Column(BigInteger, nullable=False) + expires_at = Column(BigInteger, nullable=True) + is_multiple = Column(Boolean, default=False) + is_anonymous = Column(Boolean, default=False) + status = Column(String(16), default="active") # active | closed | deleted + + options = relationship("PollOption", back_populates="poll", + cascade="all, delete-orphan", + order_by="PollOption.position") + votes = relationship("PollVote", back_populates="poll", + cascade="all, delete-orphan") + + +class PollOption(Base): + __tablename__ = "poll_options" + + id = Column(Integer, primary_key=True) + poll_id = Column(Integer, ForeignKey("polls.id"), nullable=False) + label = Column(Text, nullable=False) # encrypted + position = Column(Integer, default=0) + + poll = relationship("Poll", back_populates="options") + votes = relationship("PollVote", back_populates="option", + cascade="all, delete-orphan") + + +class PollVote(Base): + __tablename__ = "poll_votes" + __table_args__ = ( + UniqueConstraint("poll_id", "voter_ip", + name="uq_poll_voter"), + ) + + id = Column(Integer, primary_key=True) + poll_id = Column(Integer, ForeignKey("polls.id"), nullable=False) + option_id = Column(Integer, ForeignKey("poll_options.id"), nullable=False) + voter_ip = Column(String(64), nullable=False) # IP-hash + voted_at = Column(BigInteger, nullable=False) + + poll = relationship("Poll", back_populates="votes") + option = relationship("PollOption", back_populates="votes") def make_engine(db_cfg): """Create a SQLAlchemy engine from DatabaseConfig.""" diff --git a/models/db.py b/models/db.py index cbf073a..08bd35a 100644 --- a/models/db.py +++ b/models/db.py @@ -10,7 +10,7 @@ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC import base64 from sqlalchemy.orm import sessionmaker -from .base import make_engine, Messages +from .base import make_engine, Messages, Poll, PollOption, PollVote class DBfrontend: @@ -77,3 +77,142 @@ class DBfrontend: session.commit() return True return False + + def add_poll( + self, + channel: str, + creator_ip: str, + question: str, + options: list[str], + expires_at: Optional[int] = None, + is_multiple: bool = False, + is_anonymous: bool = False, + ) -> int: + with self.Session() as session: + poll = Poll( + channel=channel, + creator_ip=creator_ip, + question=self._encrypt(question), + created_at=int(time.time() * 1000), + expires_at=expires_at, + is_multiple=is_multiple, + is_anonymous=is_anonymous, + status="active", + ) + session.add(poll) + session.flush() + + for i, label in enumerate(options): + opt = PollOption(poll_id=poll.id, label=self._encrypt(label), position=i) + session.add(opt) + + session.commit() + return poll.id + + def get_polls(self, channel: str = "general") -> list: + with self.Session() as session: + polls = ( + session.query(Poll) + .filter(Poll.channel == channel, Poll.status != "deleted") + .order_by(Poll.created_at.desc()) + .limit(20) + ) + result = [] + for poll in polls.all(): + result.append(self._serialize_poll(session, poll)) + return result + + def get_poll(self, poll_id: int) -> Optional[dict]: + with self.Session() as session: + poll = session.get(Poll, poll_id) + if not poll or poll.status == "deleted": + return None + return self._serialize_poll(session, poll, include_voters=True) + + def _serialize_poll(self, session, poll, include_voters=False) -> dict: + options_data = [] + total_votes = 0 + for opt in poll.options: + vote_count = len(opt.votes) + total_votes += vote_count + voters = [v.voter_ip for v in opt.votes] if include_voters else None + options_data.append({ + "id": opt.id, + "label": self._decrypt(opt.label), + "position": opt.position, + "votes": vote_count, + "voters": voters, + }) + + return { + "id": poll.id, + "channel": poll.channel, + "creator_ip": poll.creator_ip, + "question": self._decrypt(poll.question), + "created_at": poll.created_at, + "expires_at": poll.expires_at, + "is_multiple": poll.is_multiple, + "is_anonymous": poll.is_anonymous, + "status": poll.status, + "options": options_data, + "total_votes": total_votes, + } + + def vote(self, poll_id: int, option_id: int, voter_ip: str) -> tuple[bool, str]: + with self.Session() as session: + poll = session.get(Poll, poll_id) + if not poll or poll.status != "active": + return False, "Poll is not active" + + if poll.expires_at and int(time.time() * 1000) > poll.expires_at: + poll.status = "closed" + session.commit() + return False, "Poll has expired" + + option_valid = any(opt.id == option_id for opt in poll.options) + if not option_valid: + return False, "Invalid option" + + if not poll.is_multiple: + existing = ( + session.query(PollVote) + .filter(PollVote.poll_id == poll_id, PollVote.voter_ip == voter_ip) + .first() + ) + if existing: + return False, "You have already voted in this poll" + + existing_opt = ( + session.query(PollVote) + .filter( + PollVote.poll_id == poll_id, + PollVote.option_id == option_id, + PollVote.voter_ip == voter_ip, + ) + .first() + ) + if existing_opt: + return False, "You have already voted for this option" + + vote = PollVote( + poll_id=poll_id, + option_id=option_id, + voter_ip=voter_ip, + voted_at=int(time.time() * 1000), + ) + session.add(vote) + session.commit() + return True, "Vote recorded" + + def close_poll(self, poll_id: int, requester_ip: str) -> tuple[bool, str]: + with self.Session() as session: + poll = session.get(Poll, poll_id) + if not poll: + return False, "Poll not found" + if poll.status != "active": + return False, "Poll is not active" + if poll.creator_ip != requester_ip: + return False, "Only the creator can close this poll" + poll.status = "closed" + session.commit() + return True, "Poll closed"