ErisPulse-Raffle 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Raffle/Core.py +858 -0
- Raffle/__init__.py +1 -0
- Raffle/templates/__init__.py +0 -0
- Raffle/templates/view.css +433 -0
- Raffle/templates/view.html +295 -0
- Raffle/templates/view.js +921 -0
- erispulse_raffle-1.0.0.dist-info/METADATA +123 -0
- erispulse_raffle-1.0.0.dist-info/RECORD +11 -0
- erispulse_raffle-1.0.0.dist-info/WHEEL +5 -0
- erispulse_raffle-1.0.0.dist-info/entry_points.txt +2 -0
- erispulse_raffle-1.0.0.dist-info/top_level.txt +1 -0
Raffle/Core.py
ADDED
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import random
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ErisPulse import sdk
|
|
7
|
+
from ErisPulse.Core.Bases import BaseModule
|
|
8
|
+
from ErisPulse.Core.Event import command, message
|
|
9
|
+
from fastapi import Request
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
|
|
12
|
+
_MODULE_NAME = "Raffle"
|
|
13
|
+
_TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
14
|
+
_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 9L12 2L4 9"/><path d="M12 2v14"/><circle cx="12" cy="20" r="2"/><path d="M4 20h4"/><path d="M16 20h4"/></svg>'
|
|
15
|
+
|
|
16
|
+
_DEFAULT_SETTINGS = {
|
|
17
|
+
"current_activity": None,
|
|
18
|
+
"auto_confirm": False,
|
|
19
|
+
"reply_templates": {
|
|
20
|
+
"success": "{name},你已成功加入「{activity_name}」抽奖名单!\n本次将抽取 {count} 位中奖者,祝你好运!",
|
|
21
|
+
"already_joined": "{name},你已经在抽奖名单中了,无需重复报名。",
|
|
22
|
+
"hint": "想参与抽奖吗?发送指定关键词即可加入名单!",
|
|
23
|
+
"no_activity": "当前没有进行中的抽奖活动。",
|
|
24
|
+
"closed": "报名已截止,抽奖结果即将揭晓。",
|
|
25
|
+
"drawn": "抽奖已结束,感谢参与!",
|
|
26
|
+
"blacklisted": "你已被加入黑名单,无法参与本次活动。",
|
|
27
|
+
"not_in_whitelist": "本次活动仅限指定用户参与。",
|
|
28
|
+
"pending": "{name},你的参与申请已提交,等待管理员确认中。",
|
|
29
|
+
"notify": "抽奖活动开始啦!\n\n活动名称:{activity_name}\n活动描述:{description}\n开奖人数:{draw_count} 人\n参与关键词:{keywords}\n\n快来参与吧!",
|
|
30
|
+
"broadcast": "抽奖结果揭晓!\n活动:{activity_name}\n获奖者:{winner_names}\n恭喜以上 {winner_count} 位中奖者!",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Main(BaseModule):
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.sdk = sdk
|
|
38
|
+
self.logger = sdk.logger.get_child("Raffle")
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def get_load_strategy():
|
|
42
|
+
from ErisPulse.loaders import ModuleLoadStrategy
|
|
43
|
+
return ModuleLoadStrategy(lazy_load=False, priority=50)
|
|
44
|
+
|
|
45
|
+
async def on_load(self, event):
|
|
46
|
+
self._ensure_settings()
|
|
47
|
+
self._register_commands()
|
|
48
|
+
self._register_message_handler()
|
|
49
|
+
self._register_routes()
|
|
50
|
+
self._register_dashboard_view()
|
|
51
|
+
self.logger.info("Raffle 模块已加载")
|
|
52
|
+
|
|
53
|
+
async def on_unload(self, event):
|
|
54
|
+
self._unregister_routes()
|
|
55
|
+
try:
|
|
56
|
+
if hasattr(self.sdk, 'Dashboard') and self.sdk.Dashboard:
|
|
57
|
+
self.sdk.Dashboard.unregister_view(_MODULE_NAME)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
self.logger.info("Raffle 模块已卸载")
|
|
61
|
+
|
|
62
|
+
def _ensure_settings(self):
|
|
63
|
+
settings = self.sdk.storage.get("raffle:settings")
|
|
64
|
+
if not settings:
|
|
65
|
+
self.sdk.storage.set("raffle:settings", dict(_DEFAULT_SETTINGS))
|
|
66
|
+
|
|
67
|
+
def _get_settings(self):
|
|
68
|
+
settings = self.sdk.storage.get("raffle:settings")
|
|
69
|
+
if not settings:
|
|
70
|
+
return dict(_DEFAULT_SETTINGS)
|
|
71
|
+
for k, v in _DEFAULT_SETTINGS.items():
|
|
72
|
+
if k not in settings:
|
|
73
|
+
settings[k] = v
|
|
74
|
+
default_tpl = _DEFAULT_SETTINGS.get("reply_templates", {})
|
|
75
|
+
current_tpl = settings.get("reply_templates", {})
|
|
76
|
+
for k, v in default_tpl.items():
|
|
77
|
+
if k not in current_tpl:
|
|
78
|
+
current_tpl[k] = v
|
|
79
|
+
settings["reply_templates"] = current_tpl
|
|
80
|
+
return settings
|
|
81
|
+
|
|
82
|
+
def _save_settings(self, settings):
|
|
83
|
+
self.sdk.storage.set("raffle:settings", settings)
|
|
84
|
+
|
|
85
|
+
def _select_best_format(self, platform, templates):
|
|
86
|
+
try:
|
|
87
|
+
supported_methods = self.sdk.adapter.list_sends(platform)
|
|
88
|
+
if "Html" in supported_methods:
|
|
89
|
+
return ("Html", templates["html"])
|
|
90
|
+
elif "Markdown" in supported_methods:
|
|
91
|
+
return ("Markdown", templates["markdown"])
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
return ("Text", templates["text"])
|
|
95
|
+
|
|
96
|
+
def _register_commands(self):
|
|
97
|
+
@command(["raffle", "活动"], help="查看抽奖活动信息")
|
|
98
|
+
async def raffle_cmd(event):
|
|
99
|
+
user_id = event.get_user_id()
|
|
100
|
+
platform = event.get_platform()
|
|
101
|
+
is_group = event.is_group_message()
|
|
102
|
+
group_id = event.get_group_id() if is_group else None
|
|
103
|
+
|
|
104
|
+
all_activities = self._get_all_activities()
|
|
105
|
+
joined = []
|
|
106
|
+
for act in all_activities:
|
|
107
|
+
p = self.sdk.storage.get(f"raffle:participant:{act['id']}:{user_id}")
|
|
108
|
+
if p:
|
|
109
|
+
joined.append({"activity": act, "participant": p})
|
|
110
|
+
|
|
111
|
+
group_activities = []
|
|
112
|
+
if is_group and group_id:
|
|
113
|
+
for act in all_activities:
|
|
114
|
+
for g in act.get("allowed_groups", []):
|
|
115
|
+
if g["platform"] == platform and g["group_id"] == group_id:
|
|
116
|
+
group_activities.append(act)
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
templates = self._build_raffle_info(joined, group_activities)
|
|
120
|
+
fmt, content = self._select_best_format(platform, templates)
|
|
121
|
+
try:
|
|
122
|
+
await event.reply(content, method=fmt)
|
|
123
|
+
except Exception:
|
|
124
|
+
await event.reply(templates["text"])
|
|
125
|
+
|
|
126
|
+
def _build_raffle_info(self, joined, group_activities):
|
|
127
|
+
status_map = {"open": "报名中", "closed": "已关闭", "drawn": "已开奖"}
|
|
128
|
+
status_color = {"open": "#22c55e", "closed": "#f59e0b", "drawn": "#6366f1"}
|
|
129
|
+
accent = "#6366f1"
|
|
130
|
+
accent_bg = "rgba(99, 102, 241, 0.05)"
|
|
131
|
+
sec_color = "#666"
|
|
132
|
+
|
|
133
|
+
joined_html = ""
|
|
134
|
+
if joined:
|
|
135
|
+
items = []
|
|
136
|
+
for item in joined:
|
|
137
|
+
act = item["activity"]
|
|
138
|
+
p = item["participant"]
|
|
139
|
+
st = status_map.get(act.get("status", ""), act.get("status", ""))
|
|
140
|
+
sc = status_color.get(act.get("status", ""), sec_color)
|
|
141
|
+
date_str = time.strftime("%m-%d %H:%M", time.localtime(p.get("joined_at", 0)))
|
|
142
|
+
grp = "已确认" if p.get("group") == "confirmed" else "待确认"
|
|
143
|
+
grp_c = "#22c55e" if p.get("group") == "confirmed" else "#f59e0b"
|
|
144
|
+
items.append(
|
|
145
|
+
f'<div style="padding:6px 0;border-bottom:1px solid rgba(0,0,0,0.04)">'
|
|
146
|
+
f'<span style="font-weight:600">{act.get("name", "未命名")}</span>'
|
|
147
|
+
f' <span style="font-size:12px;color:{sc};margin-left:6px">{st}</span>'
|
|
148
|
+
f'<div style="font-size:12px;color:{sec_color};margin-top:2px">'
|
|
149
|
+
f'{date_str} · <span style="color:{grp_c}">{grp}</span></div></div>'
|
|
150
|
+
)
|
|
151
|
+
joined_html = ''.join(items)
|
|
152
|
+
else:
|
|
153
|
+
joined_html = f'<div style="color:{sec_color};font-size:13px">暂未参与任何活动</div>'
|
|
154
|
+
|
|
155
|
+
group_html = ""
|
|
156
|
+
if group_activities:
|
|
157
|
+
items = []
|
|
158
|
+
for act in group_activities:
|
|
159
|
+
st = status_map.get(act.get("status", ""), act.get("status", ""))
|
|
160
|
+
sc = status_color.get(act.get("status", ""), sec_color)
|
|
161
|
+
kw = "、".join(act.get("keywords", []))
|
|
162
|
+
confirmed = len(self._get_participants(act["id"], "confirmed"))
|
|
163
|
+
total = len(self._get_participants(act["id"]))
|
|
164
|
+
items.append(
|
|
165
|
+
f'<div style="padding:8px 0;border-bottom:1px solid rgba(0,0,0,0.04)">'
|
|
166
|
+
f'<div><span style="font-weight:600">{act.get("name", "未命名")}</span>'
|
|
167
|
+
f' <span style="font-size:12px;color:{sc};margin-left:6px">{st}</span></div>'
|
|
168
|
+
f'<div style="font-size:12px;color:{sec_color};margin-top:3px">'
|
|
169
|
+
f'关键词: {kw} · 开奖 {act.get("draw_count", 1)} 人 · 已报名 {confirmed}/{total}</div></div>'
|
|
170
|
+
)
|
|
171
|
+
group_html = ''.join(items)
|
|
172
|
+
else:
|
|
173
|
+
group_html = f'<div style="color:{sec_color};font-size:13px">当前群聊没有关联活动</div>'
|
|
174
|
+
|
|
175
|
+
html = (
|
|
176
|
+
f'<div style="padding:12px;border-radius:10px">'
|
|
177
|
+
f'<div style="color:{accent};font-size:15px;font-weight:700;margin-bottom:8px">我参与的活动</div>'
|
|
178
|
+
f'{joined_html}'
|
|
179
|
+
f'<div style="color:{accent};font-size:15px;font-weight:700;margin:12px 0 8px">本群活动</div>'
|
|
180
|
+
f'{group_html}'
|
|
181
|
+
f'</div>'
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
joined_md_lines = ["**我参与的活动**", ""]
|
|
185
|
+
if joined:
|
|
186
|
+
for item in joined:
|
|
187
|
+
act = item["activity"]
|
|
188
|
+
p = item["participant"]
|
|
189
|
+
st = status_map.get(act.get("status", ""), act.get("status", ""))
|
|
190
|
+
date_str = time.strftime("%m-%d %H:%M", time.localtime(p.get("joined_at", 0)))
|
|
191
|
+
joined_md_lines.append(f'- **{act.get("name", "未命名")}** ({st}) — {date_str}')
|
|
192
|
+
else:
|
|
193
|
+
joined_md_lines.append("暂未参与任何活动")
|
|
194
|
+
|
|
195
|
+
joined_md_lines.extend(["", "**本群活动**", ""])
|
|
196
|
+
if group_activities:
|
|
197
|
+
for act in group_activities:
|
|
198
|
+
st = status_map.get(act.get("status", ""), act.get("status", ""))
|
|
199
|
+
kw = "、".join(act.get("keywords", []))
|
|
200
|
+
confirmed = len(self._get_participants(act["id"], "confirmed"))
|
|
201
|
+
total = len(self._get_participants(act["id"]))
|
|
202
|
+
joined_md_lines.append(
|
|
203
|
+
f'- **{act.get("name", "未命名")}** ({st})\n 关键词: {kw} · 开奖 {act.get("draw_count", 1)} 人 · 已报名 {confirmed}/{total}'
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
joined_md_lines.append("当前群聊没有关联活动")
|
|
207
|
+
|
|
208
|
+
markdown = '\n'.join(joined_md_lines)
|
|
209
|
+
|
|
210
|
+
joined_text_lines = ["我参与的活动", "─" * 20]
|
|
211
|
+
if joined:
|
|
212
|
+
for item in joined:
|
|
213
|
+
act = item["activity"]
|
|
214
|
+
p = item["participant"]
|
|
215
|
+
st = status_map.get(act.get("status", ""), act.get("status", ""))
|
|
216
|
+
date_str = time.strftime("%m-%d %H:%M", time.localtime(p.get("joined_at", 0)))
|
|
217
|
+
joined_text_lines.append(f' {act.get("name", "未命名")} [{st}] - {date_str}')
|
|
218
|
+
else:
|
|
219
|
+
joined_text_lines.append(" 暂未参与任何活动")
|
|
220
|
+
|
|
221
|
+
joined_text_lines.extend(["", "本群活动", "─" * 20])
|
|
222
|
+
if group_activities:
|
|
223
|
+
for act in group_activities:
|
|
224
|
+
st = status_map.get(act.get("status", ""), act.get("status", ""))
|
|
225
|
+
kw = "、".join(act.get("keywords", []))
|
|
226
|
+
confirmed = len(self._get_participants(act["id"], "confirmed"))
|
|
227
|
+
total = len(self._get_participants(act["id"]))
|
|
228
|
+
joined_text_lines.append(
|
|
229
|
+
f' {act.get("name", "未命名")} [{st}]\n 关键词: {kw} | 开奖 {act.get("draw_count", 1)} 人 | 已报名 {confirmed}/{total}'
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
joined_text_lines.append(" 当前群聊没有关联活动")
|
|
233
|
+
|
|
234
|
+
text = '\n'.join(joined_text_lines)
|
|
235
|
+
|
|
236
|
+
return {"html": html, "markdown": markdown, "text": text}
|
|
237
|
+
|
|
238
|
+
def _read_template(self, name):
|
|
239
|
+
path = _TEMPLATES_DIR / name
|
|
240
|
+
if path.exists():
|
|
241
|
+
return path.read_text(encoding='utf-8')
|
|
242
|
+
return ""
|
|
243
|
+
|
|
244
|
+
def _get_current_activity(self):
|
|
245
|
+
settings = self._get_settings()
|
|
246
|
+
activity_id = settings.get("current_activity")
|
|
247
|
+
if not activity_id:
|
|
248
|
+
return None
|
|
249
|
+
return self.sdk.storage.get(f"raffle:activity:{activity_id}")
|
|
250
|
+
|
|
251
|
+
def _get_all_activities(self):
|
|
252
|
+
activity_ids = self.sdk.storage.get("raffle:activities:list", [])
|
|
253
|
+
activities = []
|
|
254
|
+
for aid in activity_ids:
|
|
255
|
+
act = self.sdk.storage.get(f"raffle:activity:{aid}")
|
|
256
|
+
if act:
|
|
257
|
+
activities.append(act)
|
|
258
|
+
return activities
|
|
259
|
+
|
|
260
|
+
def _get_participants(self, activity_id, group=None):
|
|
261
|
+
prefix = f"raffle:participant:{activity_id}:"
|
|
262
|
+
keys = self.sdk.storage.keys()
|
|
263
|
+
participants = []
|
|
264
|
+
for key in keys:
|
|
265
|
+
if key.startswith(prefix):
|
|
266
|
+
data = self.sdk.storage.get(key)
|
|
267
|
+
if data and (group is None or data.get("group") == group):
|
|
268
|
+
participants.append(data)
|
|
269
|
+
participants.sort(key=lambda x: x.get("joined_at", 0))
|
|
270
|
+
return participants
|
|
271
|
+
|
|
272
|
+
def _get_blacklist(self, activity_id):
|
|
273
|
+
return self.sdk.storage.get(f"raffle:blacklist:{activity_id}", [])
|
|
274
|
+
|
|
275
|
+
def _save_blacklist(self, activity_id, blacklist):
|
|
276
|
+
self.sdk.storage.set(f"raffle:blacklist:{activity_id}", blacklist)
|
|
277
|
+
|
|
278
|
+
def _get_whitelist(self, activity_id):
|
|
279
|
+
return self.sdk.storage.get(f"raffle:whitelist:{activity_id}", [])
|
|
280
|
+
|
|
281
|
+
def _save_whitelist(self, activity_id, whitelist):
|
|
282
|
+
self.sdk.storage.set(f"raffle:whitelist:{activity_id}", whitelist)
|
|
283
|
+
|
|
284
|
+
def _register_message_handler(self):
|
|
285
|
+
@message.on_message()
|
|
286
|
+
async def handle_raffle_message(event):
|
|
287
|
+
if not event.is_group_message():
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
activity = self._get_current_activity()
|
|
291
|
+
if not activity:
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
if activity.get("status") != "open":
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
platform = event.get_platform()
|
|
298
|
+
group_id = event.get_group_id()
|
|
299
|
+
allowed = False
|
|
300
|
+
for g in activity.get("allowed_groups", []):
|
|
301
|
+
if g["platform"] == platform and g["group_id"] == group_id:
|
|
302
|
+
allowed = True
|
|
303
|
+
break
|
|
304
|
+
if not allowed:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
text = event.get_text()
|
|
308
|
+
keywords = activity.get("keywords", [])
|
|
309
|
+
matched = any(kw in text for kw in keywords)
|
|
310
|
+
|
|
311
|
+
if not matched:
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
settings = self._get_settings()
|
|
315
|
+
tpl = settings.get("reply_templates", {})
|
|
316
|
+
|
|
317
|
+
user_id = event.get_user_id()
|
|
318
|
+
user_name = event.get_user_nickname() or "开发者"
|
|
319
|
+
activity_id = activity["id"]
|
|
320
|
+
|
|
321
|
+
blacklist = self._get_blacklist(activity_id)
|
|
322
|
+
if any(b.get("user_id") == user_id for b in blacklist):
|
|
323
|
+
await event.reply(tpl.get("blacklisted", "你已被加入抽奖黑名单"))
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
whitelist_mode = activity.get("whitelist_mode", False)
|
|
327
|
+
if whitelist_mode:
|
|
328
|
+
whitelist = self._get_whitelist(activity_id)
|
|
329
|
+
if not any(w.get("user_id") == user_id for w in whitelist):
|
|
330
|
+
await event.reply(tpl.get("not_in_whitelist", "本次活动仅限指定用户参与"))
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
existing = self.sdk.storage.get(f"raffle:participant:{activity_id}:{user_id}")
|
|
334
|
+
if existing:
|
|
335
|
+
await event.reply(tpl.get("already_joined", "").format(name=user_name))
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
auto_confirm = settings.get("auto_confirm", False) or activity.get("auto_confirm", False)
|
|
339
|
+
user_group = "confirmed" if auto_confirm else "pending"
|
|
340
|
+
|
|
341
|
+
self.sdk.storage.set(f"raffle:participant:{activity_id}:{user_id}", {
|
|
342
|
+
"user_id": user_id,
|
|
343
|
+
"user_name": user_name,
|
|
344
|
+
"platform": platform,
|
|
345
|
+
"joined_at": int(time.time()),
|
|
346
|
+
"group": user_group,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
if user_group == "pending":
|
|
350
|
+
await event.reply(tpl.get("pending", "").format(name=user_name))
|
|
351
|
+
else:
|
|
352
|
+
await event.reply(tpl.get("success", "").format(
|
|
353
|
+
name=user_name,
|
|
354
|
+
count=activity.get("draw_count", 1),
|
|
355
|
+
activity_name=activity.get("name", "抽奖活动"),
|
|
356
|
+
))
|
|
357
|
+
|
|
358
|
+
def _verify_token(self, request: Request) -> bool:
|
|
359
|
+
token = self._get_token(request)
|
|
360
|
+
if not token:
|
|
361
|
+
return False
|
|
362
|
+
try:
|
|
363
|
+
dashboard = self.sdk.Dashboard
|
|
364
|
+
if dashboard and hasattr(dashboard, '_verify_token'):
|
|
365
|
+
return dashboard._verify_token(token)
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
def _get_token(self, request: Request) -> str | None:
|
|
371
|
+
auth = request.headers.get("Authorization", "")
|
|
372
|
+
if auth.startswith("Bearer "):
|
|
373
|
+
return auth[7:]
|
|
374
|
+
return request.query_params.get("token")
|
|
375
|
+
|
|
376
|
+
def _register_routes(self):
|
|
377
|
+
r = self.sdk.router
|
|
378
|
+
mn = _MODULE_NAME
|
|
379
|
+
r.register_http_route(mn, "/api/platforms", handler=self._api_platforms, methods=["GET"])
|
|
380
|
+
r.register_http_route(mn, "/api/settings", handler=self._api_settings_get, methods=["GET"])
|
|
381
|
+
r.register_http_route(mn, "/api/settings", handler=self._api_settings_put, methods=["PUT"])
|
|
382
|
+
r.register_http_route(mn, "/api/activities", handler=self._api_activities_list, methods=["GET"])
|
|
383
|
+
r.register_http_route(mn, "/api/activities", handler=self._api_activities_create, methods=["POST"])
|
|
384
|
+
r.register_http_route(mn, "/api/activities/{activity_id}", handler=self._api_activities_get, methods=["GET"])
|
|
385
|
+
r.register_http_route(mn, "/api/activities/{activity_id}", handler=self._api_activities_update, methods=["PUT"])
|
|
386
|
+
r.register_http_route(mn, "/api/activities/{activity_id}", handler=self._api_activities_delete, methods=["DELETE"])
|
|
387
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/participants", handler=self._api_participants, methods=["GET"])
|
|
388
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/participants/{user_id}", handler=self._api_participant_action, methods=["PUT"])
|
|
389
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/participants/{user_id}", handler=self._api_participant_remove, methods=["DELETE"])
|
|
390
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/draw", handler=self._api_draw, methods=["POST"])
|
|
391
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/draw/revert", handler=self._api_draw_revert, methods=["POST"])
|
|
392
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/result", handler=self._api_result, methods=["GET"])
|
|
393
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/blacklist", handler=self._api_blacklist_get, methods=["GET"])
|
|
394
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/blacklist", handler=self._api_blacklist_update, methods=["PUT"])
|
|
395
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/whitelist", handler=self._api_whitelist_get, methods=["GET"])
|
|
396
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/whitelist", handler=self._api_whitelist_update, methods=["PUT"])
|
|
397
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/notify", handler=self._api_notify_send, methods=["POST"])
|
|
398
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/notify/history", handler=self._api_notify_history, methods=["GET"])
|
|
399
|
+
r.register_http_route(mn, "/api/activities/{activity_id}/notify/resend/{history_id}", handler=self._api_notify_resend, methods=["POST"])
|
|
400
|
+
|
|
401
|
+
def _unregister_routes(self):
|
|
402
|
+
r = self.sdk.router
|
|
403
|
+
mn = _MODULE_NAME
|
|
404
|
+
for p in [
|
|
405
|
+
"/api/platforms", "/api/settings",
|
|
406
|
+
"/api/activities", "/api/activities/{activity_id}",
|
|
407
|
+
"/api/activities/{activity_id}/participants",
|
|
408
|
+
"/api/activities/{activity_id}/participants/{user_id}",
|
|
409
|
+
"/api/activities/{activity_id}/draw",
|
|
410
|
+
"/api/activities/{activity_id}/draw/revert",
|
|
411
|
+
"/api/activities/{activity_id}/result",
|
|
412
|
+
"/api/activities/{activity_id}/blacklist",
|
|
413
|
+
"/api/activities/{activity_id}/whitelist",
|
|
414
|
+
"/api/activities/{activity_id}/notify",
|
|
415
|
+
"/api/activities/{activity_id}/notify/history",
|
|
416
|
+
"/api/activities/{activity_id}/notify/resend/{history_id}",
|
|
417
|
+
]:
|
|
418
|
+
try:
|
|
419
|
+
r.unregister_http_route(mn, p)
|
|
420
|
+
except Exception:
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
def _register_dashboard_view(self):
|
|
424
|
+
try:
|
|
425
|
+
dashboard = self.sdk.Dashboard
|
|
426
|
+
if not dashboard:
|
|
427
|
+
self.logger.warning("Dashboard 不可用,跳过视窗注册")
|
|
428
|
+
return
|
|
429
|
+
dashboard.register_view(
|
|
430
|
+
id=_MODULE_NAME,
|
|
431
|
+
title="抽奖管理", title_en="Raffle",
|
|
432
|
+
icon_svg=_ICON_SVG,
|
|
433
|
+
html_content=self._read_template("view.html"),
|
|
434
|
+
js_content=self._read_template("view.js"),
|
|
435
|
+
css_content=self._read_template("view.css"),
|
|
436
|
+
loader="loadRaffleView",
|
|
437
|
+
group="group_tools",
|
|
438
|
+
)
|
|
439
|
+
self.logger.info("Dashboard 视窗已注册")
|
|
440
|
+
except Exception as e:
|
|
441
|
+
self.logger.warning(f"Dashboard 视窗注册失败: {e}")
|
|
442
|
+
|
|
443
|
+
async def _api_platforms(self, request: Request) -> JSONResponse:
|
|
444
|
+
if not self._verify_token(request):
|
|
445
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
446
|
+
platforms = list(self.sdk.adapter.list_registered())
|
|
447
|
+
return JSONResponse({"platforms": platforms})
|
|
448
|
+
|
|
449
|
+
async def _api_settings_get(self, request: Request) -> JSONResponse:
|
|
450
|
+
if not self._verify_token(request):
|
|
451
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
452
|
+
return JSONResponse({"settings": self._get_settings()})
|
|
453
|
+
|
|
454
|
+
async def _api_settings_put(self, request: Request) -> JSONResponse:
|
|
455
|
+
if not self._verify_token(request):
|
|
456
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
457
|
+
body = await request.json()
|
|
458
|
+
settings = self._get_settings()
|
|
459
|
+
new_settings = body.get("settings", {})
|
|
460
|
+
settings.update(new_settings)
|
|
461
|
+
self._save_settings(settings)
|
|
462
|
+
self.logger.info("模块设置已更新")
|
|
463
|
+
return JSONResponse({"success": True})
|
|
464
|
+
|
|
465
|
+
async def _api_activities_list(self, request: Request) -> JSONResponse:
|
|
466
|
+
if not self._verify_token(request):
|
|
467
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
468
|
+
activities = self._get_all_activities()
|
|
469
|
+
return JSONResponse({"activities": activities})
|
|
470
|
+
|
|
471
|
+
async def _api_activities_create(self, request: Request) -> JSONResponse:
|
|
472
|
+
if not self._verify_token(request):
|
|
473
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
474
|
+
body = await request.json()
|
|
475
|
+
activity_id = body.get("id", f"act_{int(time.time())}")
|
|
476
|
+
activity = {
|
|
477
|
+
"id": activity_id,
|
|
478
|
+
"name": body.get("name", "未命名活动"),
|
|
479
|
+
"description": body.get("description", ""),
|
|
480
|
+
"draw_count": body.get("draw_count", 1),
|
|
481
|
+
"keywords": body.get("keywords", ["抽奖", "参与抽奖"]),
|
|
482
|
+
"allowed_groups": body.get("allowed_groups", []),
|
|
483
|
+
"auto_confirm": body.get("auto_confirm", False),
|
|
484
|
+
"whitelist_mode": body.get("whitelist_mode", False),
|
|
485
|
+
"status": "open",
|
|
486
|
+
"created_at": int(time.time()),
|
|
487
|
+
"draw_result": None,
|
|
488
|
+
}
|
|
489
|
+
self.sdk.storage.set(f"raffle:activity:{activity_id}", activity)
|
|
490
|
+
activity_ids = self.sdk.storage.get("raffle:activities:list", [])
|
|
491
|
+
if activity_id not in activity_ids:
|
|
492
|
+
activity_ids.append(activity_id)
|
|
493
|
+
self.sdk.storage.set("raffle:activities:list", activity_ids)
|
|
494
|
+
settings = self._get_settings()
|
|
495
|
+
if not settings.get("current_activity"):
|
|
496
|
+
settings["current_activity"] = activity_id
|
|
497
|
+
self._save_settings(settings)
|
|
498
|
+
self.logger.info(f"活动已创建: {activity_id}")
|
|
499
|
+
return JSONResponse({"success": True, "activity": activity})
|
|
500
|
+
|
|
501
|
+
async def _api_activities_get(self, request: Request) -> JSONResponse:
|
|
502
|
+
if not self._verify_token(request):
|
|
503
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
504
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
505
|
+
activity = self.sdk.storage.get(f"raffle:activity:{activity_id}")
|
|
506
|
+
if not activity:
|
|
507
|
+
return JSONResponse({"error": "活动不存在"}, status_code=404)
|
|
508
|
+
confirmed = self._get_participants(activity_id, "confirmed")
|
|
509
|
+
pending = self._get_participants(activity_id, "pending")
|
|
510
|
+
activity["participant_count"] = len(confirmed)
|
|
511
|
+
activity["pending_count"] = len(pending)
|
|
512
|
+
return JSONResponse({"activity": activity})
|
|
513
|
+
|
|
514
|
+
async def _api_activities_update(self, request: Request) -> JSONResponse:
|
|
515
|
+
if not self._verify_token(request):
|
|
516
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
517
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
518
|
+
activity = self.sdk.storage.get(f"raffle:activity:{activity_id}")
|
|
519
|
+
if not activity:
|
|
520
|
+
return JSONResponse({"error": "活动不存在"}, status_code=404)
|
|
521
|
+
body = await request.json()
|
|
522
|
+
for key in ["name", "description", "draw_count", "keywords", "allowed_groups",
|
|
523
|
+
"status", "auto_confirm", "whitelist_mode"]:
|
|
524
|
+
if key in body:
|
|
525
|
+
activity[key] = body[key]
|
|
526
|
+
self.sdk.storage.set(f"raffle:activity:{activity_id}", activity)
|
|
527
|
+
self.logger.info(f"活动已更新: {activity_id}")
|
|
528
|
+
return JSONResponse({"success": True, "activity": activity})
|
|
529
|
+
|
|
530
|
+
async def _api_activities_delete(self, request: Request) -> JSONResponse:
|
|
531
|
+
if not self._verify_token(request):
|
|
532
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
533
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
534
|
+
all_participants = self._get_participants(activity_id)
|
|
535
|
+
for p in all_participants:
|
|
536
|
+
self.sdk.storage.delete(f"raffle:participant:{activity_id}:{p['user_id']}")
|
|
537
|
+
self.sdk.storage.delete(f"raffle:activity:{activity_id}")
|
|
538
|
+
self.sdk.storage.delete(f"raffle:blacklist:{activity_id}")
|
|
539
|
+
self.sdk.storage.delete(f"raffle:whitelist:{activity_id}")
|
|
540
|
+
activity_ids = self.sdk.storage.get("raffle:activities:list", [])
|
|
541
|
+
if activity_id in activity_ids:
|
|
542
|
+
activity_ids.remove(activity_id)
|
|
543
|
+
self.sdk.storage.set("raffle:activities:list", activity_ids)
|
|
544
|
+
settings = self._get_settings()
|
|
545
|
+
if settings.get("current_activity") == activity_id:
|
|
546
|
+
settings["current_activity"] = activity_ids[0] if activity_ids else None
|
|
547
|
+
self._save_settings(settings)
|
|
548
|
+
self.logger.info(f"活动已删除: {activity_id}")
|
|
549
|
+
return JSONResponse({"success": True})
|
|
550
|
+
|
|
551
|
+
async def _api_participants(self, request: Request) -> JSONResponse:
|
|
552
|
+
if not self._verify_token(request):
|
|
553
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
554
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
555
|
+
confirmed = self._get_participants(activity_id, "confirmed")
|
|
556
|
+
pending = self._get_participants(activity_id, "pending")
|
|
557
|
+
return JSONResponse({"confirmed": confirmed, "pending": pending})
|
|
558
|
+
|
|
559
|
+
async def _api_participant_action(self, request: Request) -> JSONResponse:
|
|
560
|
+
if not self._verify_token(request):
|
|
561
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
562
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
563
|
+
user_id = request.path_params.get("user_id", "")
|
|
564
|
+
body = await request.json()
|
|
565
|
+
action = body.get("action", "")
|
|
566
|
+
key = f"raffle:participant:{activity_id}:{user_id}"
|
|
567
|
+
data = self.sdk.storage.get(key)
|
|
568
|
+
if not data:
|
|
569
|
+
return JSONResponse({"error": "参与者不存在"}, status_code=404)
|
|
570
|
+
if action == "confirm":
|
|
571
|
+
data["group"] = "confirmed"
|
|
572
|
+
self.sdk.storage.set(key, data)
|
|
573
|
+
return JSONResponse({"success": True, "group": "confirmed"})
|
|
574
|
+
elif action == "revoke":
|
|
575
|
+
data["group"] = "pending"
|
|
576
|
+
self.sdk.storage.set(key, data)
|
|
577
|
+
return JSONResponse({"success": True, "group": "pending"})
|
|
578
|
+
return JSONResponse({"error": "未知操作"}, status_code=400)
|
|
579
|
+
|
|
580
|
+
async def _api_participant_remove(self, request: Request) -> JSONResponse:
|
|
581
|
+
if not self._verify_token(request):
|
|
582
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
583
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
584
|
+
user_id = request.path_params.get("user_id", "")
|
|
585
|
+
self.sdk.storage.delete(f"raffle:participant:{activity_id}:{user_id}")
|
|
586
|
+
self.logger.info(f"参与者已移除: {user_id} 从活动 {activity_id}")
|
|
587
|
+
return JSONResponse({"success": True})
|
|
588
|
+
|
|
589
|
+
async def _api_draw(self, request: Request) -> JSONResponse:
|
|
590
|
+
if not self._verify_token(request):
|
|
591
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
592
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
593
|
+
activity = self.sdk.storage.get(f"raffle:activity:{activity_id}")
|
|
594
|
+
if not activity:
|
|
595
|
+
return JSONResponse({"error": "活动不存在"}, status_code=404)
|
|
596
|
+
if activity.get("status") != "open":
|
|
597
|
+
return JSONResponse({"error": "活动不在报名中状态"}, status_code=400)
|
|
598
|
+
|
|
599
|
+
confirmed = self._get_participants(activity_id, "confirmed")
|
|
600
|
+
draw_count = min(activity.get("draw_count", 1), len(confirmed))
|
|
601
|
+
if draw_count == 0:
|
|
602
|
+
return JSONResponse({"error": "没有已确认的参与者可以抽奖"}, status_code=400)
|
|
603
|
+
|
|
604
|
+
winners = random.sample(confirmed, draw_count)
|
|
605
|
+
draw_result = {
|
|
606
|
+
"winners": winners,
|
|
607
|
+
"drawn_at": int(time.time()),
|
|
608
|
+
"total_participants": len(confirmed),
|
|
609
|
+
}
|
|
610
|
+
activity["status"] = "drawn"
|
|
611
|
+
activity["draw_result"] = draw_result
|
|
612
|
+
self.sdk.storage.set(f"raffle:activity:{activity_id}", activity)
|
|
613
|
+
|
|
614
|
+
self.logger.info(f"活动 {activity_id} 开奖完成,获奖者: {[w['user_name'] for w in winners]}")
|
|
615
|
+
asyncio.create_task(self._broadcast_result(activity, winners))
|
|
616
|
+
|
|
617
|
+
return JSONResponse({
|
|
618
|
+
"success": True,
|
|
619
|
+
"winners": winners,
|
|
620
|
+
"total_participants": len(confirmed),
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
async def _api_result(self, request: Request) -> JSONResponse:
|
|
624
|
+
if not self._verify_token(request):
|
|
625
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
626
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
627
|
+
activity = self.sdk.storage.get(f"raffle:activity:{activity_id}")
|
|
628
|
+
if not activity:
|
|
629
|
+
return JSONResponse({"error": "活动不存在"}, status_code=404)
|
|
630
|
+
return JSONResponse({
|
|
631
|
+
"activity_id": activity_id,
|
|
632
|
+
"activity_name": activity.get("name", ""),
|
|
633
|
+
"status": activity.get("status", ""),
|
|
634
|
+
"draw_result": activity.get("draw_result"),
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
async def _api_draw_revert(self, request: Request) -> JSONResponse:
|
|
638
|
+
if not self._verify_token(request):
|
|
639
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
640
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
641
|
+
activity = self.sdk.storage.get(f"raffle:activity:{activity_id}")
|
|
642
|
+
if not activity:
|
|
643
|
+
return JSONResponse({"error": "活动不存在"}, status_code=404)
|
|
644
|
+
if activity.get("status") != "drawn":
|
|
645
|
+
return JSONResponse({"error": "活动不在已开奖状态"}, status_code=400)
|
|
646
|
+
activity["status"] = "open"
|
|
647
|
+
activity["draw_result"] = None
|
|
648
|
+
self.sdk.storage.set(f"raffle:activity:{activity_id}", activity)
|
|
649
|
+
self.logger.info(f"活动 {activity_id} 开奖已撤回")
|
|
650
|
+
return JSONResponse({"success": True, "activity": activity})
|
|
651
|
+
|
|
652
|
+
async def _api_blacklist_get(self, request: Request) -> JSONResponse:
|
|
653
|
+
if not self._verify_token(request):
|
|
654
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
655
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
656
|
+
return JSONResponse({"blacklist": self._get_blacklist(activity_id)})
|
|
657
|
+
|
|
658
|
+
async def _api_blacklist_update(self, request: Request) -> JSONResponse:
|
|
659
|
+
if not self._verify_token(request):
|
|
660
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
661
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
662
|
+
body = await request.json()
|
|
663
|
+
blacklist = body.get("blacklist", [])
|
|
664
|
+
self._save_blacklist(activity_id, blacklist)
|
|
665
|
+
self.logger.info(f"黑名单已更新: 活动 {activity_id}")
|
|
666
|
+
return JSONResponse({"success": True})
|
|
667
|
+
|
|
668
|
+
async def _api_whitelist_get(self, request: Request) -> JSONResponse:
|
|
669
|
+
if not self._verify_token(request):
|
|
670
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
671
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
672
|
+
return JSONResponse({"whitelist": self._get_whitelist(activity_id)})
|
|
673
|
+
|
|
674
|
+
async def _api_whitelist_update(self, request: Request) -> JSONResponse:
|
|
675
|
+
if not self._verify_token(request):
|
|
676
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
677
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
678
|
+
body = await request.json()
|
|
679
|
+
whitelist = body.get("whitelist", [])
|
|
680
|
+
self._save_whitelist(activity_id, whitelist)
|
|
681
|
+
self.logger.info(f"白名单已更新: 活动 {activity_id}")
|
|
682
|
+
return JSONResponse({"success": True})
|
|
683
|
+
|
|
684
|
+
async def _broadcast_result(self, activity, winners):
|
|
685
|
+
winner_names = "、".join([w["user_name"] for w in winners])
|
|
686
|
+
settings = self._get_settings()
|
|
687
|
+
tpl = settings.get("reply_templates", {})
|
|
688
|
+
broadcast_tpl = tpl.get("broadcast", "")
|
|
689
|
+
if broadcast_tpl:
|
|
690
|
+
text = broadcast_tpl.format(
|
|
691
|
+
activity_name=activity.get("name", "抽奖活动"),
|
|
692
|
+
winner_names=winner_names,
|
|
693
|
+
winner_count=len(winners),
|
|
694
|
+
total_participants=len(self._get_participants(activity["id"])),
|
|
695
|
+
)
|
|
696
|
+
else:
|
|
697
|
+
text = (
|
|
698
|
+
f"抽奖结果揭晓!\n"
|
|
699
|
+
f"活动:{activity.get('name', '抽奖活动')}\n"
|
|
700
|
+
f"获奖者:{winner_names}\n"
|
|
701
|
+
f"恭喜以上 {len(winners)} 位中奖者!"
|
|
702
|
+
)
|
|
703
|
+
for group in activity.get("allowed_groups", []):
|
|
704
|
+
try:
|
|
705
|
+
adapter = self.sdk.adapter.get(group["platform"])
|
|
706
|
+
if adapter:
|
|
707
|
+
await adapter.Send.To("group", group["group_id"]).Text(text)
|
|
708
|
+
self.logger.info(f"广播已发送: {group['platform']}/{group['group_id']}")
|
|
709
|
+
except Exception as e:
|
|
710
|
+
self.logger.error(f"广播失败 {group['platform']}/{group['group_id']}: {e}")
|
|
711
|
+
|
|
712
|
+
def _get_notify_history(self, activity_id):
|
|
713
|
+
return self.sdk.storage.get(f"raffle:notify_history:{activity_id}", [])
|
|
714
|
+
|
|
715
|
+
def _save_notify_history(self, activity_id, history):
|
|
716
|
+
self.sdk.storage.set(f"raffle:notify_history:{activity_id}", history)
|
|
717
|
+
|
|
718
|
+
def _build_notify_message(self, activity, custom_content=""):
|
|
719
|
+
settings = self._get_settings()
|
|
720
|
+
tpl = settings.get("reply_templates", {}).get("notify", "")
|
|
721
|
+
message = tpl.format(
|
|
722
|
+
activity_name=activity.get("name", "抽奖活动"),
|
|
723
|
+
description=activity.get("description", ""),
|
|
724
|
+
draw_count=activity.get("draw_count", 1),
|
|
725
|
+
keywords="、".join(activity.get("keywords", [])),
|
|
726
|
+
)
|
|
727
|
+
if custom_content:
|
|
728
|
+
message += "\n\n" + custom_content
|
|
729
|
+
return message
|
|
730
|
+
|
|
731
|
+
async def _send_notifications(self, activity, targets, custom_content=""):
|
|
732
|
+
message = self._build_notify_message(activity, custom_content)
|
|
733
|
+
results = []
|
|
734
|
+
for target in targets:
|
|
735
|
+
platform = target.get("platform", "")
|
|
736
|
+
session_type = target.get("session_type", "")
|
|
737
|
+
target_id = target.get("target_id", "")
|
|
738
|
+
account_id = target.get("account_id", "")
|
|
739
|
+
if not platform or not session_type or not target_id:
|
|
740
|
+
results.append({
|
|
741
|
+
"platform": platform, "session_type": session_type,
|
|
742
|
+
"target_id": target_id, "success": False, "error": "缺少必要参数",
|
|
743
|
+
})
|
|
744
|
+
continue
|
|
745
|
+
try:
|
|
746
|
+
adapter = self.sdk.adapter.get(platform)
|
|
747
|
+
if not adapter:
|
|
748
|
+
results.append({
|
|
749
|
+
"platform": platform, "session_type": session_type,
|
|
750
|
+
"target_id": target_id, "success": False, "error": "适配器不可用",
|
|
751
|
+
})
|
|
752
|
+
continue
|
|
753
|
+
send_dsl = adapter.Send
|
|
754
|
+
if account_id:
|
|
755
|
+
send_dsl = send_dsl.Using(account_id)
|
|
756
|
+
await send_dsl.To(session_type, target_id).Text(message)
|
|
757
|
+
results.append({
|
|
758
|
+
"platform": platform, "session_type": session_type,
|
|
759
|
+
"target_id": target_id, "success": True, "error": None,
|
|
760
|
+
})
|
|
761
|
+
self.logger.info(f"通知已发送: {platform}/{session_type}/{target_id}")
|
|
762
|
+
except Exception as e:
|
|
763
|
+
results.append({
|
|
764
|
+
"platform": platform, "session_type": session_type,
|
|
765
|
+
"target_id": target_id, "success": False, "error": str(e),
|
|
766
|
+
})
|
|
767
|
+
self.logger.error(f"通知发送失败: {platform}/{session_type}/{target_id}: {e}")
|
|
768
|
+
return message, results
|
|
769
|
+
|
|
770
|
+
async def _api_notify_send(self, request: Request) -> JSONResponse:
|
|
771
|
+
if not self._verify_token(request):
|
|
772
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
773
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
774
|
+
activity = self.sdk.storage.get(f"raffle:activity:{activity_id}")
|
|
775
|
+
if not activity:
|
|
776
|
+
return JSONResponse({"error": "活动不存在"}, status_code=404)
|
|
777
|
+
|
|
778
|
+
body = await request.json()
|
|
779
|
+
targets = body.get("targets", [])
|
|
780
|
+
custom_content = body.get("custom_content", "")
|
|
781
|
+
|
|
782
|
+
if not targets:
|
|
783
|
+
return JSONResponse({"error": "至少需要一个发送目标"}, status_code=400)
|
|
784
|
+
|
|
785
|
+
message, results = await self._send_notifications(activity, targets, custom_content)
|
|
786
|
+
|
|
787
|
+
history = self._get_notify_history(activity_id)
|
|
788
|
+
record = {
|
|
789
|
+
"id": f"notify_{int(time.time() * 1000)}",
|
|
790
|
+
"targets": targets,
|
|
791
|
+
"custom_content": custom_content,
|
|
792
|
+
"message": message,
|
|
793
|
+
"sent_at": int(time.time()),
|
|
794
|
+
"results": results,
|
|
795
|
+
}
|
|
796
|
+
history.insert(0, record)
|
|
797
|
+
self._save_notify_history(activity_id, history)
|
|
798
|
+
|
|
799
|
+
success_count = sum(1 for r in results if r.get("success"))
|
|
800
|
+
self.logger.info(f"活动通知发送完成: {activity_id}, 成功 {success_count}/{len(targets)}")
|
|
801
|
+
return JSONResponse({
|
|
802
|
+
"success": True,
|
|
803
|
+
"record": record,
|
|
804
|
+
"success_count": success_count,
|
|
805
|
+
"total_count": len(targets),
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
async def _api_notify_history(self, request: Request) -> JSONResponse:
|
|
809
|
+
if not self._verify_token(request):
|
|
810
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
811
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
812
|
+
activity = self.sdk.storage.get(f"raffle:activity:{activity_id}")
|
|
813
|
+
if not activity:
|
|
814
|
+
return JSONResponse({"error": "活动不存在"}, status_code=404)
|
|
815
|
+
history = self._get_notify_history(activity_id)
|
|
816
|
+
return JSONResponse({"history": history})
|
|
817
|
+
|
|
818
|
+
async def _api_notify_resend(self, request: Request) -> JSONResponse:
|
|
819
|
+
if not self._verify_token(request):
|
|
820
|
+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
821
|
+
activity_id = request.path_params.get("activity_id", "")
|
|
822
|
+
history_id = request.path_params.get("history_id", "")
|
|
823
|
+
activity = self.sdk.storage.get(f"raffle:activity:{activity_id}")
|
|
824
|
+
if not activity:
|
|
825
|
+
return JSONResponse({"error": "活动不存在"}, status_code=404)
|
|
826
|
+
|
|
827
|
+
history = self._get_notify_history(activity_id)
|
|
828
|
+
record = None
|
|
829
|
+
for h in history:
|
|
830
|
+
if h.get("id") == history_id:
|
|
831
|
+
record = h
|
|
832
|
+
break
|
|
833
|
+
if not record:
|
|
834
|
+
return JSONResponse({"error": "记录不存在"}, status_code=404)
|
|
835
|
+
|
|
836
|
+
targets = record.get("targets", [])
|
|
837
|
+
custom_content = record.get("custom_content", "")
|
|
838
|
+
message, results = await self._send_notifications(activity, targets, custom_content)
|
|
839
|
+
|
|
840
|
+
new_record = {
|
|
841
|
+
"id": f"notify_{int(time.time() * 1000)}",
|
|
842
|
+
"targets": targets,
|
|
843
|
+
"custom_content": custom_content,
|
|
844
|
+
"message": message,
|
|
845
|
+
"sent_at": int(time.time()),
|
|
846
|
+
"results": results,
|
|
847
|
+
"resent_from": history_id,
|
|
848
|
+
}
|
|
849
|
+
history.insert(0, new_record)
|
|
850
|
+
self._save_notify_history(activity_id, history)
|
|
851
|
+
|
|
852
|
+
success_count = sum(1 for r in results if r.get("success"))
|
|
853
|
+
return JSONResponse({
|
|
854
|
+
"success": True,
|
|
855
|
+
"record": new_record,
|
|
856
|
+
"success_count": success_count,
|
|
857
|
+
"total_count": len(targets),
|
|
858
|
+
})
|