135 lines
4.8 KiB
Python
135 lines
4.8 KiB
Python
import asyncio
|
|
import html
|
|
import logging
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.core.database import AsyncSessionLocal
|
|
from app.models.lottery import Lottery, LotteryStatus
|
|
from app.services.draw_service import execute_draw
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def _announce_winners(lottery: Lottery, winners_data: list[dict]) -> None:
|
|
if not winners_data:
|
|
return
|
|
targets = lottery.announcement_targets or (
|
|
[{"chat_id": lottery.target_chat_id, "message_id": lottery.announcement_message_id}]
|
|
if lottery.target_chat_id else []
|
|
)
|
|
if not targets:
|
|
return
|
|
from app.bot.router import bot
|
|
from aiogram.exceptions import TelegramAPIError
|
|
|
|
lines = []
|
|
for w in winners_data:
|
|
full_name = html.escape(
|
|
w["first_name"] + (f" {w['last_name']}" if w.get("last_name") else "")
|
|
)
|
|
safe_name = f'<a href="tg://user?id={w["user_id"]}">{full_name}</a>'
|
|
lines.append(f"🏆 {safe_name} - {html.escape(w['prize_name'])}")
|
|
|
|
text = (
|
|
f"🎉 <b>Draw Results: {html.escape(lottery.title)}</b>\n\n"
|
|
+ "\n".join(lines)
|
|
+ "\n\nCongratulations to all winners! 🎊"
|
|
)
|
|
from aiogram.types import ReplyParameters
|
|
for t in targets:
|
|
try:
|
|
reply = (
|
|
ReplyParameters(message_id=t["message_id"], allow_sending_without_reply=True)
|
|
if t.get("message_id")
|
|
else None
|
|
)
|
|
await bot.send_message(
|
|
t["chat_id"],
|
|
text,
|
|
parse_mode="HTML",
|
|
reply_parameters=reply,
|
|
)
|
|
except TelegramAPIError as e:
|
|
logger.warning("Failed to announce auto-draw winners to %s: %s", t["chat_id"], e)
|
|
|
|
|
|
async def notify_winners(
|
|
lottery_title: str, winners_data: list[dict], contact_info: str | None = None,
|
|
lottery_id: uuid.UUID | None = None,
|
|
) -> None:
|
|
"""Send a private congratulations message to each winner via the bot."""
|
|
from app.bot.router import bot
|
|
from aiogram.exceptions import TelegramAPIError
|
|
from app.bot.keyboards.inline import lottery_detail_keyboard
|
|
|
|
from collections import defaultdict
|
|
user_wins: dict[int, list[dict]] = defaultdict(list)
|
|
for w in winners_data:
|
|
user_wins[w["user_id"]].append(w)
|
|
|
|
keyboard = lottery_detail_keyboard(str(lottery_id), text="🏆 View results") if lottery_id else None
|
|
|
|
for user_id, wins in user_wins.items():
|
|
if len(wins) == 1:
|
|
text = (
|
|
f"🎉 Congratulations! You won in <b>{html.escape(lottery_title)}</b>!\n"
|
|
f"🎁 Prize: <b>{html.escape(wins[0]['prize_name'])}</b>"
|
|
)
|
|
else:
|
|
prizes_lines = "\n".join(f"🎁 {html.escape(w['prize_name'])}" for w in wins)
|
|
text = (
|
|
f"🎉 Congratulations! You won {len(wins)} prizes in <b>{html.escape(lottery_title)}</b>!\n"
|
|
+ prizes_lines
|
|
)
|
|
if contact_info:
|
|
text += f"\n\n📬 Contact to claim your prize: {html.escape(contact_info)}"
|
|
else:
|
|
text += "\n\nPlease contact the lottery organizer to claim your prize."
|
|
try:
|
|
await bot.send_message(user_id, text, parse_mode="HTML", reply_markup=keyboard)
|
|
except TelegramAPIError:
|
|
pass
|
|
|
|
|
|
async def run_auto_draw(lottery_id: uuid.UUID, trigger: str = "scheduled") -> None:
|
|
async with AsyncSessionLocal() as db:
|
|
try:
|
|
winners_data = await execute_draw(db, lottery_id, trigger=trigger)
|
|
except ValueError as e:
|
|
logger.info("Auto-draw skipped for %s: %s", lottery_id, e)
|
|
return
|
|
except Exception as e:
|
|
logger.error("Auto-draw error for %s: %s", lottery_id, e)
|
|
return
|
|
|
|
result = await db.execute(select(Lottery).where(Lottery.id == lottery_id))
|
|
lottery = result.scalar_one_or_none()
|
|
if lottery:
|
|
await _announce_winners(lottery, winners_data)
|
|
await notify_winners(lottery.title, winners_data, lottery.contact_info, lottery_id=lottery.id)
|
|
|
|
|
|
async def auto_draw_scheduler() -> None:
|
|
"""Poll every 30 s for lotteries whose draw_at has passed."""
|
|
while True:
|
|
await asyncio.sleep(30)
|
|
try:
|
|
now = datetime.utcnow()
|
|
async with AsyncSessionLocal() as db:
|
|
result = await db.execute(
|
|
select(Lottery.id).where(
|
|
Lottery.status == LotteryStatus.ACTIVE,
|
|
Lottery.draw_at.isnot(None),
|
|
Lottery.draw_at <= now,
|
|
)
|
|
)
|
|
ids = result.scalars().all()
|
|
|
|
for lottery_id in ids:
|
|
asyncio.create_task(run_auto_draw(lottery_id))
|
|
except Exception as e:
|
|
logger.error("Scheduler error: %s", e)
|