|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
|
|
|
import asyncio
|
|
|
import re
|
|
|
-from datetime import timedelta
|
|
|
from html import escape
|
|
|
from typing import Any
|
|
|
|
|
|
@@ -17,7 +16,10 @@ from wbb.services.giveaways import (
|
|
|
cancel_and_refund_giveaway,
|
|
|
create_and_publish_giveaway,
|
|
|
finish_and_publish_giveaway,
|
|
|
+ format_giveaway_time,
|
|
|
join_giveaway,
|
|
|
+ parse_giveaway_time,
|
|
|
+ refresh_giveaway_message,
|
|
|
remove_and_optionally_refund_participant,
|
|
|
render_winners,
|
|
|
reroll_giveaway,
|
|
|
@@ -32,6 +34,8 @@ from wbb.utils.dbgiveaway import (
|
|
|
list_participants,
|
|
|
list_pending_cancellations,
|
|
|
list_running_giveaways,
|
|
|
+ list_unannounced_started_giveaways,
|
|
|
+ mark_giveaway_start_announced,
|
|
|
normalize_giveaway_id,
|
|
|
remove_giveaway_ban,
|
|
|
utc_now,
|
|
|
@@ -39,12 +43,12 @@ from wbb.utils.dbgiveaway import (
|
|
|
from wbb.utils.functions import extract_user_and_reason
|
|
|
|
|
|
__MODULE__ = "抽奖管理"
|
|
|
-__HELP__ = """/giveaway <时长> | <奖项> | <标题> | [说明] | [最低积分] | [报名消耗] | [参与奖励]
|
|
|
+__HELP__ = """/giveaway <报名开始时间 ~ 开奖时间> | <奖项> | <标题> | [说明] | [最低积分] | [报名消耗] | [参与奖励]
|
|
|
在当前群创建抽奖。
|
|
|
|
|
|
奖项格式:`1`、`一等奖:1, 二等奖:3`,或 `一等奖:1:50`;
|
|
|
最后的 50 表示该奖项每位中奖者获得 50 积分。
|
|
|
-时长单位:s 秒、m 分钟、h 小时、d 天、w 周。
|
|
|
+时间格式:`YYYY-MM-DD HH:mm`,按北京时间计算。
|
|
|
|
|
|
/gjoin <编号> - 报名参加抽奖。
|
|
|
/glist - 查看进行中的抽奖。
|
|
|
@@ -59,25 +63,18 @@ __HELP__ = """/giveaway <时长> | <奖项> | <标题> | [说明] | [最低积
|
|
|
MAX_TOTAL_WINNERS = 100
|
|
|
PARTICIPANT_PREVIEW_LIMIT = 30
|
|
|
SWEEP_INTERVAL_SECONDS = 30
|
|
|
-_DURATION_RE = re.compile(r"^\s*(\d+)\s*([smhdwSMHDW])\s*$")
|
|
|
-_DURATION_UNITS = {
|
|
|
- "s": "seconds",
|
|
|
- "m": "minutes",
|
|
|
- "h": "hours",
|
|
|
- "d": "days",
|
|
|
- "w": "weeks",
|
|
|
-}
|
|
|
_sweeper_started = False
|
|
|
|
|
|
|
|
|
-def _parse_duration(raw: str):
|
|
|
- match = _DURATION_RE.match(raw)
|
|
|
- if not match:
|
|
|
+def _parse_schedule(raw: str):
|
|
|
+ pieces = re.split(r"\s*(?:~|至)\s*", raw.strip())
|
|
|
+ if len(pieces) != 2:
|
|
|
return None
|
|
|
- amount = int(match.group(1))
|
|
|
- if amount <= 0:
|
|
|
+ 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 utc_now() + timedelta(**{_DURATION_UNITS[match.group(2).lower()]: amount})
|
|
|
+ return starts_at, ends_at
|
|
|
|
|
|
|
|
|
def _parse_prizes(raw: str) -> list[dict[str, Any]] | None:
|
|
|
@@ -116,8 +113,9 @@ def _parse_optional_points(pieces: list[str], index: int) -> int | None:
|
|
|
def _create_usage() -> str:
|
|
|
return (
|
|
|
"<b>用法</b>\n"
|
|
|
- "/giveaway 1h | 1 | 月度会员抽奖\n"
|
|
|
- "/giveaway 2d | 一等奖:1:50, 二等奖:3:10 | 活动抽奖 | "
|
|
|
+ "/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"
|
|
|
)
|
|
|
|
|
|
@@ -162,6 +160,7 @@ async def _join_user(giveaway_id: str, user) -> str:
|
|
|
"duplicate": "你已经报名过该抽奖。",
|
|
|
"missing": "未找到该抽奖。",
|
|
|
"closed": "该抽奖已经关闭。",
|
|
|
+ "not_started": "该抽奖尚未开始报名。",
|
|
|
"ended": "该抽奖已经结束。",
|
|
|
"banned": "你已被禁止参加本群抽奖。",
|
|
|
"not_member": "只有当前群成员可以参加该抽奖。",
|
|
|
@@ -173,6 +172,9 @@ async def _sweep_due_giveaways() -> None:
|
|
|
await asyncio.sleep(5)
|
|
|
while True:
|
|
|
try:
|
|
|
+ for giveaway in await list_unannounced_started_giveaways():
|
|
|
+ if await refresh_giveaway_message(giveaway):
|
|
|
+ await mark_giveaway_start_announced(giveaway["giveaway_id"])
|
|
|
pending = await list_due_giveaways()
|
|
|
pending.extend(await list_pending_cancellations())
|
|
|
for giveaway in pending:
|
|
|
@@ -201,13 +203,13 @@ async def create_giveaway_command(_, message: Message):
|
|
|
pieces = [piece.strip() for piece in payload.split("|")]
|
|
|
if len(pieces) < 3:
|
|
|
return await message.reply_text(_create_usage(), parse_mode=ParseMode.HTML)
|
|
|
- ends_at = _parse_duration(pieces[0])
|
|
|
+ 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 ends_at
|
|
|
+ not schedule
|
|
|
or not prizes
|
|
|
or not pieces[2]
|
|
|
or minimum_points is None
|
|
|
@@ -215,6 +217,7 @@ async def create_giveaway_command(_, message: Message):
|
|
|
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),
|
|
|
@@ -222,6 +225,7 @@ async def create_giveaway_command(_, message: 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,
|
|
|
@@ -254,11 +258,11 @@ async def list_giveaways_command(_, message: Message):
|
|
|
lines = ["<b>进行中的抽奖</b>"]
|
|
|
for giveaway in giveaways:
|
|
|
lines.append(
|
|
|
- "#{id} - {title} - {count} 人参与 - 截止于 {ends_at}".format(
|
|
|
+ "#{id} - {title} - {count} 人参与 - 开奖于 {ends_at}".format(
|
|
|
id=escape(giveaway["giveaway_id"]),
|
|
|
title=escape(giveaway["title"]),
|
|
|
count=await count_participants(giveaway["giveaway_id"]),
|
|
|
- ends_at=giveaway["ends_at"].strftime("%Y-%m-%d %H:%M UTC"),
|
|
|
+ ends_at=format_giveaway_time(giveaway["ends_at"]),
|
|
|
)
|
|
|
)
|
|
|
return await message.reply_text("\n".join(lines), parse_mode=ParseMode.HTML)
|