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,276 @@
1
+ import json
2
+ from logging import getLogger
3
+ from typing import Any, ClassVar, Optional
4
+
5
+ from butterbot.core.data import BaseDataModel
6
+
7
+ _log = getLogger("DynamicDTO")
8
+
9
+
10
+ class AuthorDto(BaseDataModel):
11
+ """
12
+ 作者信息DTO
13
+ """
14
+
15
+ uid: int # UP主UID
16
+ name: str # UP主昵称
17
+ face: str # UP主头像URL
18
+
19
+
20
+ class StatDto(BaseDataModel):
21
+ """
22
+ 动态统计信息DTO
23
+ """
24
+
25
+ comment_count: int = 0 # 评论数
26
+ like_count: int = 0 # 点赞数
27
+ forward_count: int = 0 # 转发数
28
+
29
+
30
+ class VideoDto(BaseDataModel):
31
+ """
32
+ 视频信息DTO
33
+ """
34
+
35
+ av_id: str # 视频AV号
36
+ bv_id: str # 视频BV号
37
+ title: str # 视频标题
38
+ cover: str # 视频封面
39
+ desc: str # 视频简介
40
+ duration_text: str # 视频时长
41
+ play_count: str # 播放数
42
+ danmaku_count: str # 弹幕数
43
+ dynamic_text: str = "" # 动态文本
44
+
45
+
46
+ class MusicDto(BaseDataModel):
47
+ """
48
+ 音乐信息DTO
49
+ """
50
+
51
+ music_id: str # 音乐ID
52
+ title: str # 音乐标题
53
+ cover: str # 音乐封面
54
+ label: str # 音乐标签(作者/歌手)
55
+ dynamic_text: str = "" # 动态文本
56
+
57
+
58
+ class ArticleDto(BaseDataModel):
59
+ """
60
+ 专栏信息DTO
61
+ """
62
+
63
+ title: str # 专栏标题
64
+ summary: str # 专栏摘要
65
+ has_more: bool # 是否有更多内容
66
+ id: int
67
+
68
+
69
+ class LiveRcmdDto(BaseDataModel):
70
+ """
71
+ 直播推荐信息DTO
72
+ """
73
+
74
+ room_id: int # 直播间ID
75
+ live_status: int # 直播状态 1:直播中
76
+ title: str # 直播间标题
77
+ cover: str # 直播间封面
78
+ online: int # 在线人数
79
+ area_id: int # 直播分区ID
80
+ area_name: str # 直播分区
81
+ parent_area_id: int # 直播父分区ID
82
+ parent_area_name: str # 直播父分区
83
+ live_start_time: int # 开播时间戳
84
+ watched_num: int | None = None # 观看人数
85
+ switch: bool | None = None # 观看榜开关
86
+ text_small: str | None = None # 小文本
87
+ text_large: str | None = None # 大文本
88
+
89
+
90
+ class DynamicDTO(BaseDataModel):
91
+ """
92
+ 动态消息DTO
93
+ """
94
+
95
+ discriminator_value: ClassVar[str] = "dynamic" # 数据类型标识
96
+
97
+ dynamic_id: str # 动态ID
98
+ dynamic_type: str # 动态类型
99
+ visible: bool # 动态显示状态(false时被折叠)
100
+ pub_time: str # 发布时间
101
+ pub_ts: int # 发布时间戳
102
+ author: AuthorDto # 作者信息
103
+ tag: str | None = None # 标签(如置顶)
104
+ text: str | None = None # 文字内容
105
+ pics_url: tuple[str, ...] | None = None # 图片列表
106
+ stat: StatDto | None = None # 统计信息
107
+ video: VideoDto | None = None # 视频信息
108
+ music: MusicDto | None = None # 音乐信息
109
+ article: ArticleDto | None = None # 专栏信息
110
+ live_rcmd: LiveRcmdDto | None = None # 直播推荐信息
111
+ forward_orig: Optional["DynamicDTO"] = None # 转发的原动态
112
+
113
+ @classmethod
114
+ def from_raw(cls, data: dict[Any, Any] | None) -> "DynamicDTO | None":
115
+ """
116
+ 从原始API数据构造DTO对象
117
+ """
118
+ if data is None:
119
+ return None
120
+ try:
121
+ # 基础信息
122
+ dynamic_id = data.get("id_str", "")
123
+ dynamic_type = data.get("type", "")
124
+ visible = data.get("visible", True)
125
+ modules = data.get("modules", {})
126
+ author_info = modules.get("module_author", {})
127
+ pub_time = author_info.get("pub_time", "")
128
+ pub_ts = author_info.get("pub_ts", 0)
129
+
130
+ # 动态内容
131
+ text = None
132
+ pics_url = None
133
+ video = None
134
+ music = None
135
+ article = None
136
+ live_rcmd = None
137
+
138
+ module_dynamic = modules.get("module_dynamic", {})
139
+ major = module_dynamic.get("major", {})
140
+
141
+ # 作者信息
142
+ author = {
143
+ "uid": author_info.get("mid", 0),
144
+ "name": author_info.get("name", ""),
145
+ "face": author_info.get("face", ""),
146
+ }
147
+
148
+ # 统计信息
149
+ stat = None
150
+ stat_info = modules.get("module_stat")
151
+ if stat_info:
152
+ stat = {
153
+ "comment_count": stat_info.get("comment", {}).get("count", 0),
154
+ "like_count": stat_info.get("like", {}).get("count", 0),
155
+ "forward_count": stat_info.get("forward", {}).get("count", 0),
156
+ }
157
+
158
+ # 标签(如置顶)
159
+ tag = None
160
+ if modules.get("module_tag"):
161
+ tag = modules["module_tag"].get("text")
162
+
163
+ # 解析文字和图片(DYNAMIC_TYPE_WORD, DYNAMIC_TYPE_DRAW)
164
+ if dynamic_type in ["DYNAMIC_TYPE_WORD", "DYNAMIC_TYPE_DRAW"]:
165
+ opus = major.get("opus", {})
166
+ summary = opus.get("summary", {})
167
+ text = summary.get("text", "")
168
+ pics = opus.get("pics", [])
169
+ if pics:
170
+ pics_url = tuple(pic.get("url", "") for pic in pics)
171
+
172
+ # 解析视频信息
173
+ elif dynamic_type == "DYNAMIC_TYPE_AV":
174
+ archive = major.get("archive", {})
175
+ desc_info = module_dynamic.get("desc", {})
176
+ stat_info_video = archive.get("stat", {})
177
+ video = {
178
+ "av_id": archive.get("aid", ""),
179
+ "bv_id": archive.get("bvid", ""),
180
+ "title": archive.get("title", ""),
181
+ "cover": archive.get("cover", ""),
182
+ "desc": archive.get("desc", ""),
183
+ "duration_text": archive.get("duration_text", ""),
184
+ "dynamic_text": desc_info.get("text", "") if desc_info else "",
185
+ "play_count": stat_info_video.get("play"),
186
+ "danmaku_count": stat_info_video.get("danmaku"),
187
+ }
188
+
189
+ # 解析音乐信息
190
+ elif dynamic_type == "DYNAMIC_TYPE_MUSIC":
191
+ music_info = major.get("music", {})
192
+ desc_info = module_dynamic.get("desc", {})
193
+ music = {
194
+ "music_id": str(music_info.get("id", "")),
195
+ "title": music_info.get("title", ""),
196
+ "cover": music_info.get("cover", ""),
197
+ "label": music_info.get("label", ""),
198
+ "dynamic_text": desc_info.get("text", "") if desc_info else "",
199
+ }
200
+
201
+ # 解析专栏信息
202
+ elif dynamic_type == "DYNAMIC_TYPE_ARTICLE":
203
+ opus = major.get("opus", {})
204
+ summary_info = opus.get("summary", {})
205
+ article = {
206
+ "title": opus.get("title", ""),
207
+ "summary": summary_info.get("text", ""),
208
+ "has_more": summary_info.get("has_more", False),
209
+ "id": int(dynamic_id) if dynamic_id else 0,
210
+ }
211
+
212
+ # 解析直播推荐信息
213
+ elif dynamic_type == "DYNAMIC_TYPE_LIVE_RCMD":
214
+ live_rcmd_content = major.get("live_rcmd", {}).get("content", "{}")
215
+ live_data = json.loads(live_rcmd_content)
216
+ live_play_info = live_data.get("live_play_info", {})
217
+ watched_show = live_play_info.get("watched_show", {})
218
+ live_rcmd = {
219
+ "room_id": live_play_info.get("room_id", 0),
220
+ "live_status": live_play_info.get("live_status", 0),
221
+ "title": live_play_info.get("title", ""),
222
+ "cover": live_play_info.get("cover", ""),
223
+ "online": live_play_info.get("online", 0),
224
+ "area_id": live_play_info.get("area_id", 0),
225
+ "area_name": live_play_info.get("area_name", ""),
226
+ "parent_area_id": live_play_info.get("parent_area_id", 0),
227
+ "parent_area_name": live_play_info.get("parent_area_name", ""),
228
+ "live_start_time": live_play_info.get("live_start_time", 0),
229
+ "watched_num": watched_show.get("num"),
230
+ "switch": watched_show.get("switch"),
231
+ "text_small": watched_show.get("text_small"),
232
+ "text_large": watched_show.get("text_large"),
233
+ }
234
+
235
+ # 解析转发信息
236
+ forward_orig = None
237
+ if dynamic_type == "DYNAMIC_TYPE_FORWARD":
238
+ orig_data = data.get("orig")
239
+ if orig_data:
240
+ forward_orig = cls.from_raw(orig_data)
241
+
242
+ # 构造标准化字典后使用model_validate
243
+ normalized_data = {
244
+ "dynamic_id": dynamic_id,
245
+ "dynamic_type": dynamic_type,
246
+ "visible": visible,
247
+ "pub_ts": pub_ts,
248
+ "pub_time": pub_time,
249
+ "author": author,
250
+ "stat": stat,
251
+ "tag": tag,
252
+ "text": text,
253
+ "pics_url": pics_url,
254
+ "video": video,
255
+ "music": music,
256
+ "article": article,
257
+ "live_rcmd": live_rcmd,
258
+ "forward_orig": forward_orig,
259
+ }
260
+
261
+ return cls.model_validate(normalized_data)
262
+
263
+ except Exception as e:
264
+ _log.error("解析动态数据失败: %s", e, exc_info=True)
265
+ return None
266
+
267
+ @classmethod
268
+ def from_list(cls, data_dict: list[dict[Any, Any]]) -> "list[DynamicDTO | None]":
269
+ """
270
+ 从动态列表数据构造DTO对象列表
271
+ """
272
+ try:
273
+ return [cls.from_raw(data) for data in data_dict]
274
+ except Exception as e:
275
+ _log.error("解析动态数据列表失败: %s", e, exc_info=True)
276
+ return [None]
@@ -0,0 +1,169 @@
1
+ import re
2
+ from html import unescape
3
+ from logging import getLogger
4
+ from typing import Any, ClassVar
5
+
6
+ from butterbot.core.data import BaseDataModel
7
+
8
+ _log = getLogger("LiveRoomDTO")
9
+
10
+
11
+ def _html2_text(text: str) -> str:
12
+ """处理html以及换行符.
13
+
14
+ Args:
15
+ text (str): HTML内容
16
+
17
+ Returns:
18
+ text (str): 纯文本内容
19
+ """
20
+ # 将<br>标签替换为换行符
21
+ text = re.sub(r"<br\s*/?>", "\n", text)
22
+ # 去除其他HTML标签
23
+ text = re.sub(r"<[^>]*>", "", text)
24
+ # 处理HTML实体
25
+ text = unescape(text)
26
+ # 处理多余的空白字符,但保留单个换行符
27
+ text = re.sub(r"[ \t]+", " ", text) # 将多个空格或制表符合并为单个空格
28
+ text = re.sub(r" *\n *", "\n", text) # 去除换行符前后的空格
29
+ text = re.sub(r"\n+", "\n", text).strip() # 合并多个换行符并去除首尾空白
30
+ return text
31
+
32
+
33
+ class RoomInfoDto(BaseDataModel):
34
+ """直播间信息DTO"""
35
+
36
+ uid: int # 用户uid
37
+ room_id: int # 房间号
38
+ title: str # 直播间标题
39
+ cover_url: str # 直播间封面url
40
+ background_url: str # 直播间背景图url
41
+ description: str # 主播简介
42
+ tags: tuple[str, ...] # 直播间标签列表
43
+ live_status: int # 直播状态 0:未开播 1:直播中 2:轮播中
44
+ live_start_time: int # 直播开始时间戳
45
+ parent_area_name: str # 直播间父分区
46
+ parent_area_id: int # 直播间父分区ID
47
+ area_name: str # 直播间子分区
48
+ area_id: int # 直播间子分区ID
49
+ keyframe_url: str # 直播间关键帧url
50
+ online: int # 直播间当前在线人数
51
+
52
+
53
+ class AnchorInfoDto(BaseDataModel):
54
+ """主播信息DTO"""
55
+
56
+ name: str # 主播昵称
57
+ face_url: str # 主播头像url
58
+ gender: str # 主播性别
59
+ official_info: str # 主播官方信息(认证信息)
60
+ fanclub_name: str # 粉丝牌名称
61
+ fanclub_num: int # 粉丝团人数
62
+ live_level: int # 主播等级
63
+ live_score: int # 直播分数
64
+ live_upgrade_score: int # 升级所需分数
65
+
66
+
67
+ class WatchedShowDto(BaseDataModel):
68
+ """观看榜信息DTO"""
69
+
70
+ switch: bool # 观看榜开关
71
+ num: int # 观看人数/人气值
72
+ text_small: str # 小文本
73
+ text_large: str # 大文本
74
+
75
+
76
+ class NoticeBoardDto(BaseDataModel):
77
+ """公告栏信息DTO"""
78
+
79
+ content: str # 公告内容
80
+ ctime: str # 公告发布时间
81
+
82
+
83
+ class LiveRoomDTO(BaseDataModel):
84
+ """直播间数据DTO"""
85
+
86
+ discriminator_value: ClassVar[str] = "live_room" # 数据类型标识
87
+
88
+ room_info: RoomInfoDto # 直播间信息
89
+ anchor_info: AnchorInfoDto # 主播信息
90
+ watched_show: WatchedShowDto # 观看榜信息
91
+ notice_board: NoticeBoardDto | None = None # 公告栏信息
92
+
93
+ @classmethod
94
+ def from_raw(cls, data: dict[Any, Any]) -> "LiveRoomDTO | None":
95
+ """从原始API数据构造DTO对象"""
96
+ try:
97
+ # 直播间信息
98
+ room_info_data = data.get("room_info", {})
99
+ _tags: str = room_info_data.get("tags", "")
100
+ tags_tuple = tuple(_tags.split(",")) if _tags else ()
101
+
102
+ room_info = {
103
+ "uid": room_info_data.get("uid", 0),
104
+ "room_id": room_info_data.get("room_id", 0),
105
+ "title": room_info_data.get("title", ""),
106
+ "cover_url": room_info_data.get("cover", ""),
107
+ "background_url": room_info_data.get("background", ""),
108
+ "description": _html2_text(room_info_data.get("description", "")),
109
+ "tags": tags_tuple,
110
+ "live_status": room_info_data.get("live_status", 0),
111
+ "live_start_time": room_info_data.get("live_start_time", 0),
112
+ "parent_area_name": room_info_data.get("parent_area_name", ""),
113
+ "parent_area_id": room_info_data.get("parent_area_id", 0),
114
+ "area_name": room_info_data.get("area_name", ""),
115
+ "area_id": room_info_data.get("area_id", 0),
116
+ "keyframe_url": room_info_data.get("keyframe", ""),
117
+ "online": room_info_data.get("online", 0),
118
+ }
119
+
120
+ # 主播信息
121
+ anchor_info_data = data.get("anchor_info", {})
122
+ base_info = anchor_info_data.get("base_info", {})
123
+ medal_info = anchor_info_data.get("medal_info", {})
124
+ live_info = anchor_info_data.get("live_info", {})
125
+ official_info = base_info.get("official_info", {})
126
+
127
+ anchor_info = {
128
+ "name": base_info.get("uname", ""),
129
+ "face_url": base_info.get("face", ""),
130
+ "gender": base_info.get("gender", ""),
131
+ "official_info": official_info.get("title", ""),
132
+ "fanclub_name": medal_info.get("medal_name", ""),
133
+ "fanclub_num": medal_info.get("fansclub", 0),
134
+ "live_level": live_info.get("level", 0),
135
+ "live_score": live_info.get("score", 0),
136
+ "live_upgrade_score": live_info.get("upgrade_score", 0),
137
+ }
138
+
139
+ # 观看榜信息
140
+ watched_show_data = data.get("watched_show", {})
141
+ watched_show = {
142
+ "switch": watched_show_data.get("switch", False),
143
+ "num": watched_show_data.get("num", 0),
144
+ "text_small": watched_show_data.get("text_small", ""),
145
+ "text_large": watched_show_data.get("text_large", ""),
146
+ }
147
+
148
+ # 公告栏信息
149
+ notice_board = None
150
+ notice_board_data = data.get("news_info")
151
+ if notice_board_data:
152
+ notice_board = {
153
+ "content": notice_board_data.get("content", ""),
154
+ "ctime": notice_board_data.get("ctime", ""),
155
+ }
156
+
157
+ # 构造标准化字典后使用model_validate
158
+ normalized_data = {
159
+ "room_info": room_info,
160
+ "anchor_info": anchor_info,
161
+ "watched_show": watched_show,
162
+ "notice_board": notice_board,
163
+ }
164
+
165
+ return cls.model_validate(normalized_data)
166
+
167
+ except Exception as e:
168
+ _log.error("解析直播间数据失败: %s", e, exc_info=True)
169
+ return None
@@ -0,0 +1,18 @@
1
+ from typing import ClassVar
2
+
3
+ from butterbot.core.data import BaseDataModel
4
+
5
+
6
+ class VideoPartDto(BaseDataModel):
7
+ """视频分段数据DTO"""
8
+
9
+ discriminator_value: ClassVar[str] = "video_part" # 数据类型标识
10
+
11
+ start_timestamp: int # 开始时间戳
12
+ end_timestamp: int # 结束时间戳
13
+ content: str # 字幕内容
14
+
15
+ @classmethod
16
+ def from_list(cls, data: list) -> list["VideoPartDto"]:
17
+ """从列表构造DTO对象列表"""
18
+ return [cls.model_validate(i) for i in data]