| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769 |
- from __future__ import annotations
- import asyncio
- import re
- from html import escape
- from typing import Any
- from pyrogram import filters
- from pyrogram.enums import ParseMode
- from pyrogram.types import (
- CallbackQuery,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- Message,
- )
- from wbb import app, log
- from wbb.core.decorators.permissions import adminsOnly
- from wbb.services.giveaway_templates import list_templates, materialize_template
- from wbb.services.giveaways import (
- GiveawayServiceError,
- buy_additional_tickets,
- cancel_and_refund_giveaway,
- create_and_publish_giveaway,
- finish_and_publish_giveaway,
- format_giveaway_time,
- giveaway_starts_at,
- join_giveaway,
- parse_giveaway_time,
- publish_finished,
- reconcile_pending_ticket_orders,
- refresh_giveaway_message,
- remove_and_optionally_refund_participant,
- render_winners,
- reroll_giveaway,
- resume_pending_giveaway,
- )
- from wbb.services.interaction_settings import command_enabled
- from wbb.services.member_identity import display_name
- from wbb.utils.dbgiveaway import (
- add_giveaway_ban,
- as_utc,
- count_participants,
- get_giveaway,
- giveawaysdb,
- list_due_giveaways,
- list_giveaways_page,
- list_participants_page,
- list_pending_cancellations,
- list_pending_publications,
- list_running_giveaways,
- list_unannounced_started_giveaways,
- mark_giveaway_start_announced,
- normalize_giveaway_id,
- remove_giveaway_ban,
- utc_now,
- )
- from wbb.utils.functions import extract_user_and_reason
- __MODULE__ = "抽奖管理"
- __HELP__ = """/giveaway <报名开始时间 ~ 开奖时间> | <奖项> | <标题> | [说明] | [最低积分] | [报名消耗] | [参与奖励]
- 在当前群创建抽奖。
- 群成员也可以直接发送“抽奖”查看当前抽奖、报名和参与者,
- 或发送“开奖”“中奖名单”查看最近一次开奖结果。
- 奖项格式:`1`、`一等奖:1, 二等奖:3`,或 `一等奖:1:50`;
- 最后的 50 表示该奖项每位中奖者获得 50 积分。
- 时间格式:`YYYY-MM-DD HH:mm`,按北京时间计算。
- /gjoin <编号> [张数] - 报名并购买奖票;免费抽奖只能领一张。
- /glist - 查看进行中的抽奖。
- /gparticipants <编号> - 查看参与者。
- /gend <编号> - 立即开奖。
- /gcancel <编号> - 取消抽奖并退还报名积分。
- /greroll <编号> [奖项] - 排除历次中奖者后重抽。
- /gremove <编号> <用户 ID> [no-refund] - 移除参与者,可选择不退款。
- /gban <成员> [原因] - 禁止成员参加本群抽奖。
- /gunban <成员> - 解除抽奖参与限制。"""
- MAX_TOTAL_WINNERS = 100
- PARTICIPANTS_PAGE_SIZE = 10
- SWEEP_INTERVAL_SECONDS = 30
- _sweeper_started = False
- def _parse_schedule(raw: str):
- pieces = re.split(r"\s*(?:~|至)\s*", raw.strip())
- if len(pieces) != 2:
- return None
- starts_at = parse_giveaway_time(pieces[0])
- ends_at = parse_giveaway_time(pieces[1])
- if not starts_at or not ends_at or starts_at >= ends_at or ends_at <= utc_now():
- return None
- return starts_at, ends_at
- def _parse_prizes(raw: str) -> list[dict[str, Any]] | None:
- raw = raw.strip()
- if raw.isdigit():
- count = int(raw)
- if 0 < count <= MAX_TOTAL_WINNERS:
- return [{"name": "中奖者", "count": count, "points_reward": 0}]
- return None
- prizes: list[dict[str, Any]] = []
- for piece in re.split(r"[,;\n]+", raw):
- parts = [part.strip() for part in re.split(r"[::]", piece.strip())]
- if len(parts) not in {2, 3} or not parts[0] or not parts[1].isdigit():
- return None
- count = int(parts[1])
- points_reward = int(parts[2]) if len(parts) == 3 and parts[2].isdigit() else 0
- if count <= 0 or points_reward < 0:
- return None
- prizes.append(
- {"name": parts[0], "count": count, "points_reward": points_reward}
- )
- if not prizes or sum(item["count"] for item in prizes) > MAX_TOTAL_WINNERS:
- return None
- return prizes
- def _parse_optional_points(pieces: list[str], index: int) -> int | None:
- if len(pieces) <= index or not pieces[index]:
- return 0
- if not pieces[index].isdigit():
- return None
- return int(pieces[index])
- def _create_usage() -> str:
- return (
- "<b>用法</b>\n"
- "/giveaway 2026-09-22 00:00 ~ 2026-09-22 23:59 | 1 | 月度会员抽奖\n"
- "/giveaway 2026-09-22 00:00 ~ 2026-09-23 20:00 | "
- "一等奖:1:50, 二等奖:3:10 | 活动抽奖 | "
- "抽奖说明 | 20 | 5 | 2"
- )
- def _actor_id(message: Message) -> int:
- if message.from_user:
- return message.from_user.id
- if message.sender_chat:
- return message.sender_chat.id
- return 0
- def _actor_name(message: Message) -> str:
- if message.from_user:
- return message.from_user.first_name or str(message.from_user.id)
- if message.sender_chat:
- return message.sender_chat.title or str(message.sender_chat.id)
- return "匿名管理员"
- def _participant_mention(participant: dict[str, Any]) -> str:
- user_id = int(participant["user_id"])
- label = (
- participant.get("display_name")
- or ("@" + str(participant["username"]) if participant.get("username") else None)
- or participant.get("first_name")
- or str(user_id)
- )
- return f'<a href="tg://user?id={user_id}">{escape(str(label))}</a>'
- def _participants_markup(
- giveaway_id: str, *, page: int, total_pages: int
- ) -> InlineKeyboardMarkup | None:
- if total_pages <= 1:
- return None
- buttons: list[InlineKeyboardButton] = []
- if page > 1:
- buttons.append(
- InlineKeyboardButton(
- "‹ 上一页",
- callback_data=f"giveaway_participants_page:{giveaway_id}:{page - 1}",
- )
- )
- buttons.append(
- InlineKeyboardButton(
- f"{page}/{total_pages}",
- callback_data=f"giveaway_participants_noop:{giveaway_id}",
- )
- )
- if page < total_pages:
- buttons.append(
- InlineKeyboardButton(
- "下一页 ›",
- callback_data=f"giveaway_participants_page:{giveaway_id}:{page + 1}",
- )
- )
- return InlineKeyboardMarkup([buttons])
- async def _render_participants_page(
- giveaway_id: str, page: int
- ) -> tuple[str, InlineKeyboardMarkup | None]:
- page = max(1, page)
- participants, total = await list_participants_page(
- giveaway_id=giveaway_id,
- active_only=True,
- page=page,
- page_size=PARTICIPANTS_PAGE_SIZE,
- )
- total_pages = max(1, (total + PARTICIPANTS_PAGE_SIZE - 1) // PARTICIPANTS_PAGE_SIZE)
- if page > total_pages:
- page = total_pages
- participants, total = await list_participants_page(
- giveaway_id=giveaway_id,
- active_only=True,
- page=page,
- page_size=PARTICIPANTS_PAGE_SIZE,
- )
- lines = [
- f"<b>抽奖 #{escape(giveaway_id)} 的参与者</b>",
- f"第 {page}/{total_pages} 页 · 共 {total} 人",
- ]
- if participants:
- offset = (page - 1) * PARTICIPANTS_PAGE_SIZE
- lines.extend(
- f"{offset + index}. {_participant_mention(item)}"
- f" · {int(item.get('paid_ticket_count', item.get('ticket_count', 1)))} 票"
- for index, item in enumerate(participants, 1)
- )
- else:
- lines.append("暂时还没有参与者。")
- return "\n".join(lines), _participants_markup(
- giveaway_id, page=page, total_pages=total_pages
- )
- def _keyword_giveaways_markup(
- giveaways: list[dict[str, Any]],
- ) -> InlineKeyboardMarkup:
- rows: list[list[InlineKeyboardButton]] = []
- for giveaway in giveaways:
- giveaway_id = giveaway["giveaway_id"]
- title = str(giveaway["title"])
- short_title = title if len(title) <= 12 else title[:11] + "…"
- rows.append(
- [
- InlineKeyboardButton(
- f"报名 · {short_title}",
- callback_data=f"giveaway_join:{giveaway_id}",
- ),
- InlineKeyboardButton(
- "查看参与者",
- callback_data=f"giveaway_participants:{giveaway_id}",
- ),
- ]
- )
- return InlineKeyboardMarkup(rows)
- def _winners_markup(
- giveaway_id: str, *, page: int, total_pages: int
- ) -> InlineKeyboardMarkup | None:
- if total_pages <= 1:
- return None
- buttons: list[InlineKeyboardButton] = []
- if page > 1:
- buttons.append(
- InlineKeyboardButton(
- "‹ 上一页",
- callback_data=f"giveaway_winners_page:{giveaway_id}:{page - 1}",
- )
- )
- buttons.append(
- InlineKeyboardButton(
- f"{page}/{total_pages}",
- callback_data=f"giveaway_winners_noop:{giveaway_id}",
- )
- )
- if page < total_pages:
- buttons.append(
- InlineKeyboardButton(
- "下一页 ›",
- callback_data=f"giveaway_winners_page:{giveaway_id}:{page + 1}",
- )
- )
- return InlineKeyboardMarkup([buttons])
- def _render_winners_page(
- giveaway: dict[str, Any], page: int
- ) -> tuple[str, InlineKeyboardMarkup | None]:
- winners = list(giveaway.get("winners", []))
- total = len(winners)
- total_pages = max(1, (total + PARTICIPANTS_PAGE_SIZE - 1) // PARTICIPANTS_PAGE_SIZE)
- page = min(max(1, page), total_pages)
- offset = (page - 1) * PARTICIPANTS_PAGE_SIZE
- current = winners[offset : offset + PARTICIPANTS_PAGE_SIZE]
- lines = [
- f"<b>抽奖结果 #{escape(giveaway['giveaway_id'])}</b>",
- f"<b>标题:</b>{escape(giveaway['title'])}",
- f"第 {page}/{total_pages} 页 · 共 {total} 位中奖者",
- ]
- if current:
- for index, winner in enumerate(current, offset + 1):
- points = int(winner.get("points_reward", 0))
- suffix = f"(+{points} 积分)" if points else ""
- lines.append(
- f"{index}. {escape(str(winner['tier']))}:"
- f"{_participant_mention(winner)}{suffix}"
- )
- else:
- lines.append("本次没有符合条件的中奖者。")
- return "\n".join(lines), _winners_markup(
- giveaway["giveaway_id"], page=page, total_pages=total_pages
- )
- async def _join_user(
- giveaway_id: str, user, *, quantity: int = 1, request_id: str = ""
- ) -> str:
- giveaway = await get_giveaway(giveaway_id)
- if not giveaway:
- return "未找到该抽奖。"
- paid = int(giveaway.get("entry_cost", 0)) > 0
- if paid:
- status, _ = await buy_additional_tickets(
- giveaway_id, user_id=user.id, quantity=quantity,
- request_id=request_id or f"join:{user.id}",
- username=user.username, first_name=user.first_name,
- display_name=display_name(user.first_name, user.last_name),
- )
- elif quantity != 1:
- status = "free_single_ticket"
- else:
- status, _ = await join_giveaway(
- giveaway_id,
- user_id=user.id,
- username=user.username,
- first_name=user.first_name,
- display_name=display_name(user.first_name, user.last_name),
- )
- return {
- "ok": f"已购买 {quantity} 张奖票。" if paid else "报名成功。",
- "duplicate": "该次购买已经处理。" if paid else "你已经报名过该抽奖。",
- "missing": "未找到该抽奖。",
- "closed": "该抽奖已经关闭。",
- "not_started": "该抽奖尚未开始报名。",
- "ended": "该抽奖已经结束。",
- "banned": "你已被禁止参加本群抽奖。",
- "not_member": "只有当前群成员可以参加该抽奖。",
- "not_eligible": "尚未满足抽奖要求的频道或群成员资格。",
- "eligibility_unavailable": "资格核验暂不可用,请稍后重试。",
- "insufficient_points": "你的积分不足,无法报名。",
- "ticket_limit": "已达到本期每人奖票上限。",
- "free_single_ticket": "免费抽奖每人只能领取一张奖票。",
- "invalid_quantity": "奖票数量无效。",
- "not_joined": "该成员已被移除,不能继续购买奖票。",
- }.get(status, "报名失败,请稍后重试。")
- async def _sweep_due_giveaways() -> None:
- await asyncio.sleep(5)
- while True:
- try:
- await reconcile_pending_ticket_orders()
- for template in await list_templates():
- try:
- await materialize_template(template)
- except Exception as exc:
- log.error(f"循环抽奖模板 {template.get('template_id')} 生成失败:{exc}")
- for giveaway in await list_unannounced_started_giveaways():
- try:
- if giveaway.get("template_id"):
- previous = await giveawaysdb.find_one(
- {
- "template_id": giveaway["template_id"],
- "period_key": {"$lt": giveaway["period_key"]},
- "result_pinned": True,
- },
- sort=[("period_key", -1)],
- )
- if previous and previous.get("result_message_id"):
- await app.unpin_chat_message(
- int(giveaway["chat_id"]), int(previous["result_message_id"])
- )
- await giveawaysdb.update_one(
- {"giveaway_id": previous["giveaway_id"]},
- {"$set": {"result_pinned": False}},
- )
- if await refresh_giveaway_message(giveaway):
- await mark_giveaway_start_announced(giveaway["giveaway_id"])
- except Exception as exc:
- log.error(f"抽奖 {giveaway.get('giveaway_id')} 开始处理失败:{exc}")
- pending = await list_due_giveaways()
- pending.extend(await list_pending_cancellations())
- for giveaway in pending:
- await resume_pending_giveaway(giveaway)
- for giveaway in await list_pending_publications():
- await publish_finished(
- giveaway, list(giveaway.get("winners", []))
- )
- except Exception as exc:
- log.error(f"抽奖定时任务失败:{exc}")
- await asyncio.sleep(SWEEP_INTERVAL_SECONDS)
- def _start_sweeper() -> None:
- global _sweeper_started
- if _sweeper_started:
- return
- try:
- loop = asyncio.get_running_loop()
- except RuntimeError:
- return
- _sweeper_started = True
- loop.create_task(_sweep_due_giveaways(), name="giveaway-sweeper")
- @app.on_message(filters.command(["giveaway", "gcreate"]) & ~filters.private)
- @adminsOnly("can_change_info")
- async def create_giveaway_command(_, message: Message):
- payload = message.text.split(None, 1)[1].strip() if message.text and " " in message.text else ""
- pieces = [piece.strip() for piece in payload.split("|")]
- if len(pieces) < 3:
- return await message.reply_text(_create_usage(), parse_mode=ParseMode.HTML)
- schedule = _parse_schedule(pieces[0])
- prizes = _parse_prizes(pieces[1])
- minimum_points = _parse_optional_points(pieces, 4)
- entry_cost = _parse_optional_points(pieces, 5)
- participation_reward = _parse_optional_points(pieces, 6)
- if (
- not schedule
- or not prizes
- or not pieces[2]
- or minimum_points is None
- or entry_cost is None
- or participation_reward is None
- ):
- return await message.reply_text(_create_usage(), parse_mode=ParseMode.HTML)
- starts_at, ends_at = schedule
- giveaway = await create_and_publish_giveaway(
- chat_id=message.chat.id,
- creator_id=_actor_id(message),
- creator_name=_actor_name(message),
- title=pieces[2],
- description=pieces[3] if len(pieces) > 3 else "",
- prizes=prizes,
- starts_at=starts_at,
- ends_at=ends_at,
- minimum_points=minimum_points,
- entry_cost=entry_cost,
- participation_reward=participation_reward,
- )
- return await message.reply_text(f"抽奖 #{giveaway['giveaway_id']} 已创建。")
- @app.on_callback_query(filters.regex(r"^giveaway_join:"))
- async def giveaway_join_callback(_, query: CallbackQuery):
- if not await command_enabled(query.message.chat.id, "giveaway"):
- return await query.answer("本群已关闭抽奖指令。", show_alert=True)
- giveaway_id = normalize_giveaway_id(query.data.split(":", 1)[1])
- await query.answer(
- await _join_user(giveaway_id, query.from_user, request_id=str(query.id)),
- show_alert=True,
- )
- @app.on_callback_query(filters.regex(r"^giveaway_buy:[a-f0-9]+:(?:1|5)$"))
- async def giveaway_buy_callback(_, query: CallbackQuery):
- if not await command_enabled(query.message.chat.id, "giveaway"):
- return await query.answer("本群已关闭抽奖指令。", show_alert=True)
- _, giveaway_id, quantity = query.data.split(":")
- await query.answer(
- await _join_user(
- giveaway_id, query.from_user,
- quantity=int(quantity), request_id=str(query.id),
- ),
- show_alert=True,
- )
- @app.on_callback_query(filters.regex(r"^giveaway_participants:[a-f0-9]+$"))
- async def giveaway_participants_callback(_, query: CallbackQuery):
- giveaway_id = normalize_giveaway_id(query.data.rsplit(":", 1)[1])
- giveaway = await get_giveaway(giveaway_id)
- if not giveaway or int(giveaway["chat_id"]) != int(query.message.chat.id):
- return await query.answer("本群中未找到该抽奖。", show_alert=True)
- text, markup = await _render_participants_page(giveaway_id, 1)
- await query.message.reply_text(
- text,
- parse_mode=ParseMode.HTML,
- reply_markup=markup,
- disable_web_page_preview=True,
- )
- return await query.answer()
- @app.on_callback_query(
- filters.regex(r"^giveaway_participants_page:[a-f0-9]+:\d+$")
- )
- async def giveaway_participants_page_callback(_, query: CallbackQuery):
- _, giveaway_id, raw_page = query.data.split(":", 2)
- giveaway_id = normalize_giveaway_id(giveaway_id)
- giveaway = await get_giveaway(giveaway_id)
- if not giveaway or int(giveaway["chat_id"]) != int(query.message.chat.id):
- return await query.answer("本群中未找到该抽奖。", show_alert=True)
- text, markup = await _render_participants_page(giveaway_id, int(raw_page))
- await query.message.edit_text(
- text,
- parse_mode=ParseMode.HTML,
- reply_markup=markup,
- disable_web_page_preview=True,
- )
- return await query.answer()
- @app.on_callback_query(filters.regex(r"^giveaway_participants_noop:"))
- async def giveaway_participants_noop_callback(_, query: CallbackQuery):
- return await query.answer()
- @app.on_callback_query(filters.regex(r"^giveaway_winners_page:[a-f0-9]+:\d+$"))
- async def giveaway_winners_page_callback(_, query: CallbackQuery):
- _, giveaway_id, raw_page = query.data.split(":", 2)
- giveaway_id = normalize_giveaway_id(giveaway_id)
- giveaway = await get_giveaway(giveaway_id)
- if (
- not giveaway
- or giveaway["status"] != "finished"
- or int(giveaway["chat_id"]) != int(query.message.chat.id)
- ):
- return await query.answer("本群中未找到该开奖结果。", show_alert=True)
- text, markup = _render_winners_page(giveaway, int(raw_page))
- await query.message.edit_text(
- text,
- parse_mode=ParseMode.HTML,
- reply_markup=markup,
- disable_web_page_preview=True,
- )
- return await query.answer()
- @app.on_callback_query(filters.regex(r"^giveaway_winners_noop:"))
- async def giveaway_winners_noop_callback(_, query: CallbackQuery):
- return await query.answer()
- @app.on_message(filters.command("gjoin") & ~filters.private)
- async def giveaway_join_command(_, message: Message):
- if not await command_enabled(message.chat.id, "giveaway"):
- return None
- if not message.from_user:
- return await message.reply_text("只有普通用户身份可以参加抽奖。")
- if len(message.command) < 2:
- return await message.reply_text("用法:/gjoin <抽奖编号> [奖票张数]")
- try:
- quantity = int(message.command[2]) if len(message.command) > 2 else 1
- except ValueError:
- return await message.reply_text("奖票张数必须是整数。")
- return await message.reply_text(await _join_user(
- message.command[1], message.from_user, quantity=quantity,
- request_id=f"message:{message.chat.id}:{message.id}",
- ))
- @app.on_message(filters.regex(r"^抽奖$") & filters.group)
- async def giveaway_keyword_command(_, message: Message):
- if not await command_enabled(message.chat.id, "giveaway"):
- return None
- giveaways = await list_running_giveaways(chat_id=message.chat.id, limit=10)
- now = utc_now()
- giveaways = [
- giveaway for giveaway in giveaways if as_utc(giveaway["ends_at"]) > now
- ]
- if not giveaways:
- return await message.reply_text("本群当前没有进行中的抽奖。")
- lines = ["<b>本群抽奖</b>"]
- for giveaway in giveaways:
- state = "报名中" if giveaway_starts_at(giveaway) <= now else "待报名"
- lines.append(
- "#{id} · {title} · {state} · {count} 人参与 · {ends_at} 开奖".format(
- id=escape(giveaway["giveaway_id"]),
- title=escape(giveaway["title"]),
- state=state,
- count=await count_participants(giveaway["giveaway_id"]),
- ends_at=format_giveaway_time(giveaway["ends_at"]),
- )
- )
- return await message.reply_text(
- "\n".join(lines),
- parse_mode=ParseMode.HTML,
- reply_markup=_keyword_giveaways_markup(giveaways),
- disable_web_page_preview=True,
- )
- @app.on_message(filters.regex(r"^(?:开奖|中奖名单)$") & filters.group)
- async def giveaway_winners_keyword_command(_, message: Message):
- if not await command_enabled(message.chat.id, "giveaway"):
- return None
- giveaways, _ = await list_giveaways_page(
- chat_id=message.chat.id,
- status="finished",
- page=1,
- page_size=20,
- )
- if not giveaways:
- return await message.reply_text("本群暂无已开奖的抽奖。")
- giveaway = max(
- giveaways,
- key=lambda item: as_utc(item.get("finished_at") or item["updated_at"]),
- )
- text, markup = _render_winners_page(giveaway, 1)
- return await message.reply_text(
- text,
- parse_mode=ParseMode.HTML,
- reply_markup=markup,
- disable_web_page_preview=True,
- )
- @app.on_message(filters.command("glist") & ~filters.private)
- @adminsOnly("can_change_info")
- async def list_giveaways_command(_, message: Message):
- giveaways = await list_running_giveaways(chat_id=message.chat.id, limit=20)
- if not giveaways:
- return await message.reply_text("本群当前没有进行中的抽奖。")
- lines = ["<b>进行中的抽奖</b>"]
- for giveaway in giveaways:
- lines.append(
- "#{id} - {title} - {count} 人参与 - 开奖于 {ends_at}".format(
- id=escape(giveaway["giveaway_id"]),
- title=escape(giveaway["title"]),
- count=await count_participants(giveaway["giveaway_id"]),
- ends_at=format_giveaway_time(giveaway["ends_at"]),
- )
- )
- return await message.reply_text("\n".join(lines), parse_mode=ParseMode.HTML)
- @app.on_message(filters.command("gparticipants") & ~filters.private)
- @adminsOnly("can_change_info")
- async def participants_command(_, message: Message):
- if len(message.command) < 2:
- return await message.reply_text("用法:/gparticipants <抽奖编号>")
- giveaway_id = normalize_giveaway_id(message.command[1])
- giveaway = await get_giveaway(giveaway_id)
- if not giveaway or giveaway["chat_id"] != message.chat.id:
- return await message.reply_text("本群中未找到该抽奖。")
- text, markup = await _render_participants_page(giveaway_id, 1)
- return await message.reply_text(
- text,
- parse_mode=ParseMode.HTML,
- reply_markup=markup,
- disable_web_page_preview=True,
- )
- @app.on_message(filters.command("gend") & ~filters.private)
- @adminsOnly("can_change_info")
- async def end_giveaway_command(_, message: Message):
- if len(message.command) < 2:
- return await message.reply_text("用法:/gend <抽奖编号>")
- giveaway_id = normalize_giveaway_id(message.command[1])
- giveaway = await get_giveaway(giveaway_id)
- if not giveaway or giveaway["chat_id"] != message.chat.id:
- return await message.reply_text("本群中未找到该抽奖。")
- _, text, _ = await finish_and_publish_giveaway(giveaway_id)
- return await message.reply_text(text)
- @app.on_message(filters.command("gcancel") & ~filters.private)
- @adminsOnly("can_change_info")
- async def cancel_giveaway_command(_, message: Message):
- if len(message.command) < 2:
- return await message.reply_text("用法:/gcancel <抽奖编号>")
- giveaway_id = normalize_giveaway_id(message.command[1])
- giveaway = await get_giveaway(giveaway_id)
- if not giveaway or giveaway["chat_id"] != message.chat.id:
- return await message.reply_text("本群中未找到该抽奖。")
- _, text, _ = await cancel_and_refund_giveaway(
- giveaway_id, chat_id=message.chat.id
- )
- return await message.reply_text(text)
- @app.on_message(filters.command("greroll") & ~filters.private)
- @adminsOnly("can_change_info")
- async def reroll_giveaway_command(_, message: Message):
- parts = message.text.split(None, 2) if message.text else []
- if len(parts) < 2:
- return await message.reply_text("用法:/greroll <抽奖编号> [奖项名称]")
- giveaway_id = normalize_giveaway_id(parts[1])
- giveaway = await get_giveaway(giveaway_id)
- if not giveaway or giveaway["chat_id"] != message.chat.id:
- return await message.reply_text("本群中未找到该抽奖。")
- try:
- winners, _ = await reroll_giveaway(
- giveaway_id=giveaway_id,
- moderator_id=_actor_id(message),
- tier_name=parts[2].strip() if len(parts) > 2 else None,
- publish=False,
- )
- except GiveawayServiceError as exc:
- return await message.reply_text(str(exc))
- return await message.reply_text(
- render_winners(
- giveaway,
- winners,
- reroll=True,
- tier_name=parts[2].strip() if len(parts) > 2 else None,
- ),
- parse_mode=ParseMode.HTML,
- disable_web_page_preview=True,
- )
- @app.on_message(filters.command("gremove") & ~filters.private)
- @adminsOnly("can_change_info")
- async def remove_participant_command(_, message: Message):
- if len(message.command) < 3 or not message.command[2].lstrip("-").isdigit():
- return await message.reply_text(
- "用法:/gremove <抽奖编号> <用户 ID> [no-refund]"
- )
- giveaway_id = normalize_giveaway_id(message.command[1])
- giveaway = await get_giveaway(giveaway_id)
- if not giveaway or giveaway["chat_id"] != message.chat.id:
- return await message.reply_text("本群中未找到该抽奖。")
- refund = not (len(message.command) > 3 and message.command[3].lower() == "no-refund")
- removed = await remove_and_optionally_refund_participant(
- giveaway_id=giveaway_id,
- user_id=int(message.command[2]),
- moderator_id=_actor_id(message),
- reason="由群管理员移除",
- refund=refund,
- )
- return await message.reply_text(
- "参与者已移除。"
- if removed
- else "未找到该参与者,或该参与者此前已被移除。"
- )
- @app.on_message(filters.command("gban") & ~filters.private)
- @adminsOnly("can_change_info")
- async def giveaway_ban_command(_, message: Message):
- user_id, reason = await extract_user_and_reason(message)
- if not user_id:
- return await message.reply_text("用法:/gban <成员> [原因]")
- await add_giveaway_ban(
- chat_id=message.chat.id,
- user_id=int(user_id),
- moderator_id=_actor_id(message),
- reason=reason,
- )
- return await message.reply_text(f"已禁止用户 {user_id} 参加本群抽奖。")
- @app.on_message(filters.command("gunban") & ~filters.private)
- @adminsOnly("can_change_info")
- async def giveaway_unban_command(_, message: Message):
- user_id, _ = await extract_user_and_reason(message)
- if not user_id:
- return await message.reply_text("用法:/gunban <成员>")
- removed = await remove_giveaway_ban(message.chat.id, int(user_id))
- return await message.reply_text(
- f"已允许用户 {user_id} 参加本群抽奖。"
- if removed
- else "该用户没有被限制参加抽奖。"
- )
- _start_sweeper()
|