from __future__ import annotations import uuid from dataclasses import dataclass, field from datetime import datetime, UTC, timedelta from typing import Any @dataclass class PollOption: index: int text: str votes: set[str] = field(default_factory=set) @dataclass class Poll: poll_id: str question: str options: list[PollOption] chat_id: str message_ts: str created_by: str = "" created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) closed_at: datetime | None = None @property def is_closed(self) -> bool: return self.closed_at is not None @property def total_votes(self) -> int: return sum(len(opt.votes) for opt in self.options) def vote(self, option_index: int, user_id: str) -> bool: for opt in self.options: opt.votes.discard(user_id) if 0 <= option_index < len(self.options): self.options[option_index].votes.add(user_id) return True return False def results(self) -> dict[str, Any]: option_results = [ { "index": opt.index, "text": opt.text, "count": len(opt.votes), } for opt in self.options ] return { "poll_id": self.poll_id, "question": self.question, "options": option_results, "total_votes": self.total_votes, "is_closed": self.is_closed, "chat_id": self.chat_id, } class PollManager: def __init__(self, ttl_seconds: float = 86400.0): self._polls: dict[str, Poll] = {} self._ttl_seconds = ttl_seconds def create_poll( self, question: str, options: list[str], chat_id: str, message_ts: str, created_by: str = "" ) -> Poll: poll_id = uuid.uuid4().hex[:12] poll_options = [PollOption(index=i, text=opt) for i, opt in enumerate(options)] poll = Poll( poll_id=poll_id, question=question, options=poll_options, chat_id=chat_id, message_ts=message_ts, created_by=created_by, ) self._polls[poll_id] = poll self._cleanup_expired() return poll def get_poll(self, poll_id: str) -> Poll | None: return self._polls.get(poll_id) def vote_poll(self, poll_id: str, option_index: int, user_id: str) -> bool: poll = self._polls.get(poll_id) if not poll or poll.is_closed: return False return poll.vote(option_index, user_id) def close_poll(self, poll_id: str) -> bool: poll = self._polls.get(poll_id) if not poll or poll.is_closed: return False poll.closed_at = datetime.now(UTC) return True def list_polls(self, chat_id: str | None = None) -> list[dict[str, Any]]: polls = self._polls.values() if chat_id: polls = [p for p in polls if p.chat_id == chat_id] return [p.results() for p in polls if not p.is_closed] def _cleanup_expired(self) -> None: now = datetime.now(UTC) expired = [ pid for pid, poll in self._polls.items() if poll.is_closed and now - poll.closed_at > timedelta(seconds=self._ttl_seconds) ] for pid in expired: self._polls.pop(pid, None)