butterbot-python 3.1.0.dev1__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.
Files changed (79) hide show
  1. butterbot/__init__.py +15 -0
  2. butterbot/app/__init__.py +41 -0
  3. butterbot/app/bot_app.py +374 -0
  4. butterbot/app/config.py +156 -0
  5. butterbot/app/source_manager.py +367 -0
  6. butterbot/core/__init__.py +46 -0
  7. butterbot/core/api/__init__.py +9 -0
  8. butterbot/core/api/base_api.py +32 -0
  9. butterbot/core/context/README.md +1 -0
  10. butterbot/core/context/__init__.py +9 -0
  11. butterbot/core/context/api_registry.py +109 -0
  12. butterbot/core/context/app_context.py +51 -0
  13. butterbot/core/context/config_provider.py +14 -0
  14. butterbot/core/data/__init__.py +15 -0
  15. butterbot/core/data/base_data.py +29 -0
  16. butterbot/core/data/base_model.py +197 -0
  17. butterbot/core/event/README.md +1 -0
  18. butterbot/core/event/__init__.py +10 -0
  19. butterbot/core/event/event.py +27 -0
  20. butterbot/core/event/event_bus.py +276 -0
  21. butterbot/core/event/subscriber.py +130 -0
  22. butterbot/core/exceptions.py +90 -0
  23. butterbot/core/filter/__init__.py +11 -0
  24. butterbot/core/filter/base_filter.py +74 -0
  25. butterbot/core/source/README.md +1 -0
  26. butterbot/core/source/__init__.py +9 -0
  27. butterbot/core/source/base_source.py +134 -0
  28. butterbot/core/types/__init__.py +9 -0
  29. butterbot/core/types/base_type.py +74 -0
  30. butterbot/sources/__init__.py +1 -0
  31. butterbot/sources/bilibili/README.md +10 -0
  32. butterbot/sources/bilibili/__init__.py +20 -0
  33. butterbot/sources/bilibili/api/__init__.py +5 -0
  34. butterbot/sources/bilibili/api/bili_api.py +123 -0
  35. butterbot/sources/bilibili/data/__init__.py +52 -0
  36. butterbot/sources/bilibili/data/danmaku_gift_data.py +135 -0
  37. butterbot/sources/bilibili/data/danmaku_guard_data.py +54 -0
  38. butterbot/sources/bilibili/data/danmaku_msg_data.py +71 -0
  39. butterbot/sources/bilibili/data/dto/__init__.py +59 -0
  40. butterbot/sources/bilibili/data/dto/danmaku_gift_dto.py +193 -0
  41. butterbot/sources/bilibili/data/dto/danmaku_guard_buy_dto.py +54 -0
  42. butterbot/sources/bilibili/data/dto/danmaku_msg_dto.py +123 -0
  43. butterbot/sources/bilibili/data/dto/dynamic_dto.py +276 -0
  44. butterbot/sources/bilibili/data/dto/live_room_dto.py +169 -0
  45. butterbot/sources/bilibili/data/dto/video_part_dto.py +18 -0
  46. butterbot/sources/bilibili/data/dynamic_data.py +362 -0
  47. butterbot/sources/bilibili/data/live_room_data.py +162 -0
  48. butterbot/sources/bilibili/data/video_part.py +46 -0
  49. butterbot/sources/bilibili/source/__init__.py +15 -0
  50. butterbot/sources/bilibili/source/base_polling_source.py +130 -0
  51. butterbot/sources/bilibili/source/bili_danmaku_source.py +230 -0
  52. butterbot/sources/bilibili/source/bili_dynamic_source.py +135 -0
  53. butterbot/sources/bilibili/source/bili_live_source.py +137 -0
  54. butterbot/sources/bilibili/types/__init__.py +11 -0
  55. butterbot/sources/bilibili/types/bili_type.py +32 -0
  56. butterbot/sources/napcat/README.md +10 -0
  57. butterbot/sources/napcat/__init__.py +20 -0
  58. butterbot/sources/napcat/api/__init__.py +3 -0
  59. butterbot/sources/napcat/api/napcat_api.py +316 -0
  60. butterbot/sources/napcat/data/__init__.py +179 -0
  61. butterbot/sources/napcat/data/event_data.py +441 -0
  62. butterbot/sources/napcat/data/segment_data.py +432 -0
  63. butterbot/sources/napcat/events.py +91 -0
  64. butterbot/sources/napcat/filters/__init__.py +19 -0
  65. butterbot/sources/napcat/filters/filters.py +202 -0
  66. butterbot/sources/napcat/source/__init__.py +5 -0
  67. butterbot/sources/napcat/source/napcat_source.py +58 -0
  68. butterbot/sources/napcat/types/__init__.py +5 -0
  69. butterbot/sources/napcat/types/napcat_type.py +61 -0
  70. butterbot/utils/README.md +15 -0
  71. butterbot/utils/__init__.py +11 -0
  72. butterbot/utils/data_pair.py +27 -0
  73. butterbot/utils/logging_config.py +521 -0
  74. butterbot/utils/terminal.py +308 -0
  75. butterbot/utils/websocket.py +1270 -0
  76. butterbot_python-3.1.0.dev1.dist-info/METADATA +769 -0
  77. butterbot_python-3.1.0.dev1.dist-info/RECORD +79 -0
  78. butterbot_python-3.1.0.dev1.dist-info/WHEEL +4 -0
  79. butterbot_python-3.1.0.dev1.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,20 @@
