ErisPulse-DiscordAdapter 4.0.0__tar.gz

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.
@@ -0,0 +1,535 @@
1
+ import datetime
2
+ import re
3
+ import time
4
+ from typing import Dict, List, Optional
5
+
6
+ # Discord mention patterns
7
+ _MENTION_USER = re.compile(r"<@!?(\d+)>")
8
+ _MENTION_ROLE = re.compile(r"<@&(\d+)>")
9
+ _MENTION_CHANNEL = re.compile(r"<#(\d+)>")
10
+ _MENTION_ALL = re.compile(r"<@&?(\d+)>|<#(\d+)>")
11
+
12
+
13
+ class DiscordConverter:
14
+ """
15
+ Discord 事件转换器
16
+
17
+ 将 Discord Gateway Dispatch 事件转换为 ErisPulse OneBot12 标准格式。
18
+
19
+ 核心原则:
20
+ 1. 严格兼容:所有标准字段遵循 OneBot12 规范
21
+ 2. 明确扩展:平台特有功能使用 discord_ 前缀
22
+ 3. 数据完整:原始事件数据保留在 discord_raw 字段
23
+ 4. 时间统一:所有时间戳转换为 10 位 Unix 时间戳(秒级)
24
+ """
25
+
26
+ # 消息类事件
27
+ MESSAGE_EVENTS = {
28
+ "MESSAGE_CREATE",
29
+ "MESSAGE_UPDATE",
30
+ "MESSAGE_DELETE",
31
+ "MESSAGE_DELETE_BULK",
32
+ "MESSAGE_REACTION_ADD",
33
+ "MESSAGE_REACTION_REMOVE",
34
+ "MESSAGE_REACTION_REMOVE_ALL",
35
+ "MESSAGE_REACTION_REMOVE_EMOJI",
36
+ }
37
+
38
+ # 通知类事件
39
+ NOTICE_EVENTS = {
40
+ "GUILD_MEMBER_ADD",
41
+ "GUILD_MEMBER_REMOVE",
42
+ "GUILD_MEMBER_UPDATE",
43
+ "GUILD_ROLE_CREATE",
44
+ "GUILD_ROLE_DELETE",
45
+ "GUILD_ROLE_UPDATE",
46
+ "CHANNEL_CREATE",
47
+ "CHANNEL_DELETE",
48
+ "CHANNEL_UPDATE",
49
+ "TYPING_START",
50
+ }
51
+
52
+ # 通知 detail_type 映射
53
+ NOTICE_DETAIL_MAP = {
54
+ "GUILD_MEMBER_ADD": "group_member_increase",
55
+ "GUILD_MEMBER_REMOVE": "group_member_decrease",
56
+ "GUILD_MEMBER_UPDATE": "group_member_update",
57
+ "GUILD_ROLE_CREATE": "group_role_create",
58
+ "GUILD_ROLE_DELETE": "group_role_delete",
59
+ "GUILD_ROLE_UPDATE": "group_role_update",
60
+ "CHANNEL_CREATE": "channel_create",
61
+ "CHANNEL_DELETE": "channel_delete",
62
+ "CHANNEL_UPDATE": "channel_update",
63
+ "TYPING_START": "typing",
64
+ "MESSAGE_DELETE": "group_message_delete",
65
+ "MESSAGE_DELETE_BULK": "group_message_delete_bulk",
66
+ "MESSAGE_REACTION_ADD": "group_message_reaction_add",
67
+ "MESSAGE_REACTION_REMOVE": "group_message_reaction_remove",
68
+ }
69
+
70
+ def __init__(self):
71
+ self.bot_id = ""
72
+
73
+ def convert(self, raw_data: Dict, event_name: str) -> Optional[Dict]:
74
+ """
75
+ 将 Discord Dispatch 事件转换为 OneBot12 标准格式
76
+
77
+ :param raw_data: Discord Dispatch 的 d 字段(事件数据)
78
+ :param event_name: Discord Dispatch 的 t 字段(事件名)
79
+ :return: OneBot12 标准格式事件,不支持的事件返回 None
80
+ """
81
+ if not isinstance(raw_data, dict):
82
+ return None
83
+
84
+ event_type, detail_type = self._map_event_type(event_name, raw_data)
85
+
86
+ base = self._create_base_event(raw_data, event_name, event_type, detail_type)
87
+
88
+ handler = getattr(self, f"_handle_{event_name.lower()}", None)
89
+ if handler:
90
+ return handler(raw_data, base)
91
+
92
+ # 通用处理:尝试提取用户信息
93
+ self._fill_user_info(raw_data, base)
94
+ return base
95
+
96
+ # ==================== 基础事件构建 ====================
97
+
98
+ def _create_base_event(
99
+ self, raw_data: Dict, event_name: str, event_type: str, detail_type: str
100
+ ) -> Dict:
101
+ return {
102
+ "id": self._generate_id(raw_data, event_name),
103
+ "time": self._extract_time(raw_data),
104
+ "type": event_type,
105
+ "detail_type": detail_type,
106
+ "platform": "discord",
107
+ "self": {
108
+ "platform": "discord",
109
+ "user_id": self.bot_id,
110
+ },
111
+ "discord_raw": raw_data,
112
+ "discord_raw_type": event_name,
113
+ }
114
+
115
+ def _map_event_type(self, event_name: str, raw_data: Dict) -> tuple:
116
+ if event_name in self.MESSAGE_EVENTS:
117
+ return "message", self._get_detail_type(raw_data)
118
+ elif event_name == "INTERACTION_CREATE":
119
+ return "request", "interaction"
120
+ elif event_name in self.NOTICE_EVENTS:
121
+ detail = self.NOTICE_DETAIL_MAP.get(event_name, event_name.lower())
122
+ return "notice", detail
123
+ elif event_name in ("READY", "RESUMED"):
124
+ return "meta", event_name.lower()
125
+ else:
126
+ return "notice", event_name.lower()
127
+
128
+ def _get_detail_type(self, raw_data: Dict) -> str:
129
+ if "guild_id" in raw_data:
130
+ return "channel"
131
+ return "private"
132
+
133
+ def _extract_time(self, raw_data: Dict) -> int:
134
+ timestamp = raw_data.get("timestamp")
135
+ if timestamp:
136
+ try:
137
+ dt = datetime.datetime.fromisoformat(
138
+ str(timestamp).replace("Z", "+00:00")
139
+ )
140
+ return int(dt.timestamp())
141
+ except Exception:
142
+ pass
143
+ return int(time.time())
144
+
145
+ def _generate_id(self, raw_data: Dict, event_name: str) -> str:
146
+ msg_id = raw_data.get("id") or raw_data.get("message_id")
147
+ if msg_id:
148
+ return str(msg_id)
149
+ return f"{event_name}_{int(time.time() * 1000)}"
150
+
151
+ def _fill_user_info(self, raw_data: Dict, base: Dict):
152
+ author = raw_data.get("author") or raw_data.get("user") or {}
153
+ if author:
154
+ base["user_id"] = str(author.get("id", ""))
155
+ base["user_nickname"] = author.get("global_name") or author.get(
156
+ "username", ""
157
+ )
158
+
159
+ # ==================== 消息事件处理 ====================
160
+
161
+ def _handle_message_create(self, raw_data: Dict, base: Dict) -> Dict:
162
+ author = raw_data.get("author", {})
163
+
164
+ base["message_id"] = str(raw_data.get("id", ""))
165
+ base["user_id"] = str(author.get("id", ""))
166
+ base["user_nickname"] = author.get("global_name") or author.get("username", "")
167
+
168
+ # 解析消息段
169
+ segments = self._parse_message_content(raw_data)
170
+ base["message"] = segments
171
+ base["alt_message"] = self._generate_alt_message(segments)
172
+
173
+ # 频道/服务器信息
174
+ channel_id = str(raw_data.get("channel_id", ""))
175
+ guild_id = raw_data.get("guild_id")
176
+
177
+ base["channel_id"] = channel_id
178
+ base["discord_channel_id"] = channel_id
179
+ if guild_id:
180
+ base["discord_guild_id"] = str(guild_id)
181
+ base["group_id"] = channel_id
182
+ base["target_id"] = channel_id
183
+ else:
184
+ base["group_id"] = channel_id
185
+ base["target_id"] = base["user_id"]
186
+
187
+ # 话题/Thread 支持
188
+ if raw_data.get("thread"):
189
+ thread = raw_data["thread"]
190
+ if thread.get("id"):
191
+ base["thread_id"] = str(thread["id"])
192
+
193
+ # member 信息扩展
194
+ member = raw_data.get("member")
195
+ if member and isinstance(member, dict):
196
+ base["discord_member"] = member
197
+ if member.get("nick"):
198
+ base["discord_member_nick"] = member["nick"]
199
+
200
+ return base
201
+
202
+ def _handle_message_update(self, raw_data: Dict, base: Dict) -> Dict:
203
+ base["message_id"] = str(raw_data.get("id", ""))
204
+
205
+ author = raw_data.get("author")
206
+ if author:
207
+ base["user_id"] = str(author.get("id", ""))
208
+ base["user_nickname"] = author.get("global_name") or author.get(
209
+ "username", ""
210
+ )
211
+
212
+ segments = self._parse_message_content(raw_data)
213
+ base["message"] = segments
214
+ base["alt_message"] = self._generate_alt_message(segments)
215
+
216
+ channel_id = str(raw_data.get("channel_id", ""))
217
+ guild_id = raw_data.get("guild_id")
218
+ base["discord_channel_id"] = channel_id
219
+ if guild_id:
220
+ base["discord_guild_id"] = str(guild_id)
221
+ base["group_id"] = channel_id
222
+ base["discord_edit_time"] = int(time.time())
223
+
224
+ return base
225
+
226
+ def _handle_message_delete(self, raw_data: Dict, base: Dict) -> Dict:
227
+ base["message_id"] = str(raw_data.get("id", ""))
228
+ channel_id = str(raw_data.get("channel_id", ""))
229
+ guild_id = raw_data.get("guild_id")
230
+ base["discord_channel_id"] = channel_id
231
+ if guild_id:
232
+ base["discord_guild_id"] = str(guild_id)
233
+ base["group_id"] = channel_id
234
+ else:
235
+ base["detail_type"] = "private_message_delete"
236
+ return base
237
+
238
+ def _handle_message_delete_bulk(self, raw_data: Dict, base: Dict) -> Dict:
239
+ ids = raw_data.get("ids", [])
240
+ base["message_ids"] = [str(i) for i in ids]
241
+ channel_id = str(raw_data.get("channel_id", ""))
242
+ guild_id = raw_data.get("guild_id")
243
+ base["discord_channel_id"] = channel_id
244
+ if guild_id:
245
+ base["discord_guild_id"] = str(guild_id)
246
+ base["group_id"] = channel_id
247
+ return base
248
+
249
+ # ==================== 反应事件处理 ====================
250
+
251
+ def _handle_message_reaction_add(self, raw_data: Dict, base: Dict) -> Dict:
252
+ return self._fill_reaction_event(raw_data, base)
253
+
254
+ def _handle_message_reaction_remove(self, raw_data: Dict, base: Dict) -> Dict:
255
+ return self._fill_reaction_event(raw_data, base)
256
+
257
+ def _fill_reaction_event(self, raw_data: Dict, base: Dict) -> Dict:
258
+ base["message_id"] = str(raw_data.get("message_id", ""))
259
+ user_id = raw_data.get("user_id")
260
+ if user_id:
261
+ base["user_id"] = str(user_id)
262
+ channel_id = str(raw_data.get("channel_id", ""))
263
+ guild_id = raw_data.get("guild_id")
264
+ base["discord_channel_id"] = channel_id
265
+ if guild_id:
266
+ base["discord_guild_id"] = str(guild_id)
267
+ base["group_id"] = channel_id
268
+ emoji = raw_data.get("emoji", {})
269
+ if emoji:
270
+ base["discord_emoji"] = emoji
271
+ return base
272
+
273
+ # ==================== 通知事件处理 ====================
274
+
275
+ def _handle_guild_member_add(self, raw_data: Dict, base: Dict) -> Dict:
276
+ user = raw_data.get("user", {})
277
+ base["user_id"] = str(user.get("id", ""))
278
+ base["user_nickname"] = user.get("global_name") or user.get("username", "")
279
+ guild_id = raw_data.get("guild_id")
280
+ if guild_id:
281
+ base["discord_guild_id"] = str(guild_id)
282
+ base["group_id"] = str(guild_id)
283
+ return base
284
+
285
+ def _handle_guild_member_remove(self, raw_data: Dict, base: Dict) -> Dict:
286
+ user = raw_data.get("user", {})
287
+ base["user_id"] = str(user.get("id", ""))
288
+ base["user_nickname"] = user.get("global_name") or user.get("username", "")
289
+ guild_id = raw_data.get("guild_id")
290
+ if guild_id:
291
+ base["discord_guild_id"] = str(guild_id)
292
+ base["group_id"] = str(guild_id)
293
+ return base
294
+
295
+ def _handle_guild_member_update(self, raw_data: Dict, base: Dict) -> Dict:
296
+ user = raw_data.get("user", {})
297
+ base["user_id"] = str(user.get("id", ""))
298
+ base["user_nickname"] = user.get("global_name") or user.get("username", "")
299
+ guild_id = raw_data.get("guild_id")
300
+ if guild_id:
301
+ base["discord_guild_id"] = str(guild_id)
302
+ base["group_id"] = str(guild_id)
303
+ return base
304
+
305
+ def _handle_guild_role_create(self, raw_data: Dict, base: Dict) -> Dict:
306
+ guild_id = raw_data.get("guild_id")
307
+ if guild_id:
308
+ base["discord_guild_id"] = str(guild_id)
309
+ base["group_id"] = str(guild_id)
310
+ role = raw_data.get("role", {})
311
+ if role:
312
+ base["discord_role"] = role
313
+ return base
314
+
315
+ def _handle_guild_role_delete(self, raw_data: Dict, base: Dict) -> Dict:
316
+ guild_id = raw_data.get("guild_id")
317
+ if guild_id:
318
+ base["discord_guild_id"] = str(guild_id)
319
+ base["group_id"] = str(guild_id)
320
+ role_id = raw_data.get("role_id")
321
+ if role_id:
322
+ base["discord_role_id"] = str(role_id)
323
+ return base
324
+
325
+ def _handle_guild_role_update(self, raw_data: Dict, base: Dict) -> Dict:
326
+ return self._handle_guild_role_create(raw_data, base)
327
+
328
+ def _handle_channel_create(self, raw_data: Dict, base: Dict) -> Dict:
329
+ base["discord_channel"] = raw_data
330
+ guild_id = raw_data.get("guild_id")
331
+ if guild_id:
332
+ base["discord_guild_id"] = str(guild_id)
333
+ channel_id = raw_data.get("id")
334
+ if channel_id:
335
+ base["discord_channel_id"] = str(channel_id)
336
+ return base
337
+
338
+ def _handle_channel_delete(self, raw_data: Dict, base: Dict) -> Dict:
339
+ return self._handle_channel_create(raw_data, base)
340
+
341
+ def _handle_channel_update(self, raw_data: Dict, base: Dict) -> Dict:
342
+ return self._handle_channel_create(raw_data, base)
343
+
344
+ def _handle_typing_start(self, raw_data: Dict, base: Dict) -> Dict:
345
+ user_id = raw_data.get("user_id")
346
+ if user_id:
347
+ base["user_id"] = str(user_id)
348
+ channel_id = str(raw_data.get("channel_id", ""))
349
+ guild_id = raw_data.get("guild_id")
350
+ base["discord_channel_id"] = channel_id
351
+ if guild_id:
352
+ base["discord_guild_id"] = str(guild_id)
353
+ base["group_id"] = channel_id
354
+ return base
355
+
356
+ # ==================== 交互事件处理 ====================
357
+
358
+ def _handle_interaction_create(self, raw_data: Dict, base: Dict) -> Dict:
359
+ user = raw_data.get("user") or raw_data.get("member", {}).get("user", {})
360
+ if user:
361
+ base["user_id"] = str(user.get("id", ""))
362
+ base["user_nickname"] = user.get("global_name") or user.get("username", "")
363
+ channel_id = str(raw_data.get("channel_id", ""))
364
+ guild_id = raw_data.get("guild_id")
365
+ base["discord_channel_id"] = channel_id
366
+ if guild_id:
367
+ base["discord_guild_id"] = str(guild_id)
368
+ base["group_id"] = channel_id
369
+ base["discord_interaction"] = raw_data
370
+ return base
371
+
372
+ # ==================== 消息内容解析 ====================
373
+
374
+ def _parse_message_content(self, raw_data: Dict) -> List[Dict]:
375
+ segments = []
376
+
377
+ content = raw_data.get("content") or ""
378
+ if content:
379
+ segments.extend(self._parse_text_with_mentions(content, raw_data))
380
+
381
+ # Embeds
382
+ for embed in raw_data.get("embeds", []):
383
+ segments.append(
384
+ {
385
+ "type": "discord_embed",
386
+ "data": {"embed": embed},
387
+ }
388
+ )
389
+
390
+ # Attachments
391
+ for attachment in raw_data.get("attachments", []):
392
+ url = attachment.get("url", "")
393
+ filename = attachment.get("filename", "")
394
+ content_type = attachment.get("content_type", "")
395
+ seg_type = "file"
396
+ if content_type.startswith("image"):
397
+ seg_type = "image"
398
+ elif content_type.startswith("video"):
399
+ seg_type = "video"
400
+ elif content_type.startswith("audio"):
401
+ seg_type = "audio"
402
+ segments.append(
403
+ {
404
+ "type": seg_type,
405
+ "data": {
406
+ "file": url,
407
+ "file_id": attachment.get("id", ""),
408
+ "file_name": filename,
409
+ "url": url,
410
+ "content_type": content_type,
411
+ },
412
+ }
413
+ )
414
+
415
+ # Stickers
416
+ for sticker in raw_data.get("sticker_items", []):
417
+ segments.append(
418
+ {
419
+ "type": "discord_sticker",
420
+ "data": sticker,
421
+ }
422
+ )
423
+
424
+ # Components (buttons, selects)
425
+ components = raw_data.get("components")
426
+ if components:
427
+ segments.append(
428
+ {
429
+ "type": "discord_components",
430
+ "data": {"components": components},
431
+ }
432
+ )
433
+
434
+ if not segments:
435
+ segments.append({"type": "text", "data": {"text": ""}})
436
+
437
+ return segments
438
+
439
+ def _parse_text_with_mentions(self, content: str, raw_data: Dict) -> List[Dict]:
440
+ """解析 Discord 文本内容,将 mention 格式转换为 mention 消息段"""
441
+ segments = []
442
+
443
+ user_mentions = {}
444
+ for m in raw_data.get("mentions", []):
445
+ uid = m.get("id", "")
446
+ if uid:
447
+ user_mentions[uid] = m
448
+
449
+ # 合并所有 mention 模式
450
+ pattern = re.compile(r"<@!?(\d+)>|<@&(\d+)>|<#(\d+)>")
451
+
452
+ last_end = 0
453
+ for match in pattern.finditer(content):
454
+ start, end = match.span()
455
+
456
+ if start > last_end:
457
+ text = content[last_end:start]
458
+ if text:
459
+ segments.append({"type": "text", "data": {"text": text}})
460
+
461
+ user_id = match.group(1)
462
+ role_id = match.group(2)
463
+ channel_ref_id = match.group(3)
464
+
465
+ if user_id:
466
+ user_info = user_mentions.get(user_id, {})
467
+ segments.append(
468
+ {
469
+ "type": "mention",
470
+ "data": {
471
+ "user_id": user_id,
472
+ "user_nickname": (
473
+ user_info.get("global_name")
474
+ or user_info.get("username", "")
475
+ ),
476
+ },
477
+ }
478
+ )
479
+ elif role_id:
480
+ segments.append(
481
+ {
482
+ "type": "discord_role_mention",
483
+ "data": {"role_id": role_id},
484
+ }
485
+ )
486
+ elif channel_ref_id:
487
+ segments.append(
488
+ {
489
+ "type": "discord_channel_mention",
490
+ "data": {"channel_id": channel_ref_id},
491
+ }
492
+ )
493
+
494
+ last_end = end
495
+
496
+ if last_end < len(content):
497
+ text = content[last_end:]
498
+ if text:
499
+ segments.append({"type": "text", "data": {"text": text}})
500
+
501
+ if not segments and content:
502
+ segments.append({"type": "text", "data": {"text": content}})
503
+
504
+ return segments
505
+
506
+ def _generate_alt_message(self, segments: List[Dict]) -> str:
507
+ parts = []
508
+ for seg in segments:
509
+ t = seg.get("type", "")
510
+ d = seg.get("data", {})
511
+ if t == "text":
512
+ parts.append(d.get("text", ""))
513
+ elif t == "mention":
514
+ parts.append(f"@{d.get('user_nickname', d.get('user_id', ''))}")
515
+ elif t == "mention_all":
516
+ parts.append("@everyone")
517
+ elif t == "discord_role_mention":
518
+ parts.append(f"@&{d.get('role_id', '')}")
519
+ elif t == "discord_channel_mention":
520
+ parts.append(f"#{d.get('channel_id', '')}")
521
+ elif t == "image":
522
+ parts.append("[图片]")
523
+ elif t == "video":
524
+ parts.append("[视频]")
525
+ elif t == "audio":
526
+ parts.append("[语音]")
527
+ elif t == "file":
528
+ parts.append(f"[文件:{d.get('file_name', '')}]")
529
+ elif t == "discord_embed":
530
+ parts.append("[嵌入消息]")
531
+ elif t == "discord_sticker":
532
+ parts.append("[贴纸]")
533
+ elif t == "discord_components":
534
+ parts.append("[组件]")
535
+ return "".join(parts)