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 (
"用法\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'{escape(str(label))}'
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"抽奖 #{escape(giveaway_id)} 的参与者",
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"抽奖结果 #{escape(giveaway['giveaway_id'])}",
f"标题:{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 = ["本群抽奖"]
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 = ["进行中的抽奖"]
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()