mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-06 12:10:25 +00:00
368 lines
11 KiB
Python
368 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from aiogram import Bot
|
|
from aiogram.client.default import DefaultBotProperties
|
|
from aiogram.enums import ParseMode
|
|
from fastapi import (
|
|
APIRouter,
|
|
Depends,
|
|
HTTPException,
|
|
Query,
|
|
Response,
|
|
Security,
|
|
status,
|
|
)
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.database.crud.poll import (
|
|
create_poll,
|
|
delete_poll as delete_poll_record,
|
|
get_poll_by_id,
|
|
get_poll_responses_with_answers,
|
|
get_poll_statistics,
|
|
)
|
|
from app.database.models import Poll, PollAnswer, PollOption, PollQuestion, PollResponse
|
|
from app.handlers.admin.messages import get_custom_users, get_target_users
|
|
from app.services.poll_service import send_poll_to_users
|
|
|
|
from ..dependencies import get_db_session, require_api_token
|
|
from ..schemas.polls import (
|
|
PollAnswerResponse,
|
|
PollCreateRequest,
|
|
PollDetailResponse,
|
|
PollListResponse,
|
|
PollOptionStats,
|
|
PollQuestionOptionResponse,
|
|
PollQuestionResponse,
|
|
PollQuestionStats,
|
|
PollResponsesListResponse,
|
|
PollStatisticsResponse,
|
|
PollSummaryResponse,
|
|
PollUserResponse,
|
|
PollSendRequest,
|
|
PollSendResponse,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _format_price(kopeks: int) -> float:
|
|
return round(kopeks / 100, 2)
|
|
|
|
|
|
def _serialize_option(option: PollOption) -> PollQuestionOptionResponse:
|
|
return PollQuestionOptionResponse(
|
|
id=option.id,
|
|
text=option.text,
|
|
order=option.order,
|
|
)
|
|
|
|
|
|
def _serialize_question(question: PollQuestion) -> PollQuestionResponse:
|
|
options = [
|
|
_serialize_option(option)
|
|
for option in sorted(question.options, key=lambda item: item.order)
|
|
]
|
|
return PollQuestionResponse(
|
|
id=question.id,
|
|
text=question.text,
|
|
order=question.order,
|
|
options=options,
|
|
)
|
|
|
|
|
|
def _serialize_poll_summary(
|
|
poll: Poll,
|
|
responses_count: int | None = None,
|
|
) -> PollSummaryResponse:
|
|
questions = getattr(poll, "questions", [])
|
|
if responses_count is None:
|
|
responses = getattr(poll, "responses", [])
|
|
responses_count = len(responses)
|
|
return PollSummaryResponse(
|
|
id=poll.id,
|
|
title=poll.title,
|
|
description=poll.description,
|
|
reward_enabled=poll.reward_enabled,
|
|
reward_amount_kopeks=poll.reward_amount_kopeks,
|
|
reward_amount_rubles=_format_price(poll.reward_amount_kopeks),
|
|
questions_count=len(questions),
|
|
responses_count=responses_count,
|
|
created_at=poll.created_at,
|
|
updated_at=poll.updated_at,
|
|
)
|
|
|
|
|
|
def _serialize_poll_detail(poll: Poll) -> PollDetailResponse:
|
|
questions = [
|
|
_serialize_question(question)
|
|
for question in sorted(poll.questions, key=lambda item: item.order)
|
|
]
|
|
return PollDetailResponse(
|
|
id=poll.id,
|
|
title=poll.title,
|
|
description=poll.description,
|
|
reward_enabled=poll.reward_enabled,
|
|
reward_amount_kopeks=poll.reward_amount_kopeks,
|
|
reward_amount_rubles=_format_price(poll.reward_amount_kopeks),
|
|
questions=questions,
|
|
created_at=poll.created_at,
|
|
updated_at=poll.updated_at,
|
|
)
|
|
|
|
|
|
def _serialize_answer(answer: PollAnswer) -> PollAnswerResponse:
|
|
question = getattr(answer, "question", None)
|
|
option = getattr(answer, "option", None)
|
|
return PollAnswerResponse(
|
|
question_id=question.id if question else answer.question_id,
|
|
question_text=question.text if question else None,
|
|
option_id=option.id if option else answer.option_id,
|
|
option_text=option.text if option else None,
|
|
created_at=answer.created_at,
|
|
)
|
|
|
|
|
|
def _serialize_user_response(response: PollResponse) -> PollUserResponse:
|
|
user = getattr(response, "user", None)
|
|
answers = [
|
|
_serialize_answer(answer)
|
|
for answer in sorted(response.answers, key=lambda item: item.created_at)
|
|
]
|
|
return PollUserResponse(
|
|
id=response.id,
|
|
user_id=getattr(user, "id", None),
|
|
user_telegram_id=getattr(user, "telegram_id", None),
|
|
user_username=getattr(user, "username", None),
|
|
sent_at=response.sent_at,
|
|
started_at=response.started_at,
|
|
completed_at=response.completed_at,
|
|
reward_given=response.reward_given,
|
|
reward_amount_kopeks=response.reward_amount_kopeks,
|
|
reward_amount_rubles=_format_price(response.reward_amount_kopeks),
|
|
answers=answers,
|
|
)
|
|
|
|
|
|
@router.get("", response_model=PollListResponse)
|
|
async def list_polls(
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
offset: int = Query(0, ge=0),
|
|
) -> PollListResponse:
|
|
total_result = await db.execute(select(func.count()).select_from(Poll))
|
|
total = int(total_result.scalar_one() or 0)
|
|
|
|
if total == 0:
|
|
return PollListResponse(items=[], total=0, limit=limit, offset=offset)
|
|
|
|
result = await db.execute(
|
|
select(Poll)
|
|
.options(selectinload(Poll.questions))
|
|
.order_by(Poll.created_at.desc())
|
|
.offset(offset)
|
|
.limit(limit)
|
|
)
|
|
polls = result.scalars().unique().all()
|
|
|
|
if not polls:
|
|
return PollListResponse(items=[], total=total, limit=limit, offset=offset)
|
|
|
|
poll_ids = [poll.id for poll in polls]
|
|
counts_result = await db.execute(
|
|
select(PollResponse.poll_id, func.count(PollResponse.id))
|
|
.where(PollResponse.poll_id.in_(poll_ids))
|
|
.group_by(PollResponse.poll_id)
|
|
)
|
|
responses_counts = {poll_id: count for poll_id, count in counts_result.all()}
|
|
|
|
return PollListResponse(
|
|
items=[
|
|
_serialize_poll_summary(
|
|
poll, responses_counts.get(poll.id, 0)
|
|
)
|
|
for poll in polls
|
|
],
|
|
total=total,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
|
|
|
|
@router.get("/{poll_id}", response_model=PollDetailResponse)
|
|
async def get_poll(
|
|
poll_id: int,
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> PollDetailResponse:
|
|
poll = await get_poll_by_id(db, poll_id)
|
|
if not poll:
|
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Poll not found")
|
|
|
|
return _serialize_poll_detail(poll)
|
|
|
|
|
|
@router.post("", response_model=PollDetailResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_poll_endpoint(
|
|
payload: PollCreateRequest,
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> PollDetailResponse:
|
|
poll = await create_poll(
|
|
db,
|
|
title=payload.title,
|
|
description=payload.description,
|
|
reward_enabled=payload.reward_enabled,
|
|
reward_amount_kopeks=payload.reward_amount_kopeks,
|
|
created_by=None,
|
|
questions=[
|
|
{
|
|
"text": question.text,
|
|
"options": [option.text for option in question.options],
|
|
}
|
|
for question in payload.questions
|
|
],
|
|
)
|
|
|
|
poll = await get_poll_by_id(db, poll.id)
|
|
return _serialize_poll_detail(poll)
|
|
|
|
|
|
@router.delete("/{poll_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_poll(
|
|
poll_id: int,
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> Response:
|
|
success = await delete_poll_record(db, poll_id)
|
|
if not success:
|
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Poll not found")
|
|
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
@router.get("/{poll_id}/stats", response_model=PollStatisticsResponse)
|
|
async def get_poll_stats(
|
|
poll_id: int,
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> PollStatisticsResponse:
|
|
poll = await db.get(Poll, poll_id)
|
|
if not poll:
|
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Poll not found")
|
|
|
|
stats = await get_poll_statistics(db, poll_id)
|
|
|
|
formatted_questions = [
|
|
PollQuestionStats(
|
|
id=question_data["id"],
|
|
text=question_data["text"],
|
|
order=question_data["order"],
|
|
options=[
|
|
PollOptionStats(
|
|
id=option_data["id"],
|
|
text=option_data["text"],
|
|
count=option_data["count"],
|
|
)
|
|
for option_data in question_data.get("options", [])
|
|
],
|
|
)
|
|
for question_data in stats.get("questions", [])
|
|
]
|
|
|
|
return PollStatisticsResponse(
|
|
poll_id=poll.id,
|
|
poll_title=poll.title,
|
|
total_responses=stats.get("total_responses", 0),
|
|
completed_responses=stats.get("completed_responses", 0),
|
|
reward_sum_kopeks=stats.get("reward_sum_kopeks", 0),
|
|
reward_sum_rubles=_format_price(stats.get("reward_sum_kopeks", 0)),
|
|
questions=formatted_questions,
|
|
)
|
|
|
|
|
|
@router.get("/{poll_id}/responses", response_model=PollResponsesListResponse)
|
|
async def get_poll_responses(
|
|
poll_id: int,
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
offset: int = Query(0, ge=0),
|
|
) -> PollResponsesListResponse:
|
|
poll_exists = await db.get(Poll, poll_id)
|
|
if not poll_exists:
|
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Poll not found")
|
|
|
|
responses, total = await get_poll_responses_with_answers(
|
|
db,
|
|
poll_id,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
|
|
items = [_serialize_user_response(response) for response in responses]
|
|
|
|
return PollResponsesListResponse(
|
|
items=items,
|
|
total=total,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
|
|
|
|
@router.post("/{poll_id}/send", response_model=PollSendResponse)
|
|
async def send_poll(
|
|
poll_id: int,
|
|
payload: PollSendRequest,
|
|
_: Any = Security(require_api_token),
|
|
db: AsyncSession = Depends(get_db_session),
|
|
) -> PollSendResponse:
|
|
poll = await get_poll_by_id(db, poll_id)
|
|
if not poll:
|
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "Poll not found")
|
|
|
|
target = payload.target.strip()
|
|
if not target:
|
|
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Target must not be empty")
|
|
|
|
if target.startswith("custom_"):
|
|
users = await get_custom_users(db, target.replace("custom_", ""))
|
|
else:
|
|
users = await get_target_users(db, target)
|
|
|
|
if not users:
|
|
return PollSendResponse(
|
|
poll_id=poll_id,
|
|
target=target,
|
|
sent=0,
|
|
failed=0,
|
|
skipped=0,
|
|
total=0,
|
|
)
|
|
|
|
from app.config import settings
|
|
|
|
bot = Bot(
|
|
token=settings.BOT_TOKEN,
|
|
default=DefaultBotProperties(parse_mode=ParseMode.HTML),
|
|
)
|
|
|
|
try:
|
|
result = await send_poll_to_users(bot, db, poll, users)
|
|
finally:
|
|
await bot.session.close()
|
|
|
|
return PollSendResponse(
|
|
poll_id=poll_id,
|
|
target=target,
|
|
sent=result.get("sent", 0),
|
|
failed=result.get("failed", 0),
|
|
skipped=result.get("skipped", 0),
|
|
total=result.get("total", 0),
|
|
)
|