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:
@@ -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
63
bot/poll_handler.py
Normal 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)
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user