giveaway.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. from __future__ import annotations
  2. import asyncio
  3. import re
  4. from html import escape
  5. from typing import Any
  6. from pyrogram import filters
  7. from pyrogram.enums import ParseMode
  8. from pyrogram.types import (
  9. CallbackQuery,
  10. InlineKeyboardButton,
  11. InlineKeyboardMarkup,
  12. Message,
  13. )
  14. from wbb import app, log
  15. from wbb.core.decorators.permissions import adminsOnly
  16. from wbb.services.giveaways import (
  17. GiveawayServiceError,
  18. cancel_and_refund_giveaway,
  19. create_and_publish_giveaway,
  20. finish_and_publish_giveaway,
  21. format_giveaway_time,
  22. giveaway_starts_at,
  23. join_giveaway,
  24. parse_giveaway_time,
  25. refresh_giveaway_message,
  26. remove_and_optionally_refund_participant,
  27. render_winners,
  28. reroll_giveaway,
  29. resume_pending_giveaway,
  30. )
  31. from wbb.services.member_identity import display_name
  32. from wbb.utils.dbgiveaway import (
  33. add_giveaway_ban,
  34. as_utc,
  35. count_participants,
  36. get_giveaway,
  37. list_due_giveaways,
  38. list_giveaways_page,
  39. list_participants_page,
  40. list_pending_cancellations,
  41. list_running_giveaways,
  42. list_unannounced_started_giveaways,
  43. mark_giveaway_start_announced,
  44. normalize_giveaway_id,
  45. remove_giveaway_ban,
  46. utc_now,
  47. )
  48. from wbb.utils.functions import extract_user_and_reason
  49. __MODULE__ = "抽奖管理"
  50. __HELP__ = """/giveaway <报名开始时间 ~ 开奖时间> | <奖项> | <标题> | [说明] | [最低积分] | [报名消耗] | [参与奖励]
  51. 在当前群创建抽奖。
  52. 群成员也可以直接发送“抽奖”查看当前抽奖、报名和参与者,
  53. 或发送“开奖”“中奖名单”查看最近一次开奖结果。
  54. 奖项格式:`1`、`一等奖:1, 二等奖:3`,或 `一等奖:1:50`;
  55. 最后的 50 表示该奖项每位中奖者获得 50 积分。
  56. 时间格式:`YYYY-MM-DD HH:mm`,按北京时间计算。
  57. /gjoin <编号> - 报名参加抽奖。
  58. /glist - 查看进行中的抽奖。
  59. /gparticipants <编号> - 查看参与者。
  60. /gend <编号> - 立即开奖。
  61. /gcancel <编号> - 取消抽奖并退还报名积分。
  62. /greroll <编号> [奖项] - 排除历次中奖者后重抽。
  63. /gremove <编号> <用户 ID> [no-refund] - 移除参与者,可选择不退款。
  64. /gban <成员> [原因] - 禁止成员参加本群抽奖。
  65. /gunban <成员> - 解除抽奖参与限制。"""
  66. MAX_TOTAL_WINNERS = 100
  67. PARTICIPANTS_PAGE_SIZE = 10
  68. SWEEP_INTERVAL_SECONDS = 30
  69. _sweeper_started = False
  70. def _parse_schedule(raw: str):
  71. pieces = re.split(r"\s*(?:~|至)\s*", raw.strip())
  72. if len(pieces) != 2:
  73. return None
  74. starts_at = parse_giveaway_time(pieces[0])
  75. ends_at = parse_giveaway_time(pieces[1])
  76. if not starts_at or not ends_at or starts_at >= ends_at or ends_at <= utc_now():
  77. return None
  78. return starts_at, ends_at
  79. def _parse_prizes(raw: str) -> list[dict[str, Any]] | None:
  80. raw = raw.strip()
  81. if raw.isdigit():
  82. count = int(raw)
  83. if 0 < count <= MAX_TOTAL_WINNERS:
  84. return [{"name": "中奖者", "count": count, "points_reward": 0}]
  85. return None
  86. prizes: list[dict[str, Any]] = []
  87. for piece in re.split(r"[,;\n]+", raw):
  88. parts = [part.strip() for part in re.split(r"[::]", piece.strip())]
  89. if len(parts) not in {2, 3} or not parts[0] or not parts[1].isdigit():
  90. return None
  91. count = int(parts[1])
  92. points_reward = int(parts[2]) if len(parts) == 3 and parts[2].isdigit() else 0
  93. if count <= 0 or points_reward < 0:
  94. return None
  95. prizes.append(
  96. {"name": parts[0], "count": count, "points_reward": points_reward}
  97. )
  98. if not prizes or sum(item["count"] for item in prizes) > MAX_TOTAL_WINNERS:
  99. return None
  100. return prizes
  101. def _parse_optional_points(pieces: list[str], index: int) -> int | None:
  102. if len(pieces) <= index or not pieces[index]:
  103. return 0
  104. if not pieces[index].isdigit():
  105. return None
  106. return int(pieces[index])
  107. def _create_usage() -> str:
  108. return (
  109. "<b>用法</b>\n"
  110. "/giveaway 2026-09-22 00:00 ~ 2026-09-22 23:59 | 1 | 月度会员抽奖\n"
  111. "/giveaway 2026-09-22 00:00 ~ 2026-09-23 20:00 | "
  112. "一等奖:1:50, 二等奖:3:10 | 活动抽奖 | "
  113. "抽奖说明 | 20 | 5 | 2"
  114. )
  115. def _actor_id(message: Message) -> int:
  116. if message.from_user:
  117. return message.from_user.id
  118. if message.sender_chat:
  119. return message.sender_chat.id
  120. return 0
  121. def _actor_name(message: Message) -> str:
  122. if message.from_user:
  123. return message.from_user.first_name or str(message.from_user.id)
  124. if message.sender_chat:
  125. return message.sender_chat.title or str(message.sender_chat.id)
  126. return "匿名管理员"
  127. def _participant_mention(participant: dict[str, Any]) -> str:
  128. user_id = int(participant["user_id"])
  129. label = (
  130. participant.get("display_name")
  131. or ("@" + str(participant["username"]) if participant.get("username") else None)
  132. or participant.get("first_name")
  133. or str(user_id)
  134. )
  135. return f'<a href="tg://user?id={user_id}">{escape(str(label))}</a>'
  136. def _participants_markup(
  137. giveaway_id: str, *, page: int, total_pages: int
  138. ) -> InlineKeyboardMarkup | None:
  139. if total_pages <= 1:
  140. return None
  141. buttons: list[InlineKeyboardButton] = []
  142. if page > 1:
  143. buttons.append(
  144. InlineKeyboardButton(
  145. "‹ 上一页",
  146. callback_data=f"giveaway_participants_page:{giveaway_id}:{page - 1}",
  147. )
  148. )
  149. buttons.append(
  150. InlineKeyboardButton(
  151. f"{page}/{total_pages}",
  152. callback_data=f"giveaway_participants_noop:{giveaway_id}",
  153. )
  154. )
  155. if page < total_pages:
  156. buttons.append(
  157. InlineKeyboardButton(
  158. "下一页 ›",
  159. callback_data=f"giveaway_participants_page:{giveaway_id}:{page + 1}",
  160. )
  161. )
  162. return InlineKeyboardMarkup([buttons])
  163. async def _render_participants_page(
  164. giveaway_id: str, page: int
  165. ) -> tuple[str, InlineKeyboardMarkup | None]:
  166. page = max(1, page)
  167. participants, total = await list_participants_page(
  168. giveaway_id=giveaway_id,
  169. active_only=True,
  170. page=page,
  171. page_size=PARTICIPANTS_PAGE_SIZE,
  172. )
  173. total_pages = max(1, (total + PARTICIPANTS_PAGE_SIZE - 1) // PARTICIPANTS_PAGE_SIZE)
  174. if page > total_pages:
  175. page = total_pages
  176. participants, total = await list_participants_page(
  177. giveaway_id=giveaway_id,
  178. active_only=True,
  179. page=page,
  180. page_size=PARTICIPANTS_PAGE_SIZE,
  181. )
  182. lines = [
  183. f"<b>抽奖 #{escape(giveaway_id)} 的参与者</b>",
  184. f"第 {page}/{total_pages} 页 · 共 {total} 人",
  185. ]
  186. if participants:
  187. offset = (page - 1) * PARTICIPANTS_PAGE_SIZE
  188. lines.extend(
  189. f"{offset + index}. {_participant_mention(item)}"
  190. for index, item in enumerate(participants, 1)
  191. )
  192. else:
  193. lines.append("暂时还没有参与者。")
  194. return "\n".join(lines), _participants_markup(
  195. giveaway_id, page=page, total_pages=total_pages
  196. )
  197. def _keyword_giveaways_markup(
  198. giveaways: list[dict[str, Any]],
  199. ) -> InlineKeyboardMarkup:
  200. rows: list[list[InlineKeyboardButton]] = []
  201. for giveaway in giveaways:
  202. giveaway_id = giveaway["giveaway_id"]
  203. title = str(giveaway["title"])
  204. short_title = title if len(title) <= 12 else title[:11] + "…"
  205. rows.append(
  206. [
  207. InlineKeyboardButton(
  208. f"报名 · {short_title}",
  209. callback_data=f"giveaway_join:{giveaway_id}",
  210. ),
  211. InlineKeyboardButton(
  212. "查看参与者",
  213. callback_data=f"giveaway_participants:{giveaway_id}",
  214. ),
  215. ]
  216. )
  217. return InlineKeyboardMarkup(rows)
  218. def _winners_markup(
  219. giveaway_id: str, *, page: int, total_pages: int
  220. ) -> InlineKeyboardMarkup | None:
  221. if total_pages <= 1:
  222. return None
  223. buttons: list[InlineKeyboardButton] = []
  224. if page > 1:
  225. buttons.append(
  226. InlineKeyboardButton(
  227. "‹ 上一页",
  228. callback_data=f"giveaway_winners_page:{giveaway_id}:{page - 1}",
  229. )
  230. )
  231. buttons.append(
  232. InlineKeyboardButton(
  233. f"{page}/{total_pages}",
  234. callback_data=f"giveaway_winners_noop:{giveaway_id}",
  235. )
  236. )
  237. if page < total_pages:
  238. buttons.append(
  239. InlineKeyboardButton(
  240. "下一页 ›",
  241. callback_data=f"giveaway_winners_page:{giveaway_id}:{page + 1}",
  242. )
  243. )
  244. return InlineKeyboardMarkup([buttons])
  245. def _render_winners_page(
  246. giveaway: dict[str, Any], page: int
  247. ) -> tuple[str, InlineKeyboardMarkup | None]:
  248. winners = list(giveaway.get("winners", []))
  249. total = len(winners)
  250. total_pages = max(1, (total + PARTICIPANTS_PAGE_SIZE - 1) // PARTICIPANTS_PAGE_SIZE)
  251. page = min(max(1, page), total_pages)
  252. offset = (page - 1) * PARTICIPANTS_PAGE_SIZE
  253. current = winners[offset : offset + PARTICIPANTS_PAGE_SIZE]
  254. lines = [
  255. f"<b>抽奖结果 #{escape(giveaway['giveaway_id'])}</b>",
  256. f"<b>标题:</b>{escape(giveaway['title'])}",
  257. f"第 {page}/{total_pages} 页 · 共 {total} 位中奖者",
  258. ]
  259. if current:
  260. for index, winner in enumerate(current, offset + 1):
  261. points = int(winner.get("points_reward", 0))
  262. suffix = f"(+{points} 积分)" if points else ""
  263. lines.append(
  264. f"{index}. {escape(str(winner['tier']))}:"
  265. f"{_participant_mention(winner)}{suffix}"
  266. )
  267. else:
  268. lines.append("本次没有符合条件的中奖者。")
  269. return "\n".join(lines), _winners_markup(
  270. giveaway["giveaway_id"], page=page, total_pages=total_pages
  271. )
  272. async def _join_user(giveaway_id: str, user) -> str:
  273. status, _ = await join_giveaway(
  274. giveaway_id,
  275. user_id=user.id,
  276. username=user.username,
  277. first_name=user.first_name,
  278. display_name=display_name(user.first_name, user.last_name),
  279. )
  280. return {
  281. "ok": "报名成功。",
  282. "duplicate": "你已经报名过该抽奖。",
  283. "missing": "未找到该抽奖。",
  284. "closed": "该抽奖已经关闭。",
  285. "not_started": "该抽奖尚未开始报名。",
  286. "ended": "该抽奖已经结束。",
  287. "banned": "你已被禁止参加本群抽奖。",
  288. "not_member": "只有当前群成员可以参加该抽奖。",
  289. "insufficient_points": "你的积分不足,无法报名。",
  290. }.get(status, "报名失败,请稍后重试。")
  291. async def _sweep_due_giveaways() -> None:
  292. await asyncio.sleep(5)
  293. while True:
  294. try:
  295. for giveaway in await list_unannounced_started_giveaways():
  296. if await refresh_giveaway_message(giveaway):
  297. await mark_giveaway_start_announced(giveaway["giveaway_id"])
  298. pending = await list_due_giveaways()
  299. pending.extend(await list_pending_cancellations())
  300. for giveaway in pending:
  301. await resume_pending_giveaway(giveaway)
  302. except Exception as exc:
  303. log.error(f"抽奖定时任务失败:{exc}")
  304. await asyncio.sleep(SWEEP_INTERVAL_SECONDS)
  305. def _start_sweeper() -> None:
  306. global _sweeper_started
  307. if _sweeper_started:
  308. return
  309. try:
  310. loop = asyncio.get_running_loop()
  311. except RuntimeError:
  312. return
  313. _sweeper_started = True
  314. loop.create_task(_sweep_due_giveaways(), name="giveaway-sweeper")
  315. @app.on_message(filters.command(["giveaway", "gcreate"]) & ~filters.private)
  316. @adminsOnly("can_change_info")
  317. async def create_giveaway_command(_, message: Message):
  318. payload = message.text.split(None, 1)[1].strip() if message.text and " " in message.text else ""
  319. pieces = [piece.strip() for piece in payload.split("|")]
  320. if len(pieces) < 3:
  321. return await message.reply_text(_create_usage(), parse_mode=ParseMode.HTML)
  322. schedule = _parse_schedule(pieces[0])
  323. prizes = _parse_prizes(pieces[1])
  324. minimum_points = _parse_optional_points(pieces, 4)
  325. entry_cost = _parse_optional_points(pieces, 5)
  326. participation_reward = _parse_optional_points(pieces, 6)
  327. if (
  328. not schedule
  329. or not prizes
  330. or not pieces[2]
  331. or minimum_points is None
  332. or entry_cost is None
  333. or participation_reward is None
  334. ):
  335. return await message.reply_text(_create_usage(), parse_mode=ParseMode.HTML)
  336. starts_at, ends_at = schedule
  337. giveaway = await create_and_publish_giveaway(
  338. chat_id=message.chat.id,
  339. creator_id=_actor_id(message),
  340. creator_name=_actor_name(message),
  341. title=pieces[2],
  342. description=pieces[3] if len(pieces) > 3 else "",
  343. prizes=prizes,
  344. starts_at=starts_at,
  345. ends_at=ends_at,
  346. minimum_points=minimum_points,
  347. entry_cost=entry_cost,
  348. participation_reward=participation_reward,
  349. )
  350. return await message.reply_text(f"抽奖 #{giveaway['giveaway_id']} 已创建。")
  351. @app.on_callback_query(filters.regex(r"^giveaway_join:"))
  352. async def giveaway_join_callback(_, query: CallbackQuery):
  353. giveaway_id = normalize_giveaway_id(query.data.split(":", 1)[1])
  354. await query.answer(await _join_user(giveaway_id, query.from_user), show_alert=True)
  355. @app.on_callback_query(filters.regex(r"^giveaway_participants:[a-f0-9]+$"))
  356. async def giveaway_participants_callback(_, query: CallbackQuery):
  357. giveaway_id = normalize_giveaway_id(query.data.rsplit(":", 1)[1])
  358. giveaway = await get_giveaway(giveaway_id)
  359. if not giveaway or int(giveaway["chat_id"]) != int(query.message.chat.id):
  360. return await query.answer("本群中未找到该抽奖。", show_alert=True)
  361. text, markup = await _render_participants_page(giveaway_id, 1)
  362. await query.message.reply_text(
  363. text,
  364. parse_mode=ParseMode.HTML,
  365. reply_markup=markup,
  366. disable_web_page_preview=True,
  367. )
  368. return await query.answer()
  369. @app.on_callback_query(
  370. filters.regex(r"^giveaway_participants_page:[a-f0-9]+:\d+$")
  371. )
  372. async def giveaway_participants_page_callback(_, query: CallbackQuery):
  373. _, giveaway_id, raw_page = query.data.split(":", 2)
  374. giveaway_id = normalize_giveaway_id(giveaway_id)
  375. giveaway = await get_giveaway(giveaway_id)
  376. if not giveaway or int(giveaway["chat_id"]) != int(query.message.chat.id):
  377. return await query.answer("本群中未找到该抽奖。", show_alert=True)
  378. text, markup = await _render_participants_page(giveaway_id, int(raw_page))
  379. await query.message.edit_text(
  380. text,
  381. parse_mode=ParseMode.HTML,
  382. reply_markup=markup,
  383. disable_web_page_preview=True,
  384. )
  385. return await query.answer()
  386. @app.on_callback_query(filters.regex(r"^giveaway_participants_noop:"))
  387. async def giveaway_participants_noop_callback(_, query: CallbackQuery):
  388. return await query.answer()
  389. @app.on_callback_query(filters.regex(r"^giveaway_winners_page:[a-f0-9]+:\d+$"))
  390. async def giveaway_winners_page_callback(_, query: CallbackQuery):
  391. _, giveaway_id, raw_page = query.data.split(":", 2)
  392. giveaway_id = normalize_giveaway_id(giveaway_id)
  393. giveaway = await get_giveaway(giveaway_id)
  394. if (
  395. not giveaway
  396. or giveaway["status"] != "finished"
  397. or int(giveaway["chat_id"]) != int(query.message.chat.id)
  398. ):
  399. return await query.answer("本群中未找到该开奖结果。", show_alert=True)
  400. text, markup = _render_winners_page(giveaway, int(raw_page))
  401. await query.message.edit_text(
  402. text,
  403. parse_mode=ParseMode.HTML,
  404. reply_markup=markup,
  405. disable_web_page_preview=True,
  406. )
  407. return await query.answer()
  408. @app.on_callback_query(filters.regex(r"^giveaway_winners_noop:"))
  409. async def giveaway_winners_noop_callback(_, query: CallbackQuery):
  410. return await query.answer()
  411. @app.on_message(filters.command("gjoin") & ~filters.private)
  412. async def giveaway_join_command(_, message: Message):
  413. if not message.from_user:
  414. return await message.reply_text("只有普通用户身份可以参加抽奖。")
  415. if len(message.command) < 2:
  416. return await message.reply_text("用法:/gjoin <抽奖编号>")
  417. return await message.reply_text(await _join_user(message.command[1], message.from_user))
  418. @app.on_message(filters.regex(r"^抽奖$") & filters.group)
  419. async def giveaway_keyword_command(_, message: Message):
  420. giveaways = await list_running_giveaways(chat_id=message.chat.id, limit=10)
  421. now = utc_now()
  422. giveaways = [
  423. giveaway for giveaway in giveaways if as_utc(giveaway["ends_at"]) > now
  424. ]
  425. if not giveaways:
  426. return await message.reply_text("本群当前没有进行中的抽奖。")
  427. lines = ["<b>本群抽奖</b>"]
  428. for giveaway in giveaways:
  429. state = "报名中" if giveaway_starts_at(giveaway) <= now else "待报名"
  430. lines.append(
  431. "#{id} · {title} · {state} · {count} 人参与 · {ends_at} 开奖".format(
  432. id=escape(giveaway["giveaway_id"]),
  433. title=escape(giveaway["title"]),
  434. state=state,
  435. count=await count_participants(giveaway["giveaway_id"]),
  436. ends_at=format_giveaway_time(giveaway["ends_at"]),
  437. )
  438. )
  439. return await message.reply_text(
  440. "\n".join(lines),
  441. parse_mode=ParseMode.HTML,
  442. reply_markup=_keyword_giveaways_markup(giveaways),
  443. disable_web_page_preview=True,
  444. )
  445. @app.on_message(filters.regex(r"^(?:开奖|中奖名单)$") & filters.group)
  446. async def giveaway_winners_keyword_command(_, message: Message):
  447. giveaways, _ = await list_giveaways_page(
  448. chat_id=message.chat.id,
  449. status="finished",
  450. page=1,
  451. page_size=20,
  452. )
  453. if not giveaways:
  454. return await message.reply_text("本群暂无已开奖的抽奖。")
  455. giveaway = max(
  456. giveaways,
  457. key=lambda item: as_utc(item.get("finished_at") or item["updated_at"]),
  458. )
  459. text, markup = _render_winners_page(giveaway, 1)
  460. return await message.reply_text(
  461. text,
  462. parse_mode=ParseMode.HTML,
  463. reply_markup=markup,
  464. disable_web_page_preview=True,
  465. )
  466. @app.on_message(filters.command("glist") & ~filters.private)
  467. @adminsOnly("can_change_info")
  468. async def list_giveaways_command(_, message: Message):
  469. giveaways = await list_running_giveaways(chat_id=message.chat.id, limit=20)
  470. if not giveaways:
  471. return await message.reply_text("本群当前没有进行中的抽奖。")
  472. lines = ["<b>进行中的抽奖</b>"]
  473. for giveaway in giveaways:
  474. lines.append(
  475. "#{id} - {title} - {count} 人参与 - 开奖于 {ends_at}".format(
  476. id=escape(giveaway["giveaway_id"]),
  477. title=escape(giveaway["title"]),
  478. count=await count_participants(giveaway["giveaway_id"]),
  479. ends_at=format_giveaway_time(giveaway["ends_at"]),
  480. )
  481. )
  482. return await message.reply_text("\n".join(lines), parse_mode=ParseMode.HTML)
  483. @app.on_message(filters.command("gparticipants") & ~filters.private)
  484. @adminsOnly("can_change_info")
  485. async def participants_command(_, message: Message):
  486. if len(message.command) < 2:
  487. return await message.reply_text("用法:/gparticipants <抽奖编号>")
  488. giveaway_id = normalize_giveaway_id(message.command[1])
  489. giveaway = await get_giveaway(giveaway_id)
  490. if not giveaway or giveaway["chat_id"] != message.chat.id:
  491. return await message.reply_text("本群中未找到该抽奖。")
  492. text, markup = await _render_participants_page(giveaway_id, 1)
  493. return await message.reply_text(
  494. text,
  495. parse_mode=ParseMode.HTML,
  496. reply_markup=markup,
  497. disable_web_page_preview=True,
  498. )
  499. @app.on_message(filters.command("gend") & ~filters.private)
  500. @adminsOnly("can_change_info")
  501. async def end_giveaway_command(_, message: Message):
  502. if len(message.command) < 2:
  503. return await message.reply_text("用法:/gend <抽奖编号>")
  504. giveaway_id = normalize_giveaway_id(message.command[1])
  505. giveaway = await get_giveaway(giveaway_id)
  506. if not giveaway or giveaway["chat_id"] != message.chat.id:
  507. return await message.reply_text("本群中未找到该抽奖。")
  508. _, text, _ = await finish_and_publish_giveaway(giveaway_id)
  509. return await message.reply_text(text)
  510. @app.on_message(filters.command("gcancel") & ~filters.private)
  511. @adminsOnly("can_change_info")
  512. async def cancel_giveaway_command(_, message: Message):
  513. if len(message.command) < 2:
  514. return await message.reply_text("用法:/gcancel <抽奖编号>")
  515. giveaway_id = normalize_giveaway_id(message.command[1])
  516. giveaway = await get_giveaway(giveaway_id)
  517. if not giveaway or giveaway["chat_id"] != message.chat.id:
  518. return await message.reply_text("本群中未找到该抽奖。")
  519. _, text, _ = await cancel_and_refund_giveaway(
  520. giveaway_id, chat_id=message.chat.id
  521. )
  522. return await message.reply_text(text)
  523. @app.on_message(filters.command("greroll") & ~filters.private)
  524. @adminsOnly("can_change_info")
  525. async def reroll_giveaway_command(_, message: Message):
  526. parts = message.text.split(None, 2) if message.text else []
  527. if len(parts) < 2:
  528. return await message.reply_text("用法:/greroll <抽奖编号> [奖项名称]")
  529. giveaway_id = normalize_giveaway_id(parts[1])
  530. giveaway = await get_giveaway(giveaway_id)
  531. if not giveaway or giveaway["chat_id"] != message.chat.id:
  532. return await message.reply_text("本群中未找到该抽奖。")
  533. try:
  534. winners, _ = await reroll_giveaway(
  535. giveaway_id=giveaway_id,
  536. moderator_id=_actor_id(message),
  537. tier_name=parts[2].strip() if len(parts) > 2 else None,
  538. publish=False,
  539. )
  540. except GiveawayServiceError as exc:
  541. return await message.reply_text(str(exc))
  542. return await message.reply_text(
  543. render_winners(
  544. giveaway,
  545. winners,
  546. reroll=True,
  547. tier_name=parts[2].strip() if len(parts) > 2 else None,
  548. ),
  549. parse_mode=ParseMode.HTML,
  550. disable_web_page_preview=True,
  551. )
  552. @app.on_message(filters.command("gremove") & ~filters.private)
  553. @adminsOnly("can_change_info")
  554. async def remove_participant_command(_, message: Message):
  555. if len(message.command) < 3 or not message.command[2].lstrip("-").isdigit():
  556. return await message.reply_text(
  557. "用法:/gremove <抽奖编号> <用户 ID> [no-refund]"
  558. )
  559. giveaway_id = normalize_giveaway_id(message.command[1])
  560. giveaway = await get_giveaway(giveaway_id)
  561. if not giveaway or giveaway["chat_id"] != message.chat.id:
  562. return await message.reply_text("本群中未找到该抽奖。")
  563. refund = not (len(message.command) > 3 and message.command[3].lower() == "no-refund")
  564. removed = await remove_and_optionally_refund_participant(
  565. giveaway_id=giveaway_id,
  566. user_id=int(message.command[2]),
  567. moderator_id=_actor_id(message),
  568. reason="由群管理员移除",
  569. refund=refund,
  570. )
  571. return await message.reply_text(
  572. "参与者已移除。"
  573. if removed
  574. else "未找到该参与者,或该参与者此前已被移除。"
  575. )
  576. @app.on_message(filters.command("gban") & ~filters.private)
  577. @adminsOnly("can_change_info")
  578. async def giveaway_ban_command(_, message: Message):
  579. user_id, reason = await extract_user_and_reason(message)
  580. if not user_id:
  581. return await message.reply_text("用法:/gban <成员> [原因]")
  582. await add_giveaway_ban(
  583. chat_id=message.chat.id,
  584. user_id=int(user_id),
  585. moderator_id=_actor_id(message),
  586. reason=reason,
  587. )
  588. return await message.reply_text(f"已禁止用户 {user_id} 参加本群抽奖。")
  589. @app.on_message(filters.command("gunban") & ~filters.private)
  590. @adminsOnly("can_change_info")
  591. async def giveaway_unban_command(_, message: Message):
  592. user_id, _ = await extract_user_and_reason(message)
  593. if not user_id:
  594. return await message.reply_text("用法:/gunban <成员>")
  595. removed = await remove_giveaway_ban(message.chat.id, int(user_id))
  596. return await message.reply_text(
  597. f"已允许用户 {user_id} 参加本群抽奖。"
  598. if removed
  599. else "该用户没有被限制参加抽奖。"
  600. )
  601. _start_sweeper()