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 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
+ })