| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758 |
- from __future__ import annotations
- import re
- from asyncio import Lock
- from datetime import UTC, datetime, timedelta
- from secrets import token_hex
- from typing import Any
- from pymongo import ASCENDING, DESCENDING, ReturnDocument
- from pymongo.errors import DuplicateKeyError
- from wbb import BOT_PROFILE_ID, db
- STATUS_RUNNING = "running"
- STATUS_DRAWING = "drawing"
- STATUS_CANCELING = "canceling"
- STATUS_FINISHED = "finished"
- STATUS_CANCELED = "canceled"
- ACTIVE_STATUSES = {STATUS_RUNNING, STATUS_DRAWING, STATUS_CANCELING}
- ALL_STATUSES = ACTIVE_STATUSES | {STATUS_FINISHED, STATUS_CANCELED}
- giveawaysdb = db.giveaways
- participantsdb = db.giveaway_participants
- giveaway_bansdb = db.giveaway_bans
- ticket_ordersdb = db.giveaway_ticket_orders
- templatesdb = db.giveaway_templates
- _index_lock = Lock()
- _indexes_ready = False
- def utc_now() -> datetime:
- return datetime.now(UTC)
- def normalize_giveaway_id(giveaway_id: str) -> str:
- return giveaway_id.strip().lower().lstrip("#")
- def as_utc(value: datetime) -> datetime:
- if value.tzinfo is None:
- return value.replace(tzinfo=UTC)
- return value.astimezone(UTC)
- def _nonnegative_int(value: Any, *, maximum: int = 1_000_000_000) -> int:
- try:
- parsed = int(value)
- except (TypeError, ValueError) as exc:
- raise ValueError("积分数值必须是整数。") from exc
- if parsed < 0 or parsed > maximum:
- raise ValueError(f"积分数值必须在 0 到 {maximum} 之间。")
- return parsed
- def normalize_prizes(prizes: list[dict[str, Any]]) -> list[dict[str, Any]]:
- normalized: list[dict[str, Any]] = []
- total_winners = 0
- for item in prizes:
- name = str(item.get("name") or "").strip()
- count = _nonnegative_int(item.get("count"), maximum=100)
- points_reward = _nonnegative_int(item.get("points_reward", 0))
- if not name or count < 1:
- raise ValueError("每个奖项都必须填写名称,且中奖人数至少为 1。")
- total_winners += count
- normalized.append(
- {"name": name[:100], "count": count, "points_reward": points_reward}
- )
- if not normalized or total_winners > 100:
- raise ValueError("抽奖总中奖人数必须在 1 到 100 人之间。")
- return normalized
- async def ensure_giveaway_indexes() -> None:
- global _indexes_ready
- if _indexes_ready:
- return
- async with _index_lock:
- if _indexes_ready:
- return
- await giveawaysdb.create_index([("giveaway_id", ASCENDING)], unique=True)
- await giveawaysdb.create_index([("status", ASCENDING), ("ends_at", ASCENDING)])
- await giveawaysdb.create_index(
- [("chat_id", ASCENDING), ("created_at", DESCENDING)]
- )
- await participantsdb.create_index(
- [("giveaway_id", ASCENDING), ("user_id", ASCENDING)], unique=True
- )
- await participantsdb.create_index(
- [("giveaway_id", ASCENDING), ("active", ASCENDING), ("joined_at", ASCENDING)]
- )
- await giveaway_bansdb.create_index(
- [("chat_id", ASCENDING), ("user_id", ASCENDING)], unique=True
- )
- await giveaway_bansdb.create_index(
- [("chat_id", ASCENDING), ("created_at", DESCENDING)]
- )
- await ticket_ordersdb.create_index([("order_id", ASCENDING)], unique=True)
- await ticket_ordersdb.create_index([("status", ASCENDING), ("updated_at", ASCENDING)])
- await templatesdb.create_index([("template_id", ASCENDING)], unique=True)
- await giveawaysdb.create_index(
- [("template_id", ASCENDING), ("period_key", ASCENDING)],
- unique=True,
- partialFilterExpression={"template_id": {"$exists": True}},
- )
- _indexes_ready = True
- async def create_giveaway(
- *,
- chat_id: int,
- creator_id: int,
- creator_name: str,
- title: str,
- description: str,
- prizes: list[dict[str, Any]],
- ends_at: datetime,
- starts_at: datetime | None = None,
- minimum_points: int = 0,
- entry_cost: int = 0,
- participation_reward: int = 0,
- max_tickets_per_user: int = 10,
- eligibility_targets: list[dict[str, Any]] | None = None,
- eligibility_mode: str = "all",
- template_id: str | None = None,
- period_key: str | None = None,
- ) -> dict[str, Any]:
- await ensure_giveaway_indexes()
- title = title.strip()
- if not title:
- raise ValueError("必须填写抽奖标题。")
- now = utc_now()
- normalized_starts_at = as_utc(starts_at) if starts_at else now
- normalized_ends_at = as_utc(ends_at)
- if normalized_ends_at <= now:
- raise ValueError("开奖时间必须晚于当前时间。")
- if normalized_starts_at >= normalized_ends_at:
- raise ValueError("报名开始时间必须早于开奖时间。")
- normalized_prizes = normalize_prizes(prizes)
- if eligibility_mode not in {"all", "any"}:
- raise ValueError("资格条件组合方式必须是全部或任一。")
- cost = _nonnegative_int(entry_cost)
- ticket_limit = _nonnegative_int(max_tickets_per_user, maximum=100)
- if ticket_limit < 1:
- raise ValueError("每人奖票上限至少为 1。")
- if not cost:
- ticket_limit = 1
- if template_id and not period_key:
- raise ValueError("循环抽奖缺少期次。")
- for _ in range(20):
- giveaway_id = token_hex(3)
- doc = {
- "giveaway_id": giveaway_id,
- "bot_id": BOT_PROFILE_ID,
- "chat_id": int(chat_id),
- "creator_id": int(creator_id),
- "creator_name": creator_name.strip()[:100],
- "title": title[:200],
- "description": description.strip()[:4000],
- "prizes": normalized_prizes,
- "minimum_points": _nonnegative_int(minimum_points),
- "entry_cost": cost,
- "max_tickets_per_user": ticket_limit,
- "eligibility_targets": list(eligibility_targets or []),
- "eligibility_mode": eligibility_mode,
- "participation_reward": _nonnegative_int(participation_reward),
- "status": STATUS_RUNNING,
- "created_at": now,
- "updated_at": now,
- "starts_at": normalized_starts_at,
- "ends_at": normalized_ends_at,
- "message_id": None,
- "winners": [],
- "rerolls": [],
- }
- if normalized_starts_at <= now and not template_id:
- doc["start_announced_at"] = now
- if template_id:
- doc["template_id"] = template_id
- doc["period_key"] = period_key
- try:
- await giveawaysdb.insert_one(doc)
- return doc
- except DuplicateKeyError:
- if template_id:
- existing = await giveawaysdb.find_one(
- {"template_id": template_id, "period_key": period_key}
- )
- if existing:
- return existing
- continue
- raise RuntimeError("无法生成唯一的抽奖编号,请重试。")
- async def attach_giveaway_message(
- giveaway_id: str, chat_id: int, message_id: int
- ) -> None:
- await ensure_giveaway_indexes()
- await giveawaysdb.update_one(
- {"giveaway_id": normalize_giveaway_id(giveaway_id)},
- {
- "$set": {
- "chat_id": int(chat_id),
- "message_id": int(message_id),
- "updated_at": utc_now(),
- }
- },
- )
- async def get_giveaway(giveaway_id: str) -> dict[str, Any] | None:
- await ensure_giveaway_indexes()
- return await giveawaysdb.find_one(
- {
- "giveaway_id": normalize_giveaway_id(giveaway_id),
- "bot_id": BOT_PROFILE_ID,
- }
- )
- async def update_running_giveaway(
- giveaway_id: str,
- *,
- title: str,
- description: str,
- starts_at: datetime,
- ends_at: datetime,
- expected_updated_at: datetime,
- ) -> dict[str, Any] | None:
- await ensure_giveaway_indexes()
- normalized_title = title.strip()
- normalized_description = description.strip()
- normalized_starts_at = as_utc(starts_at)
- normalized_ends_at = as_utc(ends_at)
- now = utc_now()
- previous_updated_at = as_utc(expected_updated_at)
- if int(now.timestamp() * 1000) <= int(previous_updated_at.timestamp() * 1000):
- now = previous_updated_at + timedelta(milliseconds=1)
- if not normalized_title:
- raise ValueError("必须填写抽奖标题。")
- if len(normalized_title) > 200:
- raise ValueError("抽奖标题不能超过 200 个字符。")
- if len(normalized_description) > 4000:
- raise ValueError("抽奖说明不能超过 4000 个字符。")
- if normalized_ends_at <= now:
- raise ValueError("开奖时间必须晚于当前时间。")
- if normalized_starts_at >= normalized_ends_at:
- raise ValueError("报名开始时间必须早于开奖时间。")
- return await giveawaysdb.find_one_and_update(
- {
- "giveaway_id": normalize_giveaway_id(giveaway_id),
- "bot_id": BOT_PROFILE_ID,
- "status": STATUS_RUNNING,
- "updated_at": expected_updated_at,
- },
- {
- "$set": {
- "title": normalized_title,
- "description": normalized_description,
- "starts_at": normalized_starts_at,
- "ends_at": normalized_ends_at,
- "updated_at": now,
- }
- },
- return_document=ReturnDocument.AFTER,
- )
- async def list_running_giveaways(
- *, chat_id: int | None = None, creator_id: int | None = None, limit: int = 20
- ) -> list[dict[str, Any]]:
- await ensure_giveaway_indexes()
- query: dict[str, Any] = {"status": STATUS_RUNNING, "bot_id": BOT_PROFILE_ID}
- if chat_id is not None:
- query["chat_id"] = int(chat_id)
- if creator_id is not None:
- query["creator_id"] = int(creator_id)
- cursor = giveawaysdb.find(query).sort("created_at", DESCENDING).limit(limit)
- return [doc async for doc in cursor]
- async def list_giveaways(
- *, chat_id: int | None = None, status: str | None = None, limit: int = 50
- ) -> list[dict[str, Any]]:
- items, _ = await list_giveaways_page(
- chat_id=chat_id, status=status, page=1, page_size=limit
- )
- return items
- async def list_giveaways_page(
- *,
- chat_id: int | None = None,
- status: str | None = None,
- query: str = "",
- page: int = 1,
- page_size: int = 20,
- all_bots: bool = False,
- ) -> tuple[list[dict[str, Any]], int]:
- await ensure_giveaway_indexes()
- filters: dict[str, Any] = {} if all_bots else {"bot_id": BOT_PROFILE_ID}
- if chat_id is not None:
- filters["chat_id"] = int(chat_id)
- if status:
- if status not in ALL_STATUSES:
- return [], 0
- filters["status"] = status
- query = query.strip()
- if query:
- giveaway_id_pattern = re.escape(query.lstrip("#"))
- title_pattern = re.escape(query)
- filters["$or"] = [
- {"giveaway_id": {"$regex": giveaway_id_pattern, "$options": "i"}},
- {"title": {"$regex": title_pattern, "$options": "i"}},
- ]
- page = max(1, int(page))
- page_size = max(1, min(int(page_size), 100))
- total = await giveawaysdb.count_documents(filters)
- cursor = (
- giveawaysdb.find(filters)
- .sort("created_at", DESCENDING)
- .skip((page - 1) * page_size)
- .limit(page_size)
- )
- return [doc async for doc in cursor], total
- async def list_due_giveaways(limit: int = 20) -> list[dict[str, Any]]:
- await ensure_giveaway_indexes()
- cursor = (
- giveawaysdb.find(
- {
- "bot_id": BOT_PROFILE_ID,
- "$or": [
- {"status": STATUS_RUNNING, "ends_at": {"$lte": utc_now()}},
- {"status": STATUS_DRAWING},
- ]
- }
- )
- .sort("ends_at", ASCENDING)
- .limit(limit)
- )
- return [doc async for doc in cursor]
- async def list_unannounced_started_giveaways(limit: int = 20) -> list[dict[str, Any]]:
- await ensure_giveaway_indexes()
- now = utc_now()
- cursor = (
- giveawaysdb.find(
- {
- "bot_id": BOT_PROFILE_ID,
- "status": STATUS_RUNNING,
- "starts_at": {"$lte": now},
- "ends_at": {"$gt": now},
- "start_announced_at": {"$exists": False},
- }
- )
- .sort("starts_at", ASCENDING)
- .limit(limit)
- )
- return [doc async for doc in cursor]
- async def mark_giveaway_start_announced(giveaway_id: str) -> None:
- await ensure_giveaway_indexes()
- now = utc_now()
- await giveawaysdb.update_one(
- {
- "giveaway_id": normalize_giveaway_id(giveaway_id),
- "status": STATUS_RUNNING,
- "start_announced_at": {"$exists": False},
- },
- {"$set": {"start_announced_at": now, "updated_at": now}},
- )
- async def list_pending_cancellations(limit: int = 20) -> list[dict[str, Any]]:
- await ensure_giveaway_indexes()
- cursor = (
- giveawaysdb.find({"status": STATUS_CANCELING, "bot_id": BOT_PROFILE_ID})
- .sort("updated_at", ASCENDING)
- .limit(limit)
- )
- return [doc async for doc in cursor]
- async def list_pending_publications(limit: int = 20) -> list[dict[str, Any]]:
- await ensure_giveaway_indexes()
- cursor = giveawaysdb.find({
- "bot_id": BOT_PROFILE_ID,
- "status": STATUS_FINISHED,
- "publication_pending": True,
- }).sort("finished_at", ASCENDING).limit(limit)
- return [doc async for doc in cursor]
- async def claim_giveaway_draw(giveaway_id: str) -> dict[str, Any] | None:
- await ensure_giveaway_indexes()
- normalized_id = normalize_giveaway_id(giveaway_id)
- claimed = await giveawaysdb.find_one_and_update(
- {"giveaway_id": normalized_id, "status": STATUS_RUNNING},
- {
- "$set": {
- "status": STATUS_DRAWING,
- "draw_started_at": utc_now(),
- "updated_at": utc_now(),
- }
- },
- return_document=ReturnDocument.AFTER,
- )
- if claimed:
- return claimed
- return await giveawaysdb.find_one(
- {"giveaway_id": normalized_id, "status": STATUS_DRAWING}
- )
- async def save_pending_winners(
- giveaway_id: str, winners: list[dict[str, Any]]
- ) -> dict[str, Any] | None:
- await ensure_giveaway_indexes()
- normalized_id = normalize_giveaway_id(giveaway_id)
- updated = await giveawaysdb.find_one_and_update(
- {
- "giveaway_id": normalized_id,
- "status": STATUS_DRAWING,
- "pending_winners": {"$exists": False},
- },
- {
- "$set": {
- "pending_winners": winners,
- "draw_result_created_at": utc_now(),
- "updated_at": utc_now(),
- }
- },
- return_document=ReturnDocument.AFTER,
- )
- return updated or await giveawaysdb.find_one(
- {"giveaway_id": normalized_id, "status": STATUS_DRAWING}
- )
- async def finish_giveaway(
- giveaway_id: str, winners: list[dict[str, Any]] | None = None
- ) -> bool:
- await ensure_giveaway_indexes()
- values: dict[str, Any] = {
- "status": STATUS_FINISHED,
- "publication_pending": True,
- "finished_at": utc_now(),
- "updated_at": utc_now(),
- }
- if winners is not None:
- values["winners"] = winners
- result = await giveawaysdb.update_one(
- {
- "giveaway_id": normalize_giveaway_id(giveaway_id),
- "status": STATUS_DRAWING,
- },
- {"$set": values, "$unset": {"pending_winners": ""}},
- )
- return result.modified_count == 1
- async def claim_giveaway_cancel(
- giveaway_id: str, chat_id: int
- ) -> dict[str, Any] | None:
- await ensure_giveaway_indexes()
- normalized_id = normalize_giveaway_id(giveaway_id)
- claimed = await giveawaysdb.find_one_and_update(
- {
- "giveaway_id": normalized_id,
- "chat_id": int(chat_id),
- "$or": [
- {"status": STATUS_RUNNING},
- {"status": STATUS_DRAWING, "pending_winners": {"$exists": False}},
- ],
- },
- {
- "$set": {
- "status": STATUS_CANCELING,
- "cancel_started_at": utc_now(),
- "updated_at": utc_now(),
- }
- },
- return_document=ReturnDocument.AFTER,
- )
- if claimed:
- return claimed
- return await giveawaysdb.find_one(
- {
- "giveaway_id": normalized_id,
- "chat_id": int(chat_id),
- "status": STATUS_CANCELING,
- }
- )
- async def cancel_giveaway(giveaway_id: str, chat_id: int) -> bool:
- claimed = await claim_giveaway_cancel(giveaway_id, chat_id)
- if not claimed:
- return False
- return await finalize_giveaway_cancel(giveaway_id)
- async def finalize_giveaway_cancel(giveaway_id: str) -> bool:
- await ensure_giveaway_indexes()
- result = await giveawaysdb.update_one(
- {
- "giveaway_id": normalize_giveaway_id(giveaway_id),
- "status": STATUS_CANCELING,
- },
- {
- "$set": {
- "status": STATUS_CANCELED,
- "canceled_at": utc_now(),
- "updated_at": utc_now(),
- }
- },
- )
- return result.modified_count == 1
- async def record_reroll(
- giveaway_id: str,
- winners: list[dict[str, Any]],
- tier_name: str | None,
- moderator_id: int,
- reroll_id: str | None = None,
- ) -> str:
- await ensure_giveaway_indexes()
- reroll_id = reroll_id or token_hex(8)
- await giveawaysdb.update_one(
- {
- "giveaway_id": normalize_giveaway_id(giveaway_id),
- "status": STATUS_FINISHED,
- "rerolls.reroll_id": {"$ne": reroll_id},
- },
- {
- "$push": {
- "rerolls": {
- "reroll_id": reroll_id,
- "tier_name": tier_name,
- "moderator_id": int(moderator_id),
- "winners": winners,
- "created_at": utc_now(),
- }
- },
- "$set": {"updated_at": utc_now()},
- },
- )
- return reroll_id
- async def is_giveaway_banned(chat_id: int, user_id: int) -> bool:
- await ensure_giveaway_indexes()
- return (
- await giveaway_bansdb.find_one(
- {"chat_id": int(chat_id), "user_id": int(user_id)}
- )
- ) is not None
- async def add_giveaway_ban(
- *, chat_id: int, user_id: int, moderator_id: int, reason: str | None
- ) -> None:
- await ensure_giveaway_indexes()
- now = utc_now()
- await giveaway_bansdb.update_one(
- {"chat_id": int(chat_id), "user_id": int(user_id)},
- {
- "$set": {
- "moderator_id": int(moderator_id),
- "reason": (reason or "").strip()[:500],
- "updated_at": now,
- },
- "$setOnInsert": {"created_at": now},
- },
- upsert=True,
- )
- async def remove_giveaway_ban(chat_id: int, user_id: int) -> bool:
- await ensure_giveaway_indexes()
- result = await giveaway_bansdb.delete_one(
- {"chat_id": int(chat_id), "user_id": int(user_id)}
- )
- return result.deleted_count == 1
- async def list_giveaway_bans(
- *, chat_id: int, page: int = 1, page_size: int = 20
- ) -> tuple[list[dict[str, Any]], int]:
- await ensure_giveaway_indexes()
- filters = {"chat_id": int(chat_id)}
- page = max(1, int(page))
- page_size = max(1, min(int(page_size), 100))
- total = await giveaway_bansdb.count_documents(filters)
- cursor = (
- giveaway_bansdb.find(filters)
- .sort("created_at", DESCENDING)
- .skip((page - 1) * page_size)
- .limit(page_size)
- )
- return [doc async for doc in cursor], total
- async def add_participant(
- *,
- giveaway_id: str,
- user_id: int,
- username: str | None,
- first_name: str | None,
- display_name: str | None = None,
- ticket_count: int = 1,
- ) -> str:
- await ensure_giveaway_indexes()
- giveaway = await get_giveaway(giveaway_id)
- if not giveaway:
- return "missing"
- if giveaway["status"] != STATUS_RUNNING:
- return "closed"
- starts_at = giveaway.get("starts_at") or giveaway.get("created_at")
- if starts_at and as_utc(starts_at) > utc_now():
- return "not_started"
- if as_utc(giveaway["ends_at"]) <= utc_now():
- return "ended"
- if await is_giveaway_banned(giveaway["chat_id"], user_id):
- return "banned"
- try:
- await participantsdb.insert_one(
- {
- "giveaway_id": giveaway["giveaway_id"],
- "chat_id": int(giveaway["chat_id"]),
- "user_id": int(user_id),
- "username": username,
- "first_name": first_name,
- "display_name": display_name or first_name,
- "entry_cost": int(giveaway.get("entry_cost", 0)),
- "ticket_count": int(ticket_count),
- "paid_ticket_count": int(ticket_count),
- "active": True,
- "joined_at": utc_now(),
- }
- )
- except DuplicateKeyError:
- return "duplicate"
- return "ok"
- async def get_participant(giveaway_id: str, user_id: int) -> dict[str, Any] | None:
- await ensure_giveaway_indexes()
- return await participantsdb.find_one(
- {
- "giveaway_id": normalize_giveaway_id(giveaway_id),
- "user_id": int(user_id),
- }
- )
- async def remove_participant(
- *,
- giveaway_id: str,
- user_id: int,
- moderator_id: int,
- reason: str,
- refund: bool,
- ) -> dict[str, Any] | None:
- await ensure_giveaway_indexes()
- return await participantsdb.find_one_and_update(
- {
- "giveaway_id": normalize_giveaway_id(giveaway_id),
- "user_id": int(user_id),
- "active": {"$ne": False},
- },
- {
- "$set": {
- "active": False,
- "removed_at": utc_now(),
- "removed_by": int(moderator_id),
- "removal_reason": reason.strip()[:500],
- "refund_requested": bool(refund),
- }
- },
- return_document=ReturnDocument.BEFORE,
- )
- async def mark_participant_refunded(
- giveaway_id: str, user_id: int, refund_reason: str
- ) -> None:
- await ensure_giveaway_indexes()
- await participantsdb.update_one(
- {
- "giveaway_id": normalize_giveaway_id(giveaway_id),
- "user_id": int(user_id),
- },
- {
- "$set": {
- "refunded_at": utc_now(),
- "refund_reason": refund_reason[:100],
- }
- },
- )
- async def count_participants(giveaway_id: str, *, active_only: bool = True) -> int:
- await ensure_giveaway_indexes()
- filters: dict[str, Any] = {
- "giveaway_id": normalize_giveaway_id(giveaway_id)
- }
- if active_only:
- filters["active"] = {"$ne": False}
- filters["pending"] = {"$ne": True}
- return await participantsdb.count_documents(filters)
- async def list_participants(
- giveaway_id: str, limit: int | None = None, *, active_only: bool = True
- ) -> list[dict[str, Any]]:
- await ensure_giveaway_indexes()
- filters: dict[str, Any] = {
- "giveaway_id": normalize_giveaway_id(giveaway_id)
- }
- if active_only:
- filters["active"] = {"$ne": False}
- filters["pending"] = {"$ne": True}
- cursor = participantsdb.find(filters).sort("joined_at", ASCENDING)
- if limit is not None:
- cursor = cursor.limit(max(1, int(limit)))
- return [doc async for doc in cursor]
- async def list_participants_page(
- *,
- giveaway_id: str,
- active_only: bool = False,
- page: int = 1,
- page_size: int = 20,
- ) -> tuple[list[dict[str, Any]], int]:
- await ensure_giveaway_indexes()
- filters: dict[str, Any] = {
- "giveaway_id": normalize_giveaway_id(giveaway_id)
- }
- if active_only:
- filters["active"] = {"$ne": False}
- filters["pending"] = {"$ne": True}
- page = max(1, int(page))
- page_size = max(1, min(int(page_size), 100))
- total = await participantsdb.count_documents(filters)
- cursor = (
- participantsdb.find(filters)
- .sort("joined_at", ASCENDING)
- .skip((page - 1) * page_size)
- .limit(page_size)
- )
- return [doc async for doc in cursor], total
|