1
+ from .api import (
2
+ NapcatApi,
3
+ NapcatConfig,
4
+ )
5
+ from .source import (
6
+ NapcatSource,
7
+ )
8
+ from .types import (
9
+ NapcatType,
10
+ )
11
+
12
+ __all__ = [
13
+ # API类
14
+ "NapcatApi",
15
+ "NapcatConfig",
16
+ # 事件源类
17
+ "NapcatSource",
18
+ # 数据类型类
19
+ "NapcatType",
20
+ ]
@@ -0,0 +1,3 @@
1
+ from .napcat_api import NapcatApi, NapcatClient, NapcatConfig
2
+
3
+ __all__ = ["NapcatApi", "NapcatClient", "NapcatConfig"]
@@ -0,0 +1,316 @@
1
+ import asyncio
2
+ import json
3
+ from collections.abc import Awaitable, Callable
4
+ from dataclasses import dataclass
5
+ from logging import getLogger
6
+ from typing import Any
7
+ from uuid import uuid4
8
+
9
+ from butterbot.core.api import BaseApi
10
+ from butterbot.core.context import ApiRegistry
11
+ from butterbot.utils import AsyncWebSocketClient, ListenerId, MessageType
12
+ from butterbot.utils.websocket import ListenerClosedError, ListenerEvictedError
13
+
14
+ _log = getLogger("NapcatApi")
15
+
16
+
17
+ @dataclass
18
+ class NapcatConfig:
19
+ """Napcat 客户端配置
20
+
21
+ Attributes:
22
+ url: WebSocket 连接地址
23
+ token: 认证 Token(可选)
24
+ heartbeat: 心跳间隔(秒)
25
+ reconnect_attempts: 重连尝试次数
26
+ receive_timeout: 接收超时时间(秒)
27
+ """
28
+
29
+ url: str
30
+ """WebSocket 连接地址"""
31
+ token: str | None = None
32
+ """认证 Token(可选)"""
33
+ heartbeat: float = 30.0
34
+ """心跳间隔(秒)"""
35
+ reconnect_attempts: int = 5
36
+ """重连尝试次数"""
37
+ receive_timeout: float = 60.0
38
+ """接收超时时间(秒)"""
39
+
40
+
41
+ class NapcatClient:
42
+ """Napcat WebSocket 客户端,用于与 Napcat QQ Bot 进行通信"""
43
+
44
+ @classmethod
45
+ def create(cls, config: NapcatConfig) -> "NapcatClient":
46
+ """从配置创建 NapcatClient 实例
47
+
48
+ Args:
49
+ config: napcat 配置
50
+ """
51
+ napcat_config: NapcatConfig = config
52
+ headers = {}
53
+
54
+ if napcat_config.token:
55
+ headers["Authorization"] = napcat_config.token
56
+
57
+ return cls(
58
+ url=napcat_config.url,
59
+ headers=headers,
60
+ heartbeat=napcat_config.heartbeat,
61
+ reconnect_attempts=napcat_config.reconnect_attempts,
62
+ receive_timeout=napcat_config.receive_timeout,
63
+ )
64
+
65
+ def __init__(
66
+ self,
67
+ url: str,
68
+ headers: dict[str, str] | None = None,
69
+ heartbeat: float = 30.0,
70
+ reconnect_attempts: int = 5,
71
+ receive_timeout: float = 60.0,
72
+ ):
73
+ """初始化 Napcat 客户端
74
+
75
+ Args:
76
+ url: WebSocket 连接地址
77
+ headers: 请求头,通常包含 Authorization
78
+ heartbeat: 心跳间隔
79
+ reconnect_attempts: 重连尝试次数
80
+ receive_timeout: 接收超时时间
81
+ """
82
+ self.url = url
83
+ self._handler: Callable[[dict[str, Any]], Awaitable[None]] | None = None
84
+ self.timeout = receive_timeout
85
+ self.client = AsyncWebSocketClient(
86
+ uri=url,
87
+ logger=_log,
88
+ headers=headers or {},
89
+ heartbeat=heartbeat,
90
+ reconnect_attempts=reconnect_attempts,
91
+ receive_timeout=receive_timeout,
92
+ )
93
+ self._task: asyncio.Task | None = None
94
+ self._listener_id: ListenerId | None = None
95
+ self._pending_requests: dict[str, asyncio.Future[dict[str, Any]]] = {}
96
+
97
+ @property
98
+ def pending_requests(self) -> int:
99
+ """当前等待 echo 响应的请求数量."""
100
+ return len(self._pending_requests)
101
+
102
+ def set_handler(self, handler: Callable[[dict[str, Any]], Awaitable[None]]):
103
+ """设置消息处理函数
104
+
105
+ Args:
106
+ handler: 消息处理函数,必须是异步函数,接受一个 dict 参数
107
+ """
108
+ if not asyncio.iscoroutinefunction(handler):
109
+ raise TypeError("handler must be an async function")
110
+ self._handler = handler
111
+
112
+ async def start(self):
113
+ """启动客户端并创建监听器"""
114
+ if not self._handler:
115
+ raise RuntimeError(
116
+ "消息处理函数未设置,请先调用 set_handler() 设置处理函数"
117
+ )
118
+ await self.client.start()
119
+ self._listener_id = await self.client.create_listener()
120
+ self._task = asyncio.create_task(self._process_messages())
121
+ _log.info("Napcat client started with listener: %s", self._listener_id)
122
+
123
+ async def stop(self):
124
+ """停止客户端(幂等,可被 on_stop 与 ApiRegistry.aclose_all 重复调用)"""
125
+ pending = list(self._pending_requests.values())
126
+ self._pending_requests.clear()
127
+ for future in pending:
128
+ if not future.done():
129
+ future.cancel()
130
+
131
+ task = self._task
132
+ self._task = None
133
+ if task and not task.done():
134
+ try:
135
+ task.cancel()
136
+ await task
137
+ except asyncio.CancelledError:
138
+ # 任务被取消是预期行为,忽略异常
139
+ pass
140
+
141
+ listener_id = self._listener_id
142
+ self._listener_id = None
143
+ if listener_id:
144
+ await self.client.remove_listener(listener_id)
145
+
146
+ await self.client.stop()
147
+ _log.info("Napcat client stopped")
148
+
149
+ async def send_request(self, message: dict) -> dict | None:
150
+ """发送请求到服务器
151
+
152
+ Args:
153
+ message: 请求内容,Dict
154
+ """
155
+ echo = str(uuid4())
156
+ payload = dict(message)
157
+ payload["echo"] = echo
158
+ future = asyncio.get_running_loop().create_future()
159
+ self._pending_requests[echo] = future
160
+
161
+ try:
162
+ _log.debug("发送请求: action=%s, echo=%s", payload.get("action"), echo)
163
+ await self.client.send(payload)
164
+ _log.debug("发送请求%s", echo)
165
+ return await asyncio.wait_for(future, timeout=self.timeout)
166
+ except asyncio.CancelledError:
167
+ _log.debug("请求 %s 被取消", echo)
168
+ raise
169
+ finally:
170
+ registered = self._pending_requests.pop(echo, None)
171
+ if registered is not None and not registered.done():
172
+ registered.cancel()
173
+
174
+ def _resolve_response(self, data: dict[str, Any]) -> bool:
175
+ """按 echo 将响应投递给对应请求.
176
+
177
+ Returns:
178
+ 找到仍在等待的请求并完成其 Future 时返回 ``True``。
179
+ """
180
+ echo = data.get("echo")
181
+ if echo is None:
182
+ return False
183
+ future = self._pending_requests.get(str(echo))
184
+ if future is None or future.done():
185
+ return False
186
+ future.set_result(data)
187
+ return True
188
+
189
+ async def _get_message(
190
+ self, listener_id: ListenerId | None = None
191
+ ) -> tuple[Any, MessageType]:
192
+ """获取一条消息(阻塞)
193
+ Returns:
194
+ (消息内容, 消息类型) 元组
195
+ """
196
+ _listener_id = self._listener_id if listener_id is None else listener_id
197
+ assert _listener_id is not None, "Listener not available"
198
+ return await self.client.get_message(_listener_id, self.timeout)
199
+
200
+ async def _process_messages(self):
201
+ """持续处理消息(事件循环)"""
202
+ _log.info("Started processing messages")
203
+
204
+ try:
205
+ while self.client.running:
206
+ try:
207
+ message, msg_type = await self._get_message()
208
+
209
+ if msg_type == MessageType.Text:
210
+ # 解析 JSON 消息
211
+ try:
212
+ data: dict = json.loads(message)
213
+ if data.get("echo") is not None:
214
+ if not self._resolve_response(data):
215
+ _log.debug(
216
+ "收到无等待者的响应: echo=%s", data.get("echo")
217
+ )
218
+ # 带 echo 的响应不进入事件 handler
219
+ continue
220
+ _log.debug(data)
221
+ # noinspection PyCallingNonCallable
222
+ assert self._handler is not None
223
+ await self._handler(
224
+ data
225
+ ) # post_type: ignore (运行时设置 handler)
226
+ except json.JSONDecodeError:
227
+ _log.error("Failed to parse message: %s", message)
228
+ except Exception as e:
229
+ _log.error("Handler error: %s", e)
230
+ elif msg_type == MessageType.Close:
231
+ _log.warning("Received close message")
232
+ break
233
+
234
+ except TimeoutError:
235
+ # 超时是正常的,继续等待
236
+ continue
237
+ except (ListenerClosedError, ListenerEvictedError) as e:
238
+ # 监听器已关闭或被驱逐:这条 while 再转下去只会立刻拿到
239
+ # 同一个异常,变成不带任何 sleep 的 100% CPU 空转。
240
+ _log.warning("监听器不可用,停止消息处理: %s", e)
241
+ break
242
+ except Exception as e:
243
+ _log.error("Error processing message: %s", e)
244
+ except asyncio.CancelledError:
245
+ # 任务被取消(stop() 调用),优雅退出
246
+ _log.info("Message processing stopped")
247
+
248
+ async def __aenter__(self):
249
+ """异步上下文管理器入口"""
250
+ await self.start()
251
+ return self
252
+
253
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
254
+ """异步上下文管理器出口"""
255
+ await self.stop()
256
+
257
+
258
+ class NapcatApi(BaseApi):
259
+ """Napcat API,提供与 Napcat QQ Bot 交互的接口"""
260
+
261
+ @classmethod
262
+ def create(cls, ctx: ApiRegistry, config_key: str = "napcat") -> "NapcatApi":
263
+ """
264
+ 从上下文创建 NapcatApi 实例
265
+ Args:
266
+ ctx: API 上下文
267
+ config_key: 配置键
268
+
269
+ Raises:
270
+ ConfigError: 缺少 ``config_key`` 对应的 napcat 配置。
271
+ napcat 必须有 url/token 才能连接,配置缺失时用 require_config
272
+ 直接报错,而不是把 None 一路带到深处变成
273
+ ``AttributeError: 'NoneType' object has no attribute 'token'``。
274
+ """
275
+ return cls(ctx.require_config(config_key))
276
+
277
+ def __init__(self, config: NapcatConfig):
278
+ self.client = NapcatClient.create(config)
279
+
280
+ async def aclose(self) -> None:
281
+ """释放 WebSocket 连接与后台任务(由 ApiRegistry.aclose_all 调用)."""
282
+ await self.client.stop()
283
+
284
+ def set_handler(self, handler: Callable[[dict[str, Any]], Awaitable[None]]):
285
+ """设置消息处理函数"""
286
+ self.client.set_handler(handler)
287
+
288
+ async def start(self):
289
+ """启动客户端"""
290
+ await self.client.start()
291
+
292
+ async def stop(self):
293
+ """停止客户端"""
294
+ await self.client.stop()
295
+
296
+ def get_metrics(self) -> dict:
297
+ """获取客户端指标"""
298
+ return self.client.client.get_metrics()
299
+
300
+ async def send_request(self, message: dict) -> dict | None:
301
+ """发送请求到服务器"""
302
+ return await self.client.send_request(message)
303
+
304
+ # ================== 业务接口 ================== #
305
+
306
+ async def send_group_message(
307
+ self, group_id: int, message: list[dict]
308
+ ) -> dict | None:
309
+ """发送群消息"""
310
+ results = await self.send_request(
311
+ {
312
+ "action": "send_group_msg",
313
+ "params": {"group_id": group_id, "message": message},
314
+ }
315
+ )
316
+ return results
@@ -0,0 +1,179 @@
1
+ """
2
+ NapCat OneBot11 数据模型
3
+
4
+ 包含消息段和事件的数据模型定义
5
+ """
6
+
7
+ from .event_data import (
8
+ EmojiLike,
9
+ FileInfo,
10
+ # 嵌套数据类
11
+ FriendSender,
12
+ GroupSender,
13
+ HeartbeatStatus,
14
+ # 事件基类
15
+ NapcatData,
16
+ NapcatFriendAddNoticeData,
17
+ NapcatFriendRecallNoticeData,
18
+ NapcatFriendRequestData,
19
+ NapcatGroupAdminNoticeData,
20
+ NapcatGroupBanNoticeData,
21
+ NapcatGroupCardNoticeData,
22
+ NapcatGroupDecreaseNoticeData,
23
+ NapcatGroupEssenceNoticeData,
24
+ NapcatGroupIncreaseNoticeData,
25
+ NapcatGroupMessageData,
26
+ NapcatGroupMessageSentData,
27
+ NapcatGroupMsgEmojiLikeNoticeData,
28
+ NapcatGroupRecallNoticeData,
29
+ NapcatGroupRequestData,
30
+ NapcatGroupUploadNoticeData,
31
+ NapcatHeartbeatMetaData,
32
+ NapcatHonorNotifyData,
33
+ NapcatLifecycleMetaData,
34
+ NapcatLuckyKingNotifyData,
35
+ # 消息事件
36
+ NapcatMessageData,
37
+ NapcatMessageSentData,
38
+ # 元事件
39
+ NapcatMetaData,
40
+ # 通知事件
41
+ NapcatNoticeData,
42
+ NapcatNotifyData,
43
+ NapcatPokeNotifyData,
44
+ NapcatPrivateMessageData,
45
+ NapcatPrivateMessageSentData,
46
+ NapcatReactionNoticeData,
47
+ # 请求事件
48
+ NapcatRequestData,
49
+ )
50
+ from .segment_data import (
51
+ AnonymousData,
52
+ AnonymousNode,
53
+ AtData,
54
+ AtNode,
55
+ ContactData,
56
+ ContactNode,
57
+ DiceData,
58
+ DiceNode,
59
+ FaceData,
60
+ FaceNode,
61
+ FileData,
62
+ FileNode,
63
+ ForwardData,
64
+ ForwardNode,
65
+ ImageData,
66
+ ImageNode,
67
+ JsonData,
68
+ JsonNode,
69
+ LocationData,
70
+ LocationNode,
71
+ # 消息段基类与子类
72
+ MessageNode,
73
+ MusicData,
74
+ MusicNode,
75
+ NodeData,
76
+ NodeNode,
77
+ PokeData,
78
+ PokeNode,
79
+ RecordData,
80
+ RecordNode,
81
+ ReplyData,
82
+ ReplyNode,
83
+ RpsData,
84
+ RpsNode,
85
+ ShakeData,
86
+ ShakeNode,
87
+ ShareData,
88
+ ShareNode,
89
+ # 嵌套数据类
90
+ TextData,
91
+ TextNode,
92
+ VideoData,
93
+ VideoNode,
94
+ XmlData,
95
+ XmlNode,
96
+ )
97
+
98
+ __all__ = [
99
+ "AnonymousData",
100
+ "AnonymousNode",
101
+ "AtData",
102
+ "AtNode",
103
+ "ContactData",
104
+ "ContactNode",
105
+ "DiceData",
106
+ "DiceNode",
107
+ "EmojiLike",
108
+ "FaceData",
109
+ "FaceNode",
110
+ "FileData",
111
+ "FileInfo",
112
+ "FileNode",
113
+ "ForwardData",
114
+ "ForwardNode",
115
+ # event_data
116
+ "FriendSender",
117
+ "GroupSender",
118
+ "HeartbeatStatus",
119
+ "ImageData",
120
+ "ImageNode",
121
+ "JsonData",
122
+ "JsonNode",
123
+ "LocationData",
124
+ "LocationNode",
125
+ "MessageNode",
126
+ "MusicData",
127
+ "MusicNode",
128
+ "NapcatData",
129
+ "NapcatFriendAddNoticeData",
130
+ "NapcatFriendRecallNoticeData",
131
+ "NapcatFriendRequestData",
132
+ "NapcatGroupAdminNoticeData",
133
+ "NapcatGroupBanNoticeData",
134
+ "NapcatGroupCardNoticeData",
135
+ "NapcatGroupDecreaseNoticeData",
136
+ "NapcatGroupEssenceNoticeData",
137
+ "NapcatGroupIncreaseNoticeData",
138
+ "NapcatGroupMessageData",
139
+ "NapcatGroupMessageSentData",
140
+ "NapcatGroupMsgEmojiLikeNoticeData",
141
+ "NapcatGroupRecallNoticeData",
142
+ "NapcatGroupRequestData",
143
+ "NapcatGroupUploadNoticeData",
144
+ "NapcatHeartbeatMetaData",
145
+ "NapcatHonorNotifyData",
146
+ "NapcatLifecycleMetaData",
147
+ "NapcatLuckyKingNotifyData",
148
+ "NapcatMessageData",
149
+ "NapcatMessageSentData",
150
+ "NapcatMetaData",
151
+ "NapcatNoticeData",
152
+ "NapcatNotifyData",
153
+ "NapcatPokeNotifyData",
154
+ "NapcatPrivateMessageData",
155
+ "NapcatPrivateMessageSentData",
156
+ "NapcatReactionNoticeData",
157
+ "NapcatRequestData",
158
+ "NodeData",
159
+ "NodeNode",
160
+ "PokeData",
161
+ "PokeNode",
162
+ "RecordData",
163
+ "RecordNode",
164
+ "ReplyData",
165
+ "ReplyNode",
166
+ "RpsData",
167
+ "RpsNode",
168
+ "ShakeData",
169
+ "ShakeNode",
170
+ "ShareData",
171
+ "ShareNode",
172
+ # segment_data
173
+ "TextData",
174
+ "TextNode",
175
+ "VideoData",
176
+ "VideoNode",
177
+ "XmlData",
178
+ "XmlNode",
179
+ ]