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,135 @@
1
+ from dataclasses import dataclass
2
+
3
+ from butterbot.core.data import BaseDataMixin
4
+
5
+ from .dto import BlindGiftInfoDto, DanmakuGiftDTO, GiftMedalInfoDto
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class GiftMedalData(BaseDataMixin):
10
+ """
11
+ 送礼用户粉丝牌数据
12
+ """
13
+
14
+ level: int # 粉丝牌等级
15
+ name: str # 粉丝牌名称
16
+ anchor_room_id: int # 粉丝牌对应主播房间号
17
+ anchor_uname: str # 粉丝牌对应主播名称
18
+ guard_level: int # 舰长等级 (0=无, 1=总督, 2=提督, 3=舰长)
19
+ is_lighted: int # 粉丝牌是否点亮 (1=点亮, 0=未点亮)
20
+ anchor_uid: int # 粉丝牌对应主播UID
21
+
22
+ @classmethod
23
+ def from_dto(cls, medal: GiftMedalInfoDto) -> "GiftMedalData":
24
+ """从GiftMedalInfoDto构造GiftMedalData实例"""
25
+ return cls(
26
+ level=medal.level,
27
+ name=medal.name,
28
+ anchor_room_id=medal.anchor_room_id,
29
+ anchor_uname=medal.anchor_uname,
30
+ guard_level=medal.guard_level,
31
+ is_lighted=medal.is_lighted,
32
+ anchor_uid=medal.target_id,
33
+ )
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class BlindGiftData(BaseDataMixin):
38
+ """
39
+ 盲盒礼物数据(从盲盒中开出的礼物信息)
40
+ """
41
+
42
+ blind_gift_config_id: int # 盲盒配置ID
43
+ original_gift_id: int # 原始盲盒礼物ID
44
+ original_gift_name: str # 原始盲盒礼物名称
45
+ original_gift_price: int # 原始盲盒价格(电池)
46
+ gift_action: str # 礼物动作描述(如"爆出")
47
+ gift_tip_price: int # 开出礼物的价值(电池)
48
+
49
+ @classmethod
50
+ def from_dto(cls, blind_gift: BlindGiftInfoDto) -> "BlindGiftData":
51
+ """从BlindGiftInfoDto构造BlindGiftData实例"""
52
+ return cls(
53
+ blind_gift_config_id=blind_gift.blind_gift_config_id,
54
+ original_gift_id=blind_gift.original_gift_id,
55
+ original_gift_name=blind_gift.original_gift_name,
56
+ original_gift_price=blind_gift.original_gift_price,
57
+ gift_action=blind_gift.gift_action,
58
+ gift_tip_price=blind_gift.gift_tip_price,
59
+ )
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class DanmakuGiftData(BaseDataMixin):
64
+ """
65
+ 礼物消息数据
66
+ """
67
+
68
+ room_display_id: int # 房间号
69
+ room_real_id: int # 房间真实ID
70
+ gift_id: int # 礼物ID
71
+ gift_name: str # 礼物名称
72
+ gift_num: int # 礼物数量
73
+ price: int # 单个礼物价格(电池)
74
+ total_coin: int # 总价值(电池)
75
+ coin_type: str # 货币类型 (gold=电池)
76
+ action: str # 动作描述(如"投喂")
77
+ uid: int # 送礼用户UID
78
+ uname: str # 送礼用户名
79
+ face: str # 送礼用户头像URL
80
+ guard_level: int # 舰长等级 (0=无, 1=总督, 2=提督, 3=舰长)
81
+ wealth_level: int # 荣耀等级
82
+ receiver_uid: int # 收礼用户UID(主播)
83
+ receiver_uname: str # 收礼用户名(主播)
84
+ receiver_face: str # 收礼用户头像URL(主播)
85
+ receiver_official_title: str # 认证描述
86
+ medal: GiftMedalData | None # 粉丝牌信息
87
+ blind_gift: BlindGiftData | None # 盲盒礼物信息
88
+ timestamp: int # 发送时间戳
89
+ is_first: bool # 是否首次送礼
90
+ combo_total_coin: int # 连击总价值
91
+ gift_gif: str # 礼物动图URL
92
+ gift_img: str # 礼物图片URL
93
+
94
+ @classmethod
95
+ def from_dto(cls, dto: DanmakuGiftDTO) -> "DanmakuGiftData":
96
+ """从DTO对象构造DanmakuGiftData实例
97
+
98
+ Args:
99
+ dto: DanmakuGiftDTO对象
100
+
101
+ Returns:
102
+ DanmakuGiftData实例
103
+ """
104
+ medal_data = GiftMedalData.from_dto(dto.medal) if dto.medal else None
105
+ blind_gift_data = (
106
+ BlindGiftData.from_dto(dto.blind_gift) if dto.blind_gift else None
107
+ )
108
+
109
+ return cls(
110
+ room_display_id=dto.room_display_id,
111
+ room_real_id=dto.room_real_id,
112
+ gift_id=dto.gift_id,
113
+ gift_name=dto.gift_name,
114
+ gift_num=dto.gift_num,
115
+ price=dto.price,
116
+ total_coin=dto.total_coin,
117
+ coin_type=dto.coin_type,
118
+ action=dto.action,
119
+ uid=dto.sender.uid,
120
+ uname=dto.sender.uname,
121
+ face=dto.sender.face,
122
+ guard_level=dto.sender.guard_level,
123
+ wealth_level=dto.sender.wealth_level,
124
+ receiver_uid=dto.receiver.uid,
125
+ receiver_uname=dto.receiver.uname,
126
+ receiver_face=dto.receiver.face,
127
+ receiver_official_title=dto.receiver.official_title,
128
+ medal=medal_data,
129
+ blind_gift=blind_gift_data,
130
+ timestamp=dto.timestamp,
131
+ is_first=dto.is_first,
132
+ combo_total_coin=dto.combo_total_coin,
133
+ gift_gif=dto.gift_info.gif if dto.gift_info else "",
134
+ gift_img=dto.gift_info.img_basic if dto.gift_info else "",
135
+ )
@@ -0,0 +1,54 @@
1
+ from dataclasses import dataclass
2
+
3
+ from butterbot.core.data import BaseDataMixin
4
+
5
+ from .dto import DanmakuGuardDTO
6
+
7
+ # 舰长等级名称映射
8
+ GUARD_LEVEL_NAME = {
9
+ 1: "总督",
10
+ 2: "提督",
11
+ 3: "舰长",
12
+ }
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class DanmakuGuardData(BaseDataMixin):
17
+ """
18
+ 上舰事件数据
19
+ guard_level: 舰长等级 (1=总督, 2=提督, 3=舰长)
20
+ """
21
+
22
+ room_display_id: int # 房间号
23
+ room_real_id: int # 房间真实ID
24
+ uid: int # 用户UID
25
+ username: str # 用户名
26
+ guard_level: int # 舰长等级 (1=总督, 2=提督, 3=舰长)
27
+ guard_name: str # 舰长等级名称(如"舰长")
28
+ num: int # 购买数量
29
+ price: int # 价格(电池)
30
+ gift_id: int # 礼物ID
31
+ gift_name: str # 礼物名称(如"舰长")
32
+
33
+ @classmethod
34
+ def from_dto(cls, dto: DanmakuGuardDTO) -> "DanmakuGuardData":
35
+ """从DTO对象构造DanmakuGuardData实例
36
+
37
+ Args:
38
+ dto: DanmakuGuardDTO对象
39
+
40
+ Returns:
41
+ DanmakuGuardData实例
42
+ """
43
+ return cls(
44
+ room_display_id=dto.room_display_id,
45
+ room_real_id=dto.room_real_id,
46
+ uid=dto.uid,
47
+ username=dto.username,
48
+ guard_level=dto.guard_level,
49
+ guard_name=GUARD_LEVEL_NAME.get(dto.guard_level, "未知类型"),
50
+ num=dto.num,
51
+ price=dto.price,
52
+ gift_id=dto.gift_id,
53
+ gift_name=dto.gift_name,
54
+ )
@@ -0,0 +1,71 @@
1
+ from dataclasses import dataclass
2
+
3
+ from butterbot.core.data import BaseDataMixin
4
+
5
+ from .dto import DanmakuMsgDTO, MedalInfoDto
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class MedalData(BaseDataMixin):
10
+ """
11
+ 粉丝牌数据
12
+ """
13
+
14
+ level: int # 粉丝牌等级
15
+ name: str # 粉丝牌名称
16
+ anchor_name: str # 粉丝牌对应主播名
17
+ room_id: int # 粉丝牌对应主播房间号
18
+ is_light: int # 粉丝牌是否点亮 (1=点亮, 0=未点亮)
19
+ anchor_uid: int # 粉丝牌对应主播UID
20
+
21
+ @classmethod
22
+ def from_dto(cls, medal: MedalInfoDto) -> "MedalData":
23
+ """从MedalInfo DTO构造MedalData实例"""
24
+ return cls(
25
+ level=medal.level,
26
+ name=medal.name,
27
+ anchor_name=medal.anchor_name,
28
+ room_id=medal.room_id,
29
+ is_light=medal.is_light,
30
+ anchor_uid=medal.anchor_uid,
31
+ )
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class DanmakuMsgData(BaseDataMixin):
36
+ """
37
+ 弹幕消息数据
38
+ """
39
+
40
+ room_display_id: int # 房间号
41
+ room_real_id: int # 房间真实ID
42
+ message: str # 弹幕内容
43
+ uid: int # 用户UID
44
+ username: str # 用户名
45
+ user_level: int # 用户等级
46
+ face: str | None # 头像URL
47
+ medal: MedalData | None # 粉丝牌信息
48
+ timestamp: int # 发送时间戳
49
+
50
+ @classmethod
51
+ def from_dto(cls, dto: DanmakuMsgDTO) -> "DanmakuMsgData":
52
+ """从DTO对象构造DanmakuMsgData实例
53
+
54
+ Args:
55
+ dto: DanmakuMsgDTO对象
56
+
57
+ Returns:
58
+ DanmakuMsgData实例
59
+ """
60
+ medal_data = MedalData.from_dto(dto.medal) if dto.medal else None
61
+ return cls(
62
+ room_display_id=dto.room_display_id,
63
+ room_real_id=dto.room_real_id,
64
+ message=dto.message,
65
+ uid=dto.user.uid,
66
+ username=dto.user.username,
67
+ user_level=dto.user.user_level,
68
+ face=dto.user.face,
69
+ medal=medal_data,
70
+ timestamp=dto.timestamp,
71
+ )
@@ -0,0 +1,59 @@
1
+ from .danmaku_gift_dto import (
2
+ BlindGiftInfoDto,
3
+ DanmakuGiftDTO,
4
+ GiftInfoDto,
5
+ GiftMedalInfoDto,
6
+ GiftReceiverDto,
7
+ GiftSenderDto,
8
+ )
9
+ from .danmaku_guard_buy_dto import (
10
+ DanmakuGuardDTO,
11
+ )
12
+ from .danmaku_msg_dto import (
13
+ DanmakuMsgDTO,
14
+ MedalInfoDto,
15
+ UserInfoDto,
16
+ )
17
+ from .dynamic_dto import (
18
+ ArticleDto,
19
+ AuthorDto,
20
+ DynamicDTO,
21
+ LiveRcmdDto,
22
+ MusicDto,
23
+ StatDto,
24
+ VideoDto,
25
+ )
26
+ from .live_room_dto import (
27
+ AnchorInfoDto,
28
+ LiveRoomDTO,
29
+ NoticeBoardDto,
30
+ RoomInfoDto,
31
+ WatchedShowDto,
32
+ )
33
+ from .video_part_dto import VideoPartDto
34
+
35
+ __all__ = [
36
+ "AnchorInfoDto",
37
+ "ArticleDto",
38
+ "AuthorDto",
39
+ "BlindGiftInfoDto",
40
+ "DanmakuGiftDTO",
41
+ "DanmakuGuardDTO",
42
+ "DanmakuMsgDTO",
43
+ "DynamicDTO",
44
+ "GiftInfoDto",
45
+ "GiftMedalInfoDto",
46
+ "GiftReceiverDto",
47
+ "GiftSenderDto",
48
+ "LiveRcmdDto",
49
+ "LiveRoomDTO",
50
+ "MedalInfoDto",
51
+ "MusicDto",
52
+ "NoticeBoardDto",
53
+ "RoomInfoDto",
54
+ "StatDto",
55
+ "UserInfoDto",
56
+ "VideoDto",
57
+ "VideoPartDto",
58
+ "WatchedShowDto",
59
+ ]
@@ -0,0 +1,193 @@
1
+ from logging import getLogger
2
+ from typing import ClassVar
3
+
4
+ from butterbot.core.data import BaseDataModel
5
+
6
+ _log = getLogger("DanmakuGiftDTO")
7
+
8
+
9
+ class BlindGiftInfoDto(BaseDataModel):
10
+ """
11
+ 盲盒礼物信息(从盲盒中开出的礼物)
12
+ """
13
+
14
+ blind_gift_config_id: int # 盲盒配置ID
15
+ original_gift_id: int # 原始盲盒礼物ID
16
+ original_gift_name: str # 原始盲盒礼物名称
17
+ original_gift_price: int # 原始盲盒价格(电池)
18
+ gift_action: str # 礼物动作描述(如"爆出")
19
+ gift_tip_price: int # 开出礼物的价值(电池)
20
+
21
+
22
+ class GiftMedalInfoDto(BaseDataModel):
23
+ """
24
+ 送礼用户的粉丝牌信息
25
+ """
26
+
27
+ level: int # 勋章等级
28
+ name: str # 勋章名称
29
+ anchor_room_id: int # 主播房间号
30
+ anchor_uname: str # 主播名称(可能为空)
31
+ guard_level: int # 舰长等级 (0=无, 1=总督, 2=提督, 3=舰长)
32
+ is_lighted: int # 是否点亮 (1=点亮, 0=未点亮)
33
+ target_id: int # 勋章对应主播UID
34
+
35
+
36
+ class GiftInfoDto(BaseDataModel):
37
+ """
38
+ 礼物资源信息
39
+ """
40
+
41
+ img_basic: str # 礼物基础图片URL
42
+ gif: str # 礼物动图URL
43
+ webp: str # 礼物webp图片URL
44
+
45
+
46
+ class GiftSenderDto(BaseDataModel):
47
+ """
48
+ 送礼用户信息
49
+ """
50
+
51
+ uid: int # 用户UID
52
+ uname: str # 用户名
53
+ face: str # 头像URL
54
+ guard_level: int # 舰长等级 (0=无, 1=总督, 2=提督, 3=舰长)
55
+ wealth_level: int # 荣耀等级
56
+
57
+
58
+ class GiftReceiverDto(BaseDataModel):
59
+ """
60
+ 收礼用户信息(主播)
61
+ """
62
+
63
+ uid: int # 用户UID
64
+ uname: str # 用户名
65
+ face: str # 头像URL
66
+ official_title: str # 认证描述
67
+
68
+
69
+ class DanmakuGiftDTO(BaseDataModel):
70
+ """
71
+ 礼物消息DTO
72
+ """
73
+
74
+ discriminator_value: ClassVar[str] = "danmaku_gift" # 数据类型标识
75
+
76
+ room_display_id: int # 房间号
77
+ room_real_id: int # 房间真实ID
78
+ gift_id: int # 礼物ID
79
+ gift_name: str # 礼物名称
80
+ gift_num: int # 礼物数量
81
+ price: int # 单个礼物价格(电池)
82
+ total_coin: int # 总价值(电池)
83
+ coin_type: str # 货币类型 (gold=电池, silver=银瓜子)
84
+ action: str # 动作描述(如"投喂")
85
+ sender: GiftSenderDto # 送礼用户信息
86
+ receiver: GiftReceiverDto # 收礼用户信息(主播)
87
+ medal: GiftMedalInfoDto | None = None # 粉丝牌信息
88
+ gift_info: GiftInfoDto | None = None # 礼物资源信息
89
+ blind_gift: BlindGiftInfoDto | None = None # 盲盒礼物信息
90
+ timestamp: int = 0 # 发送时间戳
91
+ is_first: bool = False # 是否首次送礼 (单次送礼, 无连击)
92
+ combo_total_coin: int = 0 # 连击总价值
93
+
94
+ @classmethod
95
+ def from_raw(cls, data: dict) -> "DanmakuGiftDTO | None":
96
+ """
97
+ 从原始API数据构造DTO对象
98
+ """
99
+ try:
100
+ room_display_id = data.get("room_display_id", 0)
101
+ room_real_id = data.get("room_real_id", 0)
102
+
103
+ # 获取内层data
104
+ gift_data = data.get("data", {}).get("data", {})
105
+
106
+ # 送礼用户信息
107
+ sender = {
108
+ "uid": gift_data.get("uid", 0),
109
+ "uname": gift_data.get("uname", ""),
110
+ "face": gift_data.get("face", ""),
111
+ "guard_level": gift_data.get("guard_level", 0),
112
+ "wealth_level": gift_data.get("wealth_level", 0),
113
+ }
114
+
115
+ # 收礼用户信息(主播)
116
+ receiver_uinfo = gift_data.get("receiver_uinfo", {})
117
+ receiver_base = receiver_uinfo.get("base", {})
118
+ receiver = {
119
+ "uid": receiver_uinfo.get("uid", 0),
120
+ "uname": receiver_base.get("name", ""),
121
+ "face": receiver_base.get("face", ""),
122
+ "official_title": receiver_base.get("official_info", {}).get(
123
+ "title", ""
124
+ ),
125
+ }
126
+
127
+ # 粉丝牌信息
128
+ medal = None
129
+ medal_info = gift_data.get("medal_info", {})
130
+ if medal_info:
131
+ medal = {
132
+ "level": medal_info.get("medal_level", 0),
133
+ "name": medal_info.get("medal_name", ""),
134
+ "anchor_room_id": medal_info.get("anchor_roomid", 0),
135
+ "anchor_uname": medal_info.get("anchor_uname", ""),
136
+ "guard_level": medal_info.get("guard_level", 0),
137
+ "is_lighted": medal_info.get("is_lighted", 0),
138
+ "target_id": medal_info.get("target_id", 0),
139
+ }
140
+
141
+ # 礼物资源信息
142
+ gift_info = None
143
+ gift_info_data = gift_data.get("gift_info", {})
144
+ if gift_info_data:
145
+ gift_info = {
146
+ "img_basic": gift_info_data.get("img_basic", ""),
147
+ "gif": gift_info_data.get("gif", ""),
148
+ "webp": gift_info_data.get("webp", ""),
149
+ }
150
+
151
+ # 盲盒礼物信息
152
+ blind_gift = None
153
+ blind_gift_data = gift_data.get("blind_gift")
154
+ if blind_gift_data:
155
+ blind_gift = {
156
+ "blind_gift_config_id": blind_gift_data.get(
157
+ "blind_gift_config_id", 0
158
+ ),
159
+ "original_gift_id": blind_gift_data.get("original_gift_id", 0),
160
+ "original_gift_name": blind_gift_data.get("original_gift_name", ""),
161
+ "original_gift_price": blind_gift_data.get(
162
+ "original_gift_price", 0
163
+ ),
164
+ "gift_action": blind_gift_data.get("gift_action", ""),
165
+ "gift_tip_price": blind_gift_data.get("gift_tip_price", 0),
166
+ }
167
+
168
+ # 构造标准化字典后使用model_validate
169
+ normalized_data = {
170
+ "room_display_id": room_display_id,
171
+ "room_real_id": room_real_id,
172
+ "gift_id": gift_data.get("giftId", 0),
173
+ "gift_name": gift_data.get("giftName", ""),
174
+ "gift_num": gift_data.get("num", 1),
175
+ "price": gift_data.get("price", 0),
176
+ "total_coin": gift_data.get("total_coin", 0),
177
+ "coin_type": gift_data.get("coin_type", "gold"),
178
+ "action": gift_data.get("action", ""),
179
+ "sender": sender,
180
+ "receiver": receiver,
181
+ "medal": medal,
182
+ "gift_info": gift_info,
183
+ "blind_gift": blind_gift,
184
+ "timestamp": gift_data.get("timestamp", 0),
185
+ "is_first": gift_data.get("is_first", False),
186
+ "combo_total_coin": gift_data.get("combo_total_coin", 0),
187
+ }
188
+
189
+ return cls.model_validate(normalized_data)
190
+
191
+ except Exception as e:
192
+ _log.error("解析礼物消息失败: %s", e)
193
+ return None
@@ -0,0 +1,54 @@
1
+ from logging import getLogger
2
+ from typing import ClassVar
3
+
4
+ from butterbot.core.data import BaseDataModel
5
+
6
+ _log = getLogger("DanmakuGuardDTO")
7
+
8
+
9
+ class DanmakuGuardDTO(BaseDataModel):
10
+ """
11
+ 舰长事件 DTO
12
+ guard_level: 舰长等级 (1=总督, 2=提督, 3=舰长)
13
+ """
14
+
15
+ discriminator_value: ClassVar[str] = "guard" # 数据类型标识
16
+
17
+ room_display_id: int # 房间号
18
+ room_real_id: int # 房间真实ID
19
+ uid: int # 用户UID
20
+ username: str # 用户名
21
+ guard_level: int # 舰长等级 (1=总督, 2=提督, 3=舰长)
22
+ num: int # 购买数量
23
+ price: int # 价格(电池)
24
+ gift_id: int # 礼物ID
25
+ gift_name: str # 礼物名称(如"舰长")
26
+
27
+ @classmethod
28
+ def from_raw(cls, data: dict) -> "DanmakuGuardDTO | None":
29
+ """
30
+ 从原始API数据构造DTO对象
31
+ """
32
+ try:
33
+ room_display_id = data.get("room_display_id", 0)
34
+ room_real_id = data.get("room_real_id", 0)
35
+
36
+ guard_data = data.get("data", {}).get("data", {})
37
+
38
+ normalized_data = {
39
+ "room_display_id": room_display_id,
40
+ "room_real_id": room_real_id,
41
+ "uid": guard_data.get("uid", 0),
42
+ "username": guard_data.get("username", ""),
43
+ "guard_level": guard_data.get("guard_level", 3),
44
+ "num": guard_data.get("num", 1),
45
+ "price": guard_data.get("price", 0),
46
+ "gift_id": guard_data.get("gift_id", 0),
47
+ "gift_name": guard_data.get("gift_name", ""),
48
+ }
49
+
50
+ return cls.model_validate(normalized_data)
51
+
52
+ except Exception as e:
53
+ _log.error("解析上舰数据失败: %s", e, exc_info=True)
54
+ return None
@@ -0,0 +1,123 @@
1
+ from logging import getLogger
2
+ from typing import ClassVar
3
+
4
+ from butterbot.core.data import BaseDataModel
5
+
6
+ _log = getLogger("DanmakuMsgDTO")
7
+
8
+
9
+ class MedalInfoDto(BaseDataModel):
10
+ """
11
+ 粉丝牌信息
12
+ Tips:
13
+ 如果当前直播间或者用户开启了"优先展示当前直播间的粉丝勋章
14
+ 那么在该直播间无法获取到用户佩戴的其他主播的勋章信息"
15
+ """
16
+
17
+ level: int # 勋章等级
18
+ name: str # 勋章名称
19
+ anchor_name: str # 主播名称
20
+ room_id: int # 主播房间号
21
+ is_light: int # 是否点亮 (1=点亮, 0=未点亮)
22
+ anchor_uid: int # 主播UID
23
+
24
+
25
+ class UserInfoDto(BaseDataModel):
26
+ """
27
+ 用户基本信息
28
+ """
29
+
30
+ uid: int # 用户UID
31
+ username: str # 用户名
32
+ user_level: int # 用户等级
33
+ face: str | None = None # 头像URL
34
+
35
+
36
+ class DanmakuMsgDTO(BaseDataModel):
37
+ """
38
+ 弹幕消息DTO
39
+ """
40
+
41
+ discriminator_value: ClassVar[str] = "danmaku_msg" # 数据类型标识
42
+
43
+ room_display_id: int # 房间号
44
+ room_real_id: int # 房间真实ID
45
+ message: str # 弹幕内容
46
+ user: UserInfoDto # 用户信息
47
+ medal: MedalInfoDto | None = None # 粉丝牌信息
48
+ timestamp: int = 0 # 发送时间戳
49
+
50
+ @classmethod
51
+ def from_raw(cls, data: dict) -> "DanmakuMsgDTO | None":
52
+ """
53
+ 从原始API数据构造DTO对象
54
+ 注意: 由于弹幕数据格式特殊(数组形式), 需要先转换为字典格式
55
+ """
56
+ try:
57
+ room_display_id = data.get("room_display_id", 0)
58
+ room_real_id = data.get("room_real_id", 0)
59
+
60
+ # 获取info数组
61
+ info = data.get("data", {}).get("info", [])
62
+
63
+ # 提取弹幕内容 info[1]
64
+ message = info[1] if len(info) > 1 else ""
65
+
66
+ # 提取用户基本信息 info[2]: [uid, username, admin, vip, svip, ...]
67
+ user_basic = info[2] if len(info) > 2 else []
68
+ uid = user_basic[0] if len(user_basic) > 0 else 0
69
+ username = user_basic[1] if len(user_basic) > 1 else ""
70
+
71
+ # 提取用户等级信息 info[4]: [level, ...]
72
+ level_info = info[4] if len(info) > 4 else []
73
+ user_level = level_info[0] if len(level_info) > 0 else 0
74
+
75
+ # 提取头像 info[0][15].user.base
76
+ face = None
77
+ info_detail = info[0] if len(info) > 0 else []
78
+ if isinstance(info_detail, list) and len(info_detail) > 15:
79
+ user_detail_obj = info_detail[15]
80
+ if isinstance(user_detail_obj, dict):
81
+ user_detail = user_detail_obj.get("user", {})
82
+ if user_detail:
83
+ base_info = user_detail.get("base", {})
84
+ face = base_info.get("face")
85
+
86
+ # 提取粉丝牌信息 info[3]: [level, name, anchor_name, room_id, color, ...]
87
+ medal_info = None
88
+ medal_data = info[3] if len(info) > 3 else []
89
+ if medal_data and len(medal_data) > 0:
90
+ medal_info = {
91
+ "level": medal_data[0] if len(medal_data) > 0 else 0,
92
+ "name": medal_data[1] if len(medal_data) > 1 else "",
93
+ "anchor_name": medal_data[2] if len(medal_data) > 2 else "",
94
+ "room_id": medal_data[3] if len(medal_data) > 3 else 0,
95
+ "is_light": medal_data[11] if len(medal_data) > 11 else 0,
96
+ "anchor_uid": medal_data[12] if len(medal_data) > 12 else 0,
97
+ }
98
+
99
+ # 提取时间戳 info[9].ts
100
+ timestamp = 0
101
+ if len(info) > 9 and isinstance(info[9], dict):
102
+ timestamp = info[9].get("ts", 0)
103
+
104
+ # 构造标准化字典后使用model_validate
105
+ normalized_data = {
106
+ "room_display_id": room_display_id,
107
+ "room_real_id": room_real_id,
108
+ "message": message,
109
+ "user": {
110
+ "uid": uid,
111
+ "username": username,
112
+ "user_level": user_level,
113
+ "face": face,
114
+ },
115
+ "medal": medal_info,
116
+ "timestamp": timestamp,
117
+ }
118
+
119
+ return cls.model_validate(normalized_data)
120
+
121
+ except Exception as e:
122
+ _log.error("解析弹幕数据失败: %s", e, exc_info=True)
123
+ return None