Implement automatic poll answer statistics tracking

Features:
- Add poll_mappings table to link Telegram poll_id with question_id
- Add PollStatisticsHandler to automatically record user votes
- Implement save_poll_mapping, get_question_id_by_poll methods
- Implement get_answer_id_by_option_index for answer lookup
- Update scheduler to save poll mappings on publish
- Add comprehensive test suite (test_statistics.py)
- Add STATISTICS_GUIDE.md with detailed documentation
- Update README.md with statistics features

Statistics features:
✅ Automatic vote counting for each answer option
✅ Percentage distribution display
✅ Anonymous tracking (no user data stored)
✅ View statistics via /manage → 📊 Statistics
✅ Support for multiple poll publications per question

All tests passing successfully!

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-05 12:27:45 +02:00
parent 25380e7c1f
commit ab8e129277
7 changed files with 601 additions and 4 deletions

View File

@@ -8,6 +8,7 @@ from game import AppSettingsService, SQLiteWouldYouRatherRepository
from .command_system import CommandContext
from .commands import registry
from .manage import ManageController
from .poll_handler import PollStatisticsHandler
from .scheduler import QuestionPublisherScheduler
logger = logging.getLogger(__name__)
@@ -47,11 +48,15 @@ def _build_client() -> Client:
async def main():
client = _build_client()
context = _build_context()
repository = SQLiteWouldYouRatherRepository()
registry.bind(client, context)
ManageController(context).register(client)
PollStatisticsHandler(repository).register(client)
scheduler = QuestionPublisherScheduler(
client=client,
repository=SQLiteWouldYouRatherRepository(),
repository=repository,
channel_ids=context.poll_channel_ids,
)
logger.info("Bot client initialized and command handlers registered.")

63
bot/poll_handler.py Normal file
View File

@@ -0,0 +1,63 @@
from __future__ import annotations
import logging
from pyrogram import Client
from pyrogram.handlers import PollAnswerHandler
from pyrogram.types import PollAnswer
from game import SQLiteWouldYouRatherRepository
logger = logging.getLogger(__name__)
class PollStatisticsHandler:
"""Обработчик ответов на опросы для записи статистики."""
def __init__(self, repository: SQLiteWouldYouRatherRepository | None = None) -> None:
self.repository = repository or SQLiteWouldYouRatherRepository()
def register(self, client: Client) -> None:
"""Регистрирует обработчик в клиенте Pyrogram."""
client.add_handler(PollAnswerHandler(self._on_poll_answer))
logger.info("Poll statistics handler registered.")
async def _on_poll_answer(self, _: Client, poll_answer: PollAnswer) -> None:
"""Обрабатывает ответ пользователя на опрос."""
try:
poll_id = poll_answer.poll_id
if not poll_id:
return
# Получаем question_id по poll_id
question_id = self.repository.get_question_id_by_poll(poll_id)
if question_id is None:
logger.debug("Poll %s not found in mappings, skipping.", poll_id)
return
# Обрабатываем выбранные опции
option_ids = poll_answer.option_ids or []
for option_index in option_ids:
# Получаем answer_id по индексу опции
answer_id = self.repository.get_answer_id_by_option_index(
question_id, option_index
)
if answer_id is None:
logger.warning(
"Answer not found for question %s option %s",
question_id,
option_index,
)
continue
# Записываем статистику
self.repository.record_answer(question_id, answer_id, increment=1)
logger.debug(
"Recorded answer: question=%s, answer=%s, user=%s",
question_id,
answer_id,
poll_answer.user.id if poll_answer.user else "unknown",
)
except Exception:
logger.exception("Failed to process poll answer: %s", poll_answer)

View File

@@ -162,12 +162,18 @@ class QuestionPublisherScheduler:
options = [answer.text for answer in question.answers]
for channel_id in self.channel_ids:
try:
await self.client.send_poll(
message = await self.client.send_poll(
chat_id=channel_id,
question=question.question.text,
options=options,
is_anonymous=True,
)
# Сохраняем маппинг poll_id -> question_id для статистики
if message.poll and message.poll.id:
self.repository.save_poll_mapping(
poll_id=message.poll.id,
question_id=question.question.id,
)
logger.info(
"Published question #%s to channel %s via schedule '%s'.",
question.question.id,