Files
Stardream eace48732d
Build and Push Docker Images / build (push) Successful in 17s
Initial release
2026-06-02 12:02:02 +10:00

149 lines
5.7 KiB
Python

import uuid
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.api.schemas import ParticipantResponse
from app.core.database import get_db
from app.models.lottery import Lottery, LotteryStatus
from app.models.participant import Participant
from app.models.user import User
from app.services.membership_service import check_membership
router = APIRouter(tags=["participants"])
@router.post("/lotteries/{lottery_id}/join")
async def join_lottery(
lottery_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> dict:
# Lock the lottery row so the capacity check + insert is atomic.
# Without this, concurrent joins can all pass the count check and exceed
# max_participants before any of them commits.
result = await db.execute(
select(Lottery).where(Lottery.id == lottery_id).with_for_update()
)
lottery = result.scalar_one_or_none()
if lottery is None:
raise HTTPException(404, "Lottery not found")
if lottery.status != LotteryStatus.ACTIVE:
raise HTTPException(400, "Lottery is not active")
# Check max participants inside the lock
if lottery.max_participants is not None:
count_result = await db.execute(
select(func.count(Participant.id)).where(Participant.lottery_id == lottery_id)
)
count = count_result.scalar() or 0
if count >= lottery.max_participants:
raise HTTPException(400, "Lottery is full")
# Check membership if required
if lottery.require_membership:
chat_ids = lottery.required_chat_ids or (
[lottery.target_chat_id] if lottery.target_chat_id else []
)
for i, cid in enumerate(chat_ids):
is_member = await check_membership(cid, current_user.id)
if not is_member:
titles = lottery.required_chat_titles or []
label = titles[i] if i < len(titles) else str(cid)
raise HTTPException(403, f"You must join {label} first")
# Check passphrase if set for this group
passphrases = lottery.required_chat_passphrases or []
if i < len(passphrases) and passphrases[i]:
from app.models.passphrase_verification import PassphraseVerification
pv = await db.execute(
select(PassphraseVerification).where(
PassphraseVerification.lottery_id == lottery_id,
PassphraseVerification.chat_id == cid,
PassphraseVerification.user_id == current_user.id,
)
)
if pv.scalar_one_or_none() is None:
titles = lottery.required_chat_titles or []
label = titles[i] if i < len(titles) else str(cid)
raise HTTPException(403, f"Please send the passphrase in {label} first")
# Check already joined
existing = await db.execute(
select(Participant).where(
Participant.lottery_id == lottery_id,
Participant.user_id == current_user.id,
)
)
if existing.scalar_one_or_none() is not None:
raise HTTPException(409, "Already joined this lottery")
participant = Participant(lottery_id=lottery_id, user_id=current_user.id)
db.add(participant)
await db.commit()
# Refresh count for post-join checks
count_result = await db.execute(
select(func.count(Participant.id)).where(Participant.lottery_id == lottery_id)
)
new_count = count_result.scalar() or 0
# Always update the group announcement with the new participant count
if lottery.announcement_targets or (lottery.announcement_message_id and lottery.target_chat_id):
try:
from app.api.routers.lotteries import sync_announcement
await sync_announcement(db, lottery, force_send=False)
except Exception:
pass
# Trigger auto-draw if participant count reached the threshold
if lottery.auto_draw_count and new_count >= lottery.auto_draw_count:
import asyncio
from app.services.scheduler import run_auto_draw
asyncio.create_task(run_auto_draw(lottery_id, trigger="auto_count"))
return {"status": "joined"}
@router.get("/lotteries/{lottery_id}/passphrase-status")
async def get_passphrase_status(
lottery_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> dict:
from app.models.passphrase_verification import PassphraseVerification
result = await db.execute(
select(PassphraseVerification.chat_id).where(
PassphraseVerification.lottery_id == lottery_id,
PassphraseVerification.user_id == current_user.id,
)
)
return {"verified_chat_ids": [row[0] for row in result.all()]}
@router.get("/lotteries/{lottery_id}/participants", response_model=list[ParticipantResponse])
async def list_participants(
lottery_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> list[ParticipantResponse]:
result = await db.execute(
select(Participant, User)
.join(User, Participant.user_id == User.id)
.where(Participant.lottery_id == lottery_id)
.order_by(Participant.joined_at)
)
return [
ParticipantResponse(
user_id=user.id,
username=user.username,
first_name=user.first_name,
last_name=user.last_name,
joined_at=participant.joined_at,
)
for participant, user in result.all()
]