| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- from __future__ import annotations
- from calendar import monthrange
- from datetime import datetime, timedelta
- from secrets import token_hex
- from typing import Any
- from zoneinfo import ZoneInfo
- from wbb import BOT_PROFILE_ID, app
- from wbb.services.giveaway_eligibility import validate_targets
- from wbb.utils.dbgiveaway import (
- as_utc,
- attach_giveaway_message,
- create_giveaway,
- ensure_giveaway_indexes,
- giveawaysdb,
- normalize_prizes,
- templatesdb,
- utc_now,
- )
- TIMEZONE = ZoneInfo("Asia/Shanghai")
- def _time(value: str) -> tuple[int, int]:
- try:
- parsed = datetime.strptime(value, "%H:%M")
- except (TypeError, ValueError) as exc:
- raise ValueError("时间格式必须为 HH:mm。") from exc
- return parsed.hour, parsed.minute
- def period_schedule(template: dict[str, Any], year: int, month: int) -> tuple[datetime, datetime]:
- day = min(int(template["day_of_month"]), monthrange(year, month)[1])
- start_hour, start_minute = _time(template["start_time"])
- draw_hour, draw_minute = _time(template["draw_time"])
- local_start = datetime(year, month, day, start_hour, start_minute, tzinfo=TIMEZONE)
- local_draw = datetime(year, month, day, draw_hour, draw_minute, tzinfo=TIMEZONE)
- local_draw += timedelta(days=int(template["draw_day_offset"]))
- if local_draw <= local_start:
- raise ValueError("开奖时间必须晚于报名开始时间。")
- return as_utc(local_start), as_utc(local_draw)
- def normalize_template(body: dict[str, Any]) -> dict[str, Any]:
- day = int(body.get("day_of_month", 1))
- offset = int(body.get("draw_day_offset", 0))
- if not 1 <= day <= 31 or offset not in {0, 1}:
- raise ValueError("每月日期必须为 1 至 31,开奖只能在当天或次日。")
- mode = str(body.get("eligibility_mode", "all"))
- if mode not in {"all", "any"}:
- raise ValueError("资格条件组合方式必须是全部或任一。")
- cost = int(body.get("entry_cost", 0))
- limit = int(body.get("max_tickets_per_user", 10))
- if cost < 0 or not 1 <= limit <= 100:
- raise ValueError("积分和奖票上限无效。")
- values = {
- "chat_id": int(body["chat_id"]),
- "title": str(body.get("title") or "").strip()[:200],
- "description": str(body.get("description") or "").strip()[:4000],
- "prizes": normalize_prizes(body["prizes"]),
- "minimum_points": int(body.get("minimum_points", 0)),
- "entry_cost": cost,
- "participation_reward": int(body.get("participation_reward", 0)),
- "max_tickets_per_user": limit if cost else 1,
- "eligibility_targets": list(body.get("eligibility_targets") or []),
- "eligibility_mode": mode,
- "day_of_month": day,
- "start_time": str(body.get("start_time", "00:00")),
- "draw_day_offset": offset,
- "draw_time": str(body.get("draw_time", "20:00")),
- }
- if not values["title"] or any(values[key] < 0 for key in ("minimum_points", "participation_reward")):
- raise ValueError("标题和积分配置无效。")
- period_schedule(values, 2024, 2)
- return values
- async def create_template(body: dict[str, Any], creator_name: str) -> dict[str, Any]:
- await ensure_giveaway_indexes()
- values = normalize_template(body)
- now = utc_now()
- template = {
- **values, "template_id": token_hex(8), "bot_id": BOT_PROFILE_ID,
- "creator_name": creator_name, "active": True,
- "created_at": now, "updated_at": now,
- }
- await templatesdb.insert_one(template)
- return template
- async def list_templates(chat_id: int | None = None) -> list[dict[str, Any]]:
- await ensure_giveaway_indexes()
- query: dict[str, Any] = {"bot_id": BOT_PROFILE_ID}
- if chat_id is not None:
- query["chat_id"] = int(chat_id)
- return [item async for item in templatesdb.find(query).sort("created_at", -1)]
- async def update_template(template_id: str, body: dict[str, Any]) -> dict[str, Any] | None:
- await ensure_giveaway_indexes()
- changes = normalize_template(body)
- changes["updated_at"] = utc_now()
- if "active" in body:
- changes["active"] = bool(body["active"])
- from pymongo import ReturnDocument
- return await templatesdb.find_one_and_update(
- {"template_id": template_id, "bot_id": BOT_PROFILE_ID},
- {"$set": changes}, return_document=ReturnDocument.AFTER,
- )
- async def materialize_template(template: dict[str, Any], now: datetime | None = None) -> None:
- await ensure_giveaway_indexes()
- if not template.get("active"):
- return
- now = now or utc_now()
- local = now.astimezone(TIMEZONE)
- periods = {(local.year, local.month)}
- previous = local.replace(day=1) - timedelta(days=1)
- periods.add((previous.year, previous.month))
- from pyrogram.enums import ParseMode
- from wbb.services.giveaways import join_markup, render_giveaway
- for year, month in sorted(periods):
- starts_at, ends_at = period_schedule(template, year, month)
- if starts_at > now or ends_at <= now or as_utc(template["created_at"]) > ends_at:
- continue
- period_key = f"{year:04d}-{month:02d}"
- existing = await giveawaysdb.find_one(
- {"template_id": template["template_id"], "period_key": period_key}
- )
- if existing and (existing.get("message_id") or existing["status"] != "running"):
- continue
- if not existing:
- try:
- await validate_targets(list(template.get("eligibility_targets") or []))
- except ValueError as exc:
- await templatesdb.update_one(
- {"template_id": template["template_id"]},
- {"$set": {"last_error": str(exc), "last_error_at": utc_now()}},
- )
- raise
- await templatesdb.update_one(
- {"template_id": template["template_id"]},
- {"$unset": {"last_error": "", "last_error_at": ""}},
- )
- giveaway = existing or await create_giveaway(
- chat_id=int(template["chat_id"]), creator_id=0,
- creator_name=template["creator_name"], title=template["title"],
- description=template["description"], prizes=template["prizes"],
- starts_at=starts_at, ends_at=ends_at,
- minimum_points=template["minimum_points"], entry_cost=template["entry_cost"],
- participation_reward=template["participation_reward"],
- max_tickets_per_user=template["max_tickets_per_user"],
- eligibility_targets=template["eligibility_targets"],
- eligibility_mode=template["eligibility_mode"],
- template_id=template["template_id"], period_key=period_key,
- )
- if giveaway.get("message_id"):
- continue
- if as_utc(giveaway["ends_at"]) <= utc_now():
- continue
- # Bot accounts cannot call messages.GetHistory. The persisted message ID
- # is the only reliable indicator that this period was announced.
- sent = await app.send_message(
- int(template["chat_id"]), await render_giveaway(giveaway),
- parse_mode=ParseMode.HTML,
- reply_markup=join_markup(
- giveaway["giveaway_id"], int(giveaway.get("entry_cost", 0))
- ),
- disable_web_page_preview=True,
- )
- await attach_giveaway_message(
- giveaway["giveaway_id"], int(template["chat_id"]), sent.id
- )
|