ErisPulse-MatrixAdapter 1.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,209 @@
1
+ Metadata-Version: 2.4
2
+ Name: ErisPulse-MatrixAdapter
3
+ Version: 1.0.0
4
+ Summary: ErisPulse的Matrix协议适配模块
5
+ Author-email: wsu2059 <wsu2059@qq.com>
6
+ Project-URL: homepage, https://github.com/ErisPulse/ErisPulse-MatrixAdapter
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: aiohttp>=3.8.0
10
+
11
+ # MatrixAdapter 模块文档
12
+
13
+ ## 简介
14
+ MatrixAdapter 是基于 [ErisPulse](https://github.com/ErisPulse/ErisPulse/) 架构的 Matrix 协议适配器,通过 Long Polling Sync API 接收事件,整合了私聊、群组等多种场景的功能模块,提供统一的事件处理和消息操作接口。
15
+
16
+ ## 使用示例
17
+
18
+ ### OneBot12标准事件类型
19
+
20
+ MatrixAdapter 适配器完全兼容 OneBot12 标准事件格式,并提供了一些扩展字段:
21
+
22
+ | 事件类型 | detail_type | 说明 |
23
+ |----------|-------------|------|
24
+ | 消息事件(私聊) | private | DM房间中用户发送的消息 |
25
+ | 消息事件(群组) | group | 群组房间中用户发送的消息 |
26
+ | 群成员增加 | group_member_increase | 用户加入房间 |
27
+ | 群成员减少 | group_member_decrease | 用户离开/被封禁 |
28
+ | 成员信息更新 | matrix_member_update | 房间成员信息变更 |
29
+ | Matrix表情回应 | matrix_reaction | 消息表情回应 |
30
+ | Matrix消息撤回 | matrix_redaction | 消息被撤回/删除 |
31
+ | Matrix房间名称变更 | matrix_name | 房间名称变更 |
32
+ | Matrix房间话题变更 | matrix_topic | 房间话题变更 |
33
+ | Matrix房间头像变更 | matrix_avatar | 房间头像变更 |
34
+ | Matrix权限等级变更 | matrix_power_levels | 房间权限变更 |
35
+
36
+ ---
37
+
38
+ ## 消息发送示例
39
+
40
+ ```python
41
+ from ErisPulse import sdk
42
+ matrix = sdk.adapter.get("matrix")
43
+
44
+ # 发送文本消息
45
+ await matrix.Send.To("group", room_id).Text("Hello World!")
46
+
47
+ # 发送带@的消息
48
+ await matrix.Send.To("group", room_id).At("@user:matrix.org").Text("你好")
49
+
50
+ # 发送带@所有人的消息
51
+ await matrix.Send.To("group", room_id).AtAll().Text("公告通知")
52
+
53
+ # 发送回复消息
54
+ await matrix.Send.To("group", room_id).Reply("$event_id").Text("回复内容")
55
+
56
+ # 发送图片(URL)
57
+ await matrix.Send.To("group", room_id).Image("https://example.com/image.png")
58
+
59
+ # 发送图片(MXC URI)
60
+ await matrix.Send.To("group", room_id).Image("mxc://matrix.org/abc123")
61
+
62
+ # 发送图片(二进制数据)
63
+ with open("image.png", "rb") as f:
64
+ image_data = f.read()
65
+ await matrix.Send.To("group", room_id).Image(image_data)
66
+
67
+ # 发送图片(本地文件路径)
68
+ await matrix.Send.To("group", room_id).Image("/path/to/image.png")
69
+
70
+ # 发送通知消息(m.notice)
71
+ await matrix.Send.To("group", room_id).Notice("系统通知")
72
+
73
+ # 发送HTML格式消息
74
+ await matrix.Send.To("group", room_id).Html("<b>加粗</b> <i>斜体</i>", fallback="加粗 斜体")
75
+
76
+ # 发送文件(带文件名)
77
+ await matrix.Send.To("group", room_id).File("/path/to/file.pdf", filename="文档.pdf")
78
+
79
+ # 组合使用:回复 + @
80
+ await matrix.Send.To("group", room_id).Reply("$event_id").At("@user:matrix.org").Text("复合消息")
81
+
82
+ # 使用 Raw_ob12 发送 OneBot12 格式消息
83
+ message = [
84
+ {"type": "text", "data": {"text": "第一行"}},
85
+ {"type": "image", "data": {"file": "https://example.com/img.jpg"}},
86
+ {"type": "text", "data": {"text": "第二行"}}
87
+ ]
88
+ await matrix.Send.To("group", room_id).Raw_ob12(message)
89
+ ```
90
+
91
+ ---
92
+
93
+ ### 配置说明
94
+
95
+ 首次运行会自动生成默认配置。
96
+
97
+ ```toml
98
+ # config.toml
99
+ [Matrix_Adapter]
100
+ homeserver = "https://matrix.org" # Matrix服务器地址(必填)
101
+ access_token = "YOUR_ACCESS_TOKEN" # 访问令牌(与 user_id+password 二选一)
102
+ user_id = "" # Matrix用户ID(如 @bot:matrix.org)
103
+ password = "" # Matrix用户密码
104
+ auto_accept_invites = true # 是否自动接受房间邀请(可选,默认为true)
105
+ ```
106
+
107
+ **配置项说明:**
108
+ - `homeserver`:Matrix服务器地址(必填),默认为 `https://matrix.org`
109
+ - `access_token`:访问令牌,可从Matrix客户端(如 Element)的设置中获取
110
+ - `user_id`:Matrix用户ID(如 `@bot:matrix.org`),与 `password` 配合使用
111
+ - `password`:Matrix用户密码,用于自动登录获取 access_token
112
+ - `auto_accept_invites`:是否自动接受房间邀请,默认为 `true`
113
+
114
+ **认证方式:**
115
+ - 方式一(推荐):直接提供 `access_token`
116
+ - 方式二:提供 `user_id` 和 `password`,适配器会自动调用登录接口获取 token
117
+
118
+ ---
119
+
120
+ ## Matrix平台特有功能
121
+
122
+ 请参考 [Matrix平台特性文档](platform-features.md) 了解Matrix平台的特有功能,包括去中心化架构、房间概念、Long Polling同步、MXC URI、HTML富文本、表情回应、消息编辑、扩展字段说明等内容。
123
+
124
+ 详细的事件转换对照请参考 [转换对照文档](CoverToOnebot12.md)。
125
+
126
+ ## 事件监听示例
127
+
128
+ ### 使用 Event 模块(推荐)
129
+
130
+ ```python
131
+ from ErisPulse.Core.Event import message, notice
132
+
133
+ @message.on_message()
134
+ async def handle_message(event):
135
+ if event["platform"] == "matrix":
136
+ detail_type = event["detail_type"]
137
+ if detail_type == "private":
138
+ # 处理私聊消息
139
+ pass
140
+ elif detail_type == "group":
141
+ # 处理群组消息
142
+ pass
143
+
144
+ @notice.on_notice()
145
+ async def handle_notice(event):
146
+ if event["platform"] == "matrix":
147
+ detail_type = event["detail_type"]
148
+ if detail_type == "matrix_reaction":
149
+ # 处理表情回应
150
+ reaction_key = event.get("matrix_reaction_key", "")
151
+ elif detail_type == "matrix_redaction":
152
+ # 处理消息撤回
153
+ redacted_id = event.get("matrix_redacted_event_id", "")
154
+ elif detail_type == "group_member_increase":
155
+ # 处理成员加入
156
+ user_id = event.get("user_id", "")
157
+ ```
158
+
159
+ ### 使用 OneBot12 标准事件
160
+
161
+ ```python
162
+ @sdk.adapter.on("message")
163
+ async def handle_message(event):
164
+ if event["platform"] == "matrix":
165
+ bot_id = event["self"]["user_id"]
166
+ print(f"消息来自Bot: {bot_id}")
167
+
168
+ @sdk.adapter.on("notice")
169
+ async def handle_notice(event):
170
+ if event["platform"] == "matrix":
171
+ # 处理Matrix通知事件
172
+ pass
173
+ ```
174
+
175
+ ### 使用 Event Mixin 方法
176
+
177
+ ```python
178
+ @message.on_message()
179
+ async def handle_message(event):
180
+ if event.get("platform") != "matrix":
181
+ return
182
+
183
+ room_id = event.get_room_id() # 获取房间ID
184
+ event_type = event.get_matrix_event_type() # 获取原始Matrix事件类型
185
+ sender = event.get_matrix_sender() # 获取发送者ID
186
+ is_edited = event.is_edited() # 是否为编辑消息
187
+ is_notice = event.is_notice() # 是否为 m.notice 类型
188
+ ```
189
+
190
+ ## 注意事项:
191
+
192
+ 1. 确保在调用 `startup()` 前完成所有处理器的注册
193
+ 2. Matrix 是去中心化协议,用户ID格式为 `@user:server.domain`,房间ID格式为 `!room_id:server.domain`
194
+ 3. Matrix 不区分群聊和私聊,所有会话都是"房间"。适配器通过 DM 账户数据自动识别私聊
195
+ 4. 适配器使用 Long Polling(`/sync` API)获取事件,而非 WebSocket
196
+ 5. 媒体文件通过 `mxc://` URI 引用,适配器支持自动上传和下载
197
+ 6. 程序退出时请调用 `shutdown()` 确保资源释放
198
+ 7. 支持自动接受房间邀请(可通过 `auto_accept_invites` 配置关闭)
199
+ 8. 支持发送 HTML 格式消息,使用 `.Html()` 方法
200
+ 9. 支持消息回复(`.Reply()`)和用户提及(`.At()`、`.AtAll()`)
201
+
202
+ ---
203
+
204
+ ### 参考链接
205
+
206
+ - [ErisPulse 主库](https://github.com/ErisPulse/ErisPulse/)
207
+ - [Matrix 协议规范](https://spec.matrix.org/)
208
+ - [Matrix Client-Server API](https://spec.matrix.org/v1.11/client-server-api/)
209
+ - [模块开发指南](https://www.erisdev.com/#docs/developer-guide/README.md)
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ ErisPulse_MatrixAdapter.egg-info/PKG-INFO
4
+ ErisPulse_MatrixAdapter.egg-info/SOURCES.txt
5
+ ErisPulse_MatrixAdapter.egg-info/dependency_links.txt
6
+ ErisPulse_MatrixAdapter.egg-info/entry_points.txt
7
+ ErisPulse_MatrixAdapter.egg-info/requires.txt
8
+ ErisPulse_MatrixAdapter.egg-info/top_level.txt
9
+ MatrixAdapter/Converter.py
10
+ MatrixAdapter/Core.py
11
+ MatrixAdapter/__init__.py
12
+ test/test.py
@@ -0,0 +1,2 @@
1
+ [erispulse.adapter]
2
+ matrix = MatrixAdapter:MatrixAdapter
@@ -0,0 +1,284 @@
1
+ import time
2
+ import uuid
3
+ from typing import Dict, Optional, List
4
+
5
+
6
+ class MatrixConverter:
7
+ def __init__(self, bot_user_id: str = ""):
8
+ self.bot_user_id = bot_user_id
9
+ self._dm_rooms: Dict[str, str] = {}
10
+
11
+ def set_dm_rooms(self, dm_rooms: Dict[str, str]):
12
+ self._dm_rooms = dm_rooms
13
+
14
+ def convert(self, raw_event: Dict, room_id: str = "", is_dm: bool = False) -> Optional[Dict]:
15
+ if not isinstance(raw_event, dict):
16
+ return None
17
+
18
+ event_type = raw_event.get("type", "")
19
+ event_id = raw_event.get("event_id", str(uuid.uuid4()))
20
+
21
+ origin_server_ts = raw_event.get("origin_server_ts", 0)
22
+ if origin_server_ts:
23
+ if origin_server_ts > 10**12:
24
+ event_time = int(origin_server_ts / 1000)
25
+ else:
26
+ event_time = int(origin_server_ts)
27
+ else:
28
+ event_time = int(time.time())
29
+
30
+ sender = raw_event.get("sender", "")
31
+ if sender == self.bot_user_id:
32
+ return None
33
+
34
+ base_event = {
35
+ "id": event_id,
36
+ "time": event_time,
37
+ "type": "",
38
+ "detail_type": "",
39
+ "platform": "matrix",
40
+ "self": {
41
+ "platform": "matrix",
42
+ "user_id": self.bot_user_id,
43
+ },
44
+ "matrix_raw": raw_event,
45
+ "matrix_raw_type": event_type,
46
+ }
47
+
48
+ if is_dm:
49
+ base_event["detail_type"] = "private"
50
+ else:
51
+ base_event["detail_type"] = "group"
52
+
53
+ handler = getattr(self, f"_handle_{event_type.replace('.', '_')}", None)
54
+ if handler:
55
+ return handler(raw_event, base_event, room_id, is_dm)
56
+
57
+ if event_type.startswith("m.room."):
58
+ return self._handle_m_room_generic(raw_event, base_event, room_id, is_dm)
59
+
60
+ return self._create_unknown_event(raw_event, event_id, event_type)
61
+
62
+ def _handle_m_room_message(self, raw_event: Dict, base_event: Dict, room_id: str, is_dm: bool) -> Dict:
63
+ base_event["type"] = "message"
64
+ base_event["user_id"] = raw_event.get("sender", "")
65
+ base_event["user_nickname"] = raw_event.get("sender", "")
66
+
67
+ content = raw_event.get("content", {})
68
+ msgtype = content.get("msgtype", "m.text")
69
+
70
+ message_segments = self._parse_message_content(content, raw_event)
71
+ alt_message = self._generate_alt_message(message_segments)
72
+
73
+ base_event["message"] = message_segments
74
+ base_event["alt_message"] = alt_message
75
+
76
+ if room_id:
77
+ base_event["matrix_room_id"] = room_id
78
+ if is_dm:
79
+ pass
80
+ else:
81
+ base_event["group_id"] = room_id
82
+
83
+ relates_to = content.get("m.relates_to", {})
84
+ if relates_to:
85
+ rel_type = relates_to.get("rel_type", "")
86
+ if rel_type == "m.in_reply_to":
87
+ reply_event_id = relates_to.get("event_id", "")
88
+ if reply_event_id:
89
+ reply_seg = {"type": "reply", "data": {"message_id": reply_event_id}}
90
+ base_event["message"].insert(0, reply_seg)
91
+ elif rel_type == "m.thread":
92
+ base_event["thread_id"] = relates_to.get("event_id", "")
93
+
94
+ new_content = content.get("m.new_content")
95
+ if new_content:
96
+ base_event["matrix_edit"] = True
97
+ base_event["matrix_original_event_id"] = relates_to.get("event_id", "")
98
+
99
+ return base_event
100
+
101
+ def _handle_m_room_member(self, raw_event: Dict, base_event: Dict, room_id: str, is_dm: bool) -> Dict:
102
+ base_event["type"] = "notice"
103
+ base_event["user_id"] = raw_event.get("sender", "")
104
+ base_event["user_nickname"] = raw_event.get("sender", "")
105
+
106
+ content = raw_event.get("content", {})
107
+ prev_content = raw_event.get("unsigned", {}).get("prev_content", {})
108
+ membership = content.get("membership", "")
109
+ prev_membership = prev_content.get("membership", "")
110
+
111
+ state_key = raw_event.get("state_key", "")
112
+ target_user = state_key
113
+
114
+ if membership == "join" and prev_membership != "join":
115
+ base_event["detail_type"] = "group_member_increase"
116
+ base_event["user_id"] = target_user
117
+ displayname = content.get("displayname", target_user)
118
+ base_event["user_nickname"] = displayname
119
+ base_event["operator_id"] = raw_event.get("sender", "")
120
+ base_event["group_id"] = room_id
121
+ base_event["matrix_room_id"] = room_id
122
+ elif membership in ("leave", "ban") and prev_membership == "join":
123
+ base_event["detail_type"] = "group_member_decrease"
124
+ base_event["user_id"] = target_user
125
+ base_event["user_nickname"] = content.get("displayname", target_user)
126
+ base_event["operator_id"] = raw_event.get("sender", "")
127
+ base_event["group_id"] = room_id
128
+ base_event["matrix_room_id"] = room_id
129
+ else:
130
+ base_event["detail_type"] = "matrix_member_update"
131
+ base_event["user_id"] = target_user
132
+ base_event["group_id"] = room_id
133
+ base_event["matrix_room_id"] = room_id
134
+ base_event["matrix_membership"] = membership
135
+
136
+ return base_event
137
+
138
+ def _handle_m_reaction(self, raw_event: Dict, base_event: Dict, room_id: str, is_dm: bool) -> Dict:
139
+ base_event["type"] = "notice"
140
+ base_event["detail_type"] = "matrix_reaction"
141
+ base_event["user_id"] = raw_event.get("sender", "")
142
+ base_event["matrix_room_id"] = room_id
143
+ if not is_dm:
144
+ base_event["group_id"] = room_id
145
+
146
+ content = raw_event.get("content", {})
147
+ relates_to = content.get("m.relates_to", {})
148
+ base_event["matrix_reaction_event_id"] = relates_to.get("event_id", "")
149
+ base_event["matrix_reaction_key"] = relates_to.get("key", "")
150
+
151
+ return base_event
152
+
153
+ def _handle_m_room_redaction(self, raw_event: Dict, base_event: Dict, room_id: str, is_dm: bool) -> Dict:
154
+ base_event["type"] = "notice"
155
+ base_event["detail_type"] = "matrix_redaction"
156
+ base_event["user_id"] = raw_event.get("sender", "")
157
+ base_event["matrix_room_id"] = room_id
158
+ if not is_dm:
159
+ base_event["group_id"] = room_id
160
+
161
+ redacts = raw_event.get("redacts", "")
162
+ base_event["matrix_redacted_event_id"] = redacts
163
+
164
+ return base_event
165
+
166
+ def _handle_m_room_generic(self, raw_event: Dict, base_event: Dict, room_id: str, is_dm: bool) -> Dict:
167
+ event_type = raw_event.get("type", "")
168
+ base_event["type"] = "notice"
169
+ base_event["detail_type"] = f"matrix_{event_type.replace('.', '_').replace('m_room_', '')}"
170
+ base_event["user_id"] = raw_event.get("sender", "")
171
+ base_event["matrix_room_id"] = room_id
172
+ if not is_dm:
173
+ base_event["group_id"] = room_id
174
+
175
+ return base_event
176
+
177
+ def _parse_message_content(self, content: Dict, raw_event: Dict) -> List[Dict]:
178
+ segments = []
179
+ msgtype = content.get("msgtype", "m.text")
180
+ body = content.get("body", "")
181
+ formatted_body = content.get("formatted_body", "")
182
+
183
+ if msgtype in ("m.text", "m.notice", "m.emote"):
184
+ seg_data = {"text": body}
185
+ if formatted_body:
186
+ seg_data["html"] = formatted_body
187
+ segments.append({"type": "text", "data": seg_data})
188
+
189
+ elif msgtype == "m.image":
190
+ url = content.get("url", "")
191
+ segments.append({
192
+ "type": "image",
193
+ "data": {
194
+ "url": url,
195
+ "filename": body,
196
+ "matrix_mxc": url,
197
+ "info": content.get("info", {}),
198
+ },
199
+ })
200
+
201
+ elif msgtype == "m.audio":
202
+ url = content.get("url", "")
203
+ segments.append({
204
+ "type": "voice",
205
+ "data": {
206
+ "url": url,
207
+ "filename": body,
208
+ "matrix_mxc": url,
209
+ "info": content.get("info", {}),
210
+ },
211
+ })
212
+
213
+ elif msgtype == "m.video":
214
+ url = content.get("url", "")
215
+ segments.append({
216
+ "type": "video",
217
+ "data": {
218
+ "url": url,
219
+ "filename": body,
220
+ "matrix_mxc": url,
221
+ "info": content.get("info", {}),
222
+ },
223
+ })
224
+
225
+ elif msgtype == "m.file":
226
+ url = content.get("url", "")
227
+ segments.append({
228
+ "type": "file",
229
+ "data": {
230
+ "url": url,
231
+ "filename": body,
232
+ "matrix_mxc": url,
233
+ "info": content.get("info", {}),
234
+ },
235
+ })
236
+
237
+ elif msgtype == "m.location":
238
+ geo_uri = content.get("geo_uri", "")
239
+ segments.append({
240
+ "type": "location",
241
+ "data": {
242
+ "latitude": 0.0,
243
+ "longitude": 0.0,
244
+ "matrix_geo_uri": geo_uri,
245
+ "text": body,
246
+ },
247
+ })
248
+
249
+ return segments
250
+
251
+ def _create_unknown_event(self, raw_event: Dict, event_id: str, event_type: str) -> Dict:
252
+ return {
253
+ "id": event_id,
254
+ "time": int(time.time()),
255
+ "type": "unknown",
256
+ "detail_type": "unknown",
257
+ "platform": "matrix",
258
+ "self": {"platform": "matrix", "user_id": self.bot_user_id},
259
+ "matrix_raw": raw_event,
260
+ "matrix_raw_type": event_type,
261
+ "warning": f"Unsupported event type: {event_type}",
262
+ "alt_message": "This event type is not supported by this system.",
263
+ }
264
+
265
+ def _generate_alt_message(self, segments: List[Dict]) -> str:
266
+ parts = []
267
+ for seg in segments:
268
+ seg_type = seg["type"]
269
+ data = seg.get("data", {})
270
+ if seg_type == "text":
271
+ parts.append(data.get("text", ""))
272
+ elif seg_type == "image":
273
+ parts.append("[图片]")
274
+ elif seg_type == "voice":
275
+ parts.append("[语音]")
276
+ elif seg_type == "video":
277
+ parts.append("[视频]")
278
+ elif seg_type == "file":
279
+ parts.append(f"[文件:{data.get('filename', '')}]")
280
+ elif seg_type == "location":
281
+ parts.append("[位置]")
282
+ elif seg_type == "reply":
283
+ parts.append("[回复]")
284
+ return " ".join(parts).strip()