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.
- butterbot/__init__.py +15 -0
- butterbot/app/__init__.py +41 -0
- butterbot/app/bot_app.py +374 -0
- butterbot/app/config.py +156 -0
- butterbot/app/source_manager.py +367 -0
- butterbot/core/__init__.py +46 -0
- butterbot/core/api/__init__.py +9 -0
- butterbot/core/api/base_api.py +32 -0
- butterbot/core/context/README.md +1 -0
- butterbot/core/context/__init__.py +9 -0
- butterbot/core/context/api_registry.py +109 -0
- butterbot/core/context/app_context.py +51 -0
- butterbot/core/context/config_provider.py +14 -0
- butterbot/core/data/__init__.py +15 -0
- butterbot/core/data/base_data.py +29 -0
- butterbot/core/data/base_model.py +197 -0
- butterbot/core/event/README.md +1 -0
- butterbot/core/event/__init__.py +10 -0
- butterbot/core/event/event.py +27 -0
- butterbot/core/event/event_bus.py +276 -0
- butterbot/core/event/subscriber.py +130 -0
- butterbot/core/exceptions.py +90 -0
- butterbot/core/filter/__init__.py +11 -0
- butterbot/core/filter/base_filter.py +74 -0
- butterbot/core/source/README.md +1 -0
- butterbot/core/source/__init__.py +9 -0
- butterbot/core/source/base_source.py +134 -0
- butterbot/core/types/__init__.py +9 -0
- butterbot/core/types/base_type.py +74 -0
- butterbot/sources/__init__.py +1 -0
- butterbot/sources/bilibili/README.md +10 -0
- butterbot/sources/bilibili/__init__.py +20 -0
- butterbot/sources/bilibili/api/__init__.py +5 -0
- butterbot/sources/bilibili/api/bili_api.py +123 -0
- butterbot/sources/bilibili/data/__init__.py +52 -0
- butterbot/sources/bilibili/data/danmaku_gift_data.py +135 -0
- butterbot/sources/bilibili/data/danmaku_guard_data.py +54 -0
- butterbot/sources/bilibili/data/danmaku_msg_data.py +71 -0
- butterbot/sources/bilibili/data/dto/__init__.py +59 -0
- butterbot/sources/bilibili/data/dto/danmaku_gift_dto.py +193 -0
- butterbot/sources/bilibili/data/dto/danmaku_guard_buy_dto.py +54 -0
- butterbot/sources/bilibili/data/dto/danmaku_msg_dto.py +123 -0
- butterbot/sources/bilibili/data/dto/dynamic_dto.py +276 -0
- butterbot/sources/bilibili/data/dto/live_room_dto.py +169 -0
- butterbot/sources/bilibili/data/dto/video_part_dto.py +18 -0
- butterbot/sources/bilibili/data/dynamic_data.py +362 -0
- butterbot/sources/bilibili/data/live_room_data.py +162 -0
- butterbot/sources/bilibili/data/video_part.py +46 -0
- butterbot/sources/bilibili/source/__init__.py +15 -0
- butterbot/sources/bilibili/source/base_polling_source.py +130 -0
- butterbot/sources/bilibili/source/bili_danmaku_source.py +230 -0
- butterbot/sources/bilibili/source/bili_dynamic_source.py +135 -0
- butterbot/sources/bilibili/source/bili_live_source.py +137 -0
- butterbot/sources/bilibili/types/__init__.py +11 -0
- butterbot/sources/bilibili/types/bili_type.py +32 -0
- butterbot/sources/napcat/README.md +10 -0
- butterbot/sources/napcat/__init__.py +20 -0
- butterbot/sources/napcat/api/__init__.py +3 -0
- butterbot/sources/napcat/api/napcat_api.py +316 -0
- butterbot/sources/napcat/data/__init__.py +179 -0
- butterbot/sources/napcat/data/event_data.py +441 -0
- butterbot/sources/napcat/data/segment_data.py +432 -0
- butterbot/sources/napcat/events.py +91 -0
- butterbot/sources/napcat/filters/__init__.py +19 -0
- butterbot/sources/napcat/filters/filters.py +202 -0
- butterbot/sources/napcat/source/__init__.py +5 -0
- butterbot/sources/napcat/source/napcat_source.py +58 -0
- butterbot/sources/napcat/types/__init__.py +5 -0
- butterbot/sources/napcat/types/napcat_type.py +61 -0
- butterbot/utils/README.md +15 -0
- butterbot/utils/__init__.py +11 -0
- butterbot/utils/data_pair.py +27 -0
- butterbot/utils/logging_config.py +521 -0
- butterbot/utils/terminal.py +308 -0
- butterbot/utils/websocket.py +1270 -0
- butterbot_python-3.1.0.dev1.dist-info/METADATA +769 -0
- butterbot_python-3.1.0.dev1.dist-info/RECORD +79 -0
- butterbot_python-3.1.0.dev1.dist-info/WHEEL +4 -0
- butterbot_python-3.1.0.dev1.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""框架异常层级.
|
|
2
|
+
|
|
3
|
+
框架主动抛出的异常都继承 :class:`ButterError`,用户代码既可以用一个
|
|
4
|
+
``except ButterError`` 兜住框架内部的全部错误,也可以按子类精确区分错误来源::
|
|
5
|
+
|
|
6
|
+
from butterbot.app import ButterError, ConfigError
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
app = BotApp()
|
|
10
|
+
app.run()
|
|
11
|
+
except ConfigError as e:
|
|
12
|
+
print("配置有问题:", e)
|
|
13
|
+
except ButterError as e:
|
|
14
|
+
print("框架错误:", e)
|
|
15
|
+
|
|
16
|
+
部分异常同时继承内置异常(如 :class:`ConfigError` 继承 ``ValueError``),
|
|
17
|
+
这样既能被新的类型精确捕获,也不会让原本 ``except ValueError`` 的调用方漏掉。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ButterError(Exception):
|
|
24
|
+
"""butterbot 所有框架异常的基类."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConfigError(ButterError, ValueError):
|
|
28
|
+
"""配置错误.
|
|
29
|
+
|
|
30
|
+
触发场景:配置文件格式非法、配置项构建失败、
|
|
31
|
+
以及事件源/API 所需的配置键缺失。
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LifecycleError(ButterError, RuntimeError):
|
|
36
|
+
"""生命周期状态错误.
|
|
37
|
+
|
|
38
|
+
触发场景:在已关闭的 ``SourceManager`` 上添加事件源、
|
|
39
|
+
在已关闭的 ``EventBus`` 上发布事件等。
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SourceError(ButterError):
|
|
44
|
+
"""事件源相关错误的基类."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SourceStartError(SourceError):
|
|
48
|
+
"""一个或多个事件源启动失败.
|
|
49
|
+
|
|
50
|
+
由 ``SourceManager.start()`` 在批量启动结束后抛出。抛出前所有已成功启动的
|
|
51
|
+
事件源都已被回滚(stop),因此捕获到该异常时应用处于"未启动"状态。
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
failures: ``{事件源描述: 原始异常}``,保留每个失败源的真实异常对象
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, failures: Mapping[str, BaseException]) -> None:
|
|
58
|
+
"""初始化.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
failures: 事件源描述到原始异常的映射
|
|
62
|
+
"""
|
|
63
|
+
self.failures: dict[str, BaseException] = dict(failures)
|
|
64
|
+
detail = "; ".join(
|
|
65
|
+
"%s: %s" % (name, exc) for name, exc in self.failures.items()
|
|
66
|
+
)
|
|
67
|
+
super().__init__("以下事件源启动失败: %s" % detail)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ApiError(ButterError):
|
|
71
|
+
"""API 构建或调用错误."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SubscriptionError(ButterError, ValueError):
|
|
75
|
+
"""订阅注册错误.
|
|
76
|
+
|
|
77
|
+
触发场景:订阅规则在事件源声明的 ``supported_types`` 中无任何匹配的具体状态
|
|
78
|
+
—— 这种订阅永远不会被触发,绝大多数情况下是状态值或正则写错了。
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"ApiError",
|
|
84
|
+
"ButterError",
|
|
85
|
+
"ConfigError",
|
|
86
|
+
"LifecycleError",
|
|
87
|
+
"SourceError",
|
|
88
|
+
"SourceStartError",
|
|
89
|
+
"SubscriptionError",
|
|
90
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from butterbot.core.event import Event
|
|
7
|
+
|
|
8
|
+
_log = getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseFilter(ABC):
|
|
12
|
+
"""过滤器基类."""
|
|
13
|
+
|
|
14
|
+
filters: Any
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def check(self, event: "Event") -> bool:
|
|
18
|
+
"""检查事件是否通过过滤器
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
event: 消息事件
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
bool: True 表示通过过滤器,False 表示被拦截
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def __repr__(self) -> str:
|
|
29
|
+
return "<%s filters=%s>" % (self.__class__.__name__, self.filters)
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return self.__repr__()
|
|
33
|
+
|
|
34
|
+
def __and__(self, other: "BaseFilter") -> "BaseFilter":
|
|
35
|
+
"""使用 & 合并过滤器, 并支持链式调用, 处理顺序为传入顺序."""
|
|
36
|
+
return AndFilter(self, other)
|
|
37
|
+
|
|
38
|
+
def __or__(self, other: "BaseFilter") -> "BaseFilter":
|
|
39
|
+
"""使用 | 合并过滤器, 并支持链式调用, 处理顺序为传入顺序."""
|
|
40
|
+
return OrFilter(self, other)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AndFilter(BaseFilter):
|
|
44
|
+
"""与过滤器."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, *filters: BaseFilter):
|
|
47
|
+
self.filters = list(filters)
|
|
48
|
+
|
|
49
|
+
def check(self, event: "Event") -> bool:
|
|
50
|
+
"""检查事件是否通过所有过滤器."""
|
|
51
|
+
for f in self.filters:
|
|
52
|
+
if not f.check(event):
|
|
53
|
+
_log.debug(
|
|
54
|
+
"事件%s被过滤器参数 %s 拦截, 不再继续检查", event.id, f.filters
|
|
55
|
+
)
|
|
56
|
+
return False
|
|
57
|
+
_log.debug("事件%s通过所有%s与过滤器", event.id, self)
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class OrFilter(BaseFilter):
|
|
62
|
+
"""或过滤器."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, *filters: BaseFilter):
|
|
65
|
+
self.filters = list(filters)
|
|
66
|
+
|
|
67
|
+
def check(self, event: "Event") -> bool:
|
|
68
|
+
"""检查事件是否通过任一过滤器."""
|
|
69
|
+
for f in self.filters:
|
|
70
|
+
if f.check(event):
|
|
71
|
+
_log.debug("事件%s通过过滤器参数 %s, 不再继续检查", event.id, f.filters)
|
|
72
|
+
return True
|
|
73
|
+
_log.debug("事件%s未通过%s或过滤器", event.id, self)
|
|
74
|
+
return False
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--- 待施工 ---
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import TYPE_CHECKING, ClassVar, TypeVar
|
|
3
|
+
from uuid import UUID, uuid4
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from butterbot.core.context import AppContext
|
|
7
|
+
from butterbot.core.types import BaseType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseSource(ABC):
|
|
11
|
+
"""事件源基类.
|
|
12
|
+
|
|
13
|
+
Source 负责:
|
|
14
|
+
- 产生事件数据
|
|
15
|
+
- 通过 EventBus 发布事件
|
|
16
|
+
- 管理自己的生命周期(start/stop)
|
|
17
|
+
|
|
18
|
+
子类只需实现 :meth:`on_start` 和 :meth:`on_stop`,
|
|
19
|
+
无需手动管理 ``self.running`` 状态——
|
|
20
|
+
:meth:`start` 和 :meth:`stop` 已自动处理。
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
uuid: 唯一标识符,由 SourceManager 内部管理
|
|
24
|
+
running: 运行状态(由 start/stop 自动管理,子类不应直接修改)
|
|
25
|
+
config_key: 配置键,子类可覆盖此类属性作为默认值
|
|
26
|
+
supported_types: 事件源支持的 ``BaseType`` 枚举类,用于订阅规则编译
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
config_key: str = ""
|
|
30
|
+
|
|
31
|
+
# 事件源支持的 BaseType 枚举类。子类必须覆盖此属性,
|
|
32
|
+
# 例如 supported_types = DynamicType。
|
|
33
|
+
# 未声明时订阅将抛出 TypeError。
|
|
34
|
+
supported_types: ClassVar[type["BaseType"] | None] = None
|
|
35
|
+
|
|
36
|
+
def __init_subclass__(cls) -> None:
|
|
37
|
+
"""子类初始化检查."""
|
|
38
|
+
super().__init_subclass__()
|
|
39
|
+
|
|
40
|
+
if cls.__name__ == "BaseSource":
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
if not hasattr(cls, "supported_types"):
|
|
44
|
+
raise TypeError(f"{cls.__name__} must define supported_types")
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
uuid: UUID | None = None,
|
|
49
|
+
*,
|
|
50
|
+
config_key: str | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""初始化事件源.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
uuid: 可选,指定 UUID,默认自动生成
|
|
56
|
+
config_key: 可选,覆盖类级默认配置键
|
|
57
|
+
"""
|
|
58
|
+
self.uuid: UUID = uuid or uuid4()
|
|
59
|
+
self.running: bool = False
|
|
60
|
+
self._ctx: "AppContext | None" = None
|
|
61
|
+
if config_key is not None:
|
|
62
|
+
self.config_key = config_key
|
|
63
|
+
|
|
64
|
+
async def start(self) -> None:
|
|
65
|
+
"""启动事件源(模板方法).
|
|
66
|
+
|
|
67
|
+
自动管理 ``running`` 状态,然后委托给 :meth:`on_start`。
|
|
68
|
+
子类不应重写此方法,应实现 :meth:`on_start`。
|
|
69
|
+
|
|
70
|
+
:meth:`on_start` 抛出异常(含 ``CancelledError``)时,
|
|
71
|
+
``running`` 会回滚为 ``False`` 后再向上传播——否则启动失败的事件源
|
|
72
|
+
会以"运行中"的假象留在应用里,而它的资源其实从未建立起来。
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
Exception: :meth:`on_start` 抛出的任何异常,原样向上传播
|
|
76
|
+
"""
|
|
77
|
+
if self.running:
|
|
78
|
+
return
|
|
79
|
+
self.running = True
|
|
80
|
+
try:
|
|
81
|
+
await self.on_start()
|
|
82
|
+
except BaseException:
|
|
83
|
+
self.running = False
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
async def stop(self) -> None:
|
|
87
|
+
"""停止事件源(模板方法).
|
|
88
|
+
|
|
89
|
+
自动管理 ``running`` 状态,然后委托给 :meth:`on_stop`。
|
|
90
|
+
子类不应重写此方法,应实现 :meth:`on_stop`。
|
|
91
|
+
"""
|
|
92
|
+
if not self.running:
|
|
93
|
+
return
|
|
94
|
+
self.running = False
|
|
95
|
+
await self.on_stop()
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
async def on_start(self) -> None:
|
|
99
|
+
"""子类实现:事件源启动逻辑.
|
|
100
|
+
|
|
101
|
+
在 :meth:`start` 中被调用,此时 ``self.running`` 已为 ``True``。
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
async def on_stop(self) -> None:
|
|
106
|
+
"""子类实现:事件源停止逻辑.
|
|
107
|
+
|
|
108
|
+
在 :meth:`stop` 中被调用,此时 ``self.running`` 已为 ``False``。
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def bind(self, ctx: "AppContext") -> None:
|
|
112
|
+
"""绑定应用上下文.
|
|
113
|
+
|
|
114
|
+
由 SourceManager 在启动时调用,注入依赖。
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
ctx: 应用上下文
|
|
118
|
+
"""
|
|
119
|
+
self._ctx = ctx
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def ctx(self) -> "AppContext":
|
|
123
|
+
"""获取应用上下文."""
|
|
124
|
+
if self._ctx is None:
|
|
125
|
+
raise RuntimeError("Source 尚未绑定上下文,请先调用 bind()")
|
|
126
|
+
return self._ctx
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def is_running(self) -> bool:
|
|
130
|
+
"""检查是否正在运行."""
|
|
131
|
+
return self.running
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
BaseSourceT = TypeVar("BaseSourceT", bound=BaseSource)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import TypeVar, Union
|
|
4
|
+
|
|
5
|
+
_BaseTypeSelf = Union[str, re.Pattern[str], "BaseType"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseType(str, Enum):
|
|
9
|
+
"""标签枚举基类,提供通用的匹配方法."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def scope(self) -> str:
|
|
13
|
+
"""返回标签的作用域."""
|
|
14
|
+
return self.value.split(".", 1)[0]
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def state(self) -> str:
|
|
18
|
+
"""返回标签的状态."""
|
|
19
|
+
return self.value.split(".", 1)[1]
|
|
20
|
+
|
|
21
|
+
def matches(self, rule: _BaseTypeSelf) -> bool:
|
|
22
|
+
"""判断状态是否匹配.
|
|
23
|
+
|
|
24
|
+
支持三种匹配方式:
|
|
25
|
+
|
|
26
|
+
- ``BaseType``: 按枚举类型匹配(需同 type、同 scope)
|
|
27
|
+
- state 为 "all" 时通配同 scope 下所有状态
|
|
28
|
+
- 支持层级匹配:父状态匹配子状态(如 "message" 匹配 "message.group")
|
|
29
|
+
- ``str``: 作为正则表达式,用 ``re.fullmatch`` 与 ``self.value`` 匹配
|
|
30
|
+
- ``re.Pattern[str]``: 编译好的正则对象,直接调用其 ``fullmatch`` 方法
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
rule: 状态过滤器(``BaseType`` 枚举,``str`` 正则,或编译好的 ``re.Pattern``)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
bool: 匹配结果
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(rule, BaseType):
|
|
39
|
+
# 枚举匹配:需同 type、同 scope;state="all" 通配;层级父匹配子
|
|
40
|
+
if type(self) is type(rule):
|
|
41
|
+
if self.scope == rule.scope:
|
|
42
|
+
if (
|
|
43
|
+
self.state == rule.state
|
|
44
|
+
or rule.state == "all"
|
|
45
|
+
or self.state.startswith(rule.state + ".")
|
|
46
|
+
):
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
return False
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
if isinstance(rule, re.Pattern):
|
|
53
|
+
return bool(rule.fullmatch(self.value))
|
|
54
|
+
|
|
55
|
+
# str 正则匹配:直接与 self.value 全量匹配
|
|
56
|
+
return bool(re.fullmatch(rule, self.value))
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def matching_statuses(cls, rule: _BaseTypeSelf) -> list["BaseType"]:
|
|
60
|
+
"""返回当前枚举类中所有匹配给定规则的成员.
|
|
61
|
+
|
|
62
|
+
遍历当前枚举类的所有成员,筛掉通配标签(state="all"),
|
|
63
|
+
返回余下成员中能通过 ``matches(rule)`` 的元素列表。
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
rule: 状态过滤器(``BaseType`` 枚举、``str`` 或 ``re.Pattern`` 正则)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
匹配的枚举成员列表(不含 state="all" 的通配成员)
|
|
70
|
+
"""
|
|
71
|
+
return [m for m in cls if m.state != "all" and m.matches(rule)]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
BaseTypeT = TypeVar("BaseTypeT", bound=BaseType)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""事件源实现包:每个子包适配一个外部平台(napcat、bilibili 等)."""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Bilibili 适配文档
|
|
2
|
+
|
|
3
|
+
Bilibili 的使用与 API 文档已迁移到统一文档站:
|
|
4
|
+
|
|
5
|
+
- [Bilibili 功能指南](../../../docs/features/bilibili.md)
|
|
6
|
+
- [内置事件源 API](../../../docs/api/builtin-sources.md)
|
|
7
|
+
- [Bilibili 示例](../../../docs/examples/bilibili.md)
|
|
8
|
+
|
|
9
|
+
本文件保留用于兼容仓库旧链接。新文档以当前三个 Source、`BilibiliApi`、类型、
|
|
10
|
+
测试和示例为准。
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .api import BilibiliApi
|
|
2
|
+
from .source import (
|
|
3
|
+
BiliDanmakuSource,
|
|
4
|
+
BiliDynamicSource,
|
|
5
|
+
BiliLiveSource,
|
|
6
|
+
)
|
|
7
|
+
from .types import DanmakuType, DynamicType, LiveType
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BiliDanmakuSource",
|
|
11
|
+
# 事件源类
|
|
12
|
+
"BiliDynamicSource",
|
|
13
|
+
"BiliLiveSource",
|
|
14
|
+
# API类
|
|
15
|
+
"BilibiliApi",
|
|
16
|
+
# 数据类型类
|
|
17
|
+
"DanmakuType",
|
|
18
|
+
"DynamicType",
|
|
19
|
+
"LiveType",
|
|
20
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from bilibili_api import Credential
|
|
5
|
+
from bilibili_api.dynamic import get_dynamic_page_info
|
|
6
|
+
from bilibili_api.live import LiveDanmaku, LiveRoom
|
|
7
|
+
from bilibili_api.user import User
|
|
8
|
+
|
|
9
|
+
from butterbot.core.api import BaseApi
|
|
10
|
+
|
|
11
|
+
from ..data import DynamicData, LiveRoomData, get_max_id
|
|
12
|
+
from ..data.dto import DynamicDTO, LiveRoomDTO
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from butterbot.core.context import ApiRegistry
|
|
16
|
+
|
|
17
|
+
_log = getLogger("BilibiliApi")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BilibiliApi(BaseApi):
|
|
21
|
+
def __init__(self, credential: Credential | None) -> None:
|
|
22
|
+
"""初始化BilibiliApi客户端
|
|
23
|
+
Args:
|
|
24
|
+
credential: Optional[Credential]: B站用户凭证
|
|
25
|
+
"""
|
|
26
|
+
self._credential = credential
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def create(cls, ctx: "ApiRegistry", config_key: str = "bilibili") -> "BilibiliApi":
|
|
30
|
+
"""
|
|
31
|
+
从上下文创建 BilibiliApi 实例
|
|
32
|
+
Args:
|
|
33
|
+
ctx: API 上下文
|
|
34
|
+
config_key: 配置键
|
|
35
|
+
"""
|
|
36
|
+
return cls(ctx.config.get_config(config_key))
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def credential(self) -> Credential | None:
|
|
40
|
+
"""获取B站用户凭证
|
|
41
|
+
Returns:
|
|
42
|
+
Optional[Credential]: B站用户凭证对象,如果未配置则返回None
|
|
43
|
+
"""
|
|
44
|
+
return self._credential
|
|
45
|
+
|
|
46
|
+
async def get_all_dynamic(self, uid: int, offset: str = "") -> list[DynamicData]:
|
|
47
|
+
"""
|
|
48
|
+
获取单页动态
|
|
49
|
+
Args:
|
|
50
|
+
uid (int): 用户uid
|
|
51
|
+
offset (str): 分页偏移参数
|
|
52
|
+
Returns:
|
|
53
|
+
list[DynamicData]: 动态信息对象
|
|
54
|
+
"""
|
|
55
|
+
user = User(credential=self._credential, uid=uid)
|
|
56
|
+
dict_info = await user.get_dynamics_new(offset=offset)
|
|
57
|
+
if dict_info.get("items", None) is None or not dict_info.get("items"):
|
|
58
|
+
raise ValueError("未获取到动态数据或者动态数据为不完整")
|
|
59
|
+
info_dto = DynamicDTO.from_list(dict_info["items"])
|
|
60
|
+
info = [DynamicData.from_dto(dto) for dto in info_dto if dto is not None]
|
|
61
|
+
return info
|
|
62
|
+
|
|
63
|
+
async def get_new_dynamic(self, uid: int) -> DynamicData:
|
|
64
|
+
"""
|
|
65
|
+
获取最新的一条动态
|
|
66
|
+
Args:
|
|
67
|
+
uid (int): 用户uid
|
|
68
|
+
Returns:
|
|
69
|
+
DynamicData: 动态信息对象
|
|
70
|
+
"""
|
|
71
|
+
user = User(credential=self._credential, uid=uid)
|
|
72
|
+
dict_info = await user.get_dynamics_new()
|
|
73
|
+
max_id = get_max_id(dict_info)
|
|
74
|
+
_log.debug("获取到最大时间戳的索引为'%s'", max_id)
|
|
75
|
+
if max_id is None:
|
|
76
|
+
raise ValueError("未获取到动态数据或者动态数据为不完整")
|
|
77
|
+
dynamic_info = dict_info["items"][max_id]
|
|
78
|
+
dto = DynamicDTO.from_raw(dynamic_info)
|
|
79
|
+
if not dto:
|
|
80
|
+
raise ValueError("构造 %s 用户动态DTO对象失败" % uid)
|
|
81
|
+
info = DynamicData.from_dto(dto)
|
|
82
|
+
return info
|
|
83
|
+
|
|
84
|
+
async def get_new_dynamic_list(self) -> list[DynamicData]:
|
|
85
|
+
"""获取动态主页的动态列表
|
|
86
|
+
Returns:
|
|
87
|
+
list[DynamicData]: 动态信息对象
|
|
88
|
+
"""
|
|
89
|
+
if self._credential is None: # pyright: ignore
|
|
90
|
+
raise ValueError("获取动态主页需要 B 站凭证,请配置 credential")
|
|
91
|
+
dynamic_info = await get_dynamic_page_info(self._credential)
|
|
92
|
+
if dynamic_info.get("items", None) is None or not dynamic_info.get("items"):
|
|
93
|
+
raise ValueError("未获取到动态数据或者动态数据为不完整")
|
|
94
|
+
info_dto = DynamicDTO.from_list(dynamic_info["items"])
|
|
95
|
+
info = [DynamicData.from_dto(dto) for dto in info_dto if dto is not None]
|
|
96
|
+
return info
|
|
97
|
+
|
|
98
|
+
async def get_room_info(self, room_id: int) -> LiveRoomData:
|
|
99
|
+
"""
|
|
100
|
+
获取直播间信息
|
|
101
|
+
Args:
|
|
102
|
+
room_id (int): 直播间ID
|
|
103
|
+
Returns:
|
|
104
|
+
LiveRoomData: 直播间信息对象
|
|
105
|
+
"""
|
|
106
|
+
live_room = LiveRoom(credential=self._credential, room_display_id=room_id)
|
|
107
|
+
live = await live_room.get_room_info()
|
|
108
|
+
dto = LiveRoomDTO.from_raw(live)
|
|
109
|
+
if dto is None:
|
|
110
|
+
raise ValueError("构造直播间 %s DTO对象失败" % room_id)
|
|
111
|
+
info = LiveRoomData.from_dto(dto)
|
|
112
|
+
return info
|
|
113
|
+
|
|
114
|
+
def get_live_danmaku(self, room_id: int) -> LiveDanmaku:
|
|
115
|
+
"""
|
|
116
|
+
获取直播间弹幕对象
|
|
117
|
+
Args:
|
|
118
|
+
room_id (int): 直播间ID
|
|
119
|
+
Returns:
|
|
120
|
+
LiveDanmaku: 直播间弹幕对象
|
|
121
|
+
"""
|
|
122
|
+
live_danmaku = LiveDanmaku(credential=self._credential, room_display_id=room_id)
|
|
123
|
+
return live_danmaku
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from .danmaku_gift_data import (
|
|
2
|
+
BlindGiftData,
|
|
3
|
+
DanmakuGiftData,
|
|
4
|
+
GiftMedalData,
|
|
5
|
+
)
|
|
6
|
+
from .danmaku_guard_data import (
|
|
7
|
+
DanmakuGuardData,
|
|
8
|
+
)
|
|
9
|
+
from .danmaku_msg_data import DanmakuMsgData, MedalData
|
|
10
|
+
from .dynamic_data import (
|
|
11
|
+
ArticleData,
|
|
12
|
+
AuthorData,
|
|
13
|
+
DynamicData,
|
|
14
|
+
LiveRcmdData,
|
|
15
|
+
MusicData,
|
|
16
|
+
StatData,
|
|
17
|
+
VideoData,
|
|
18
|
+
get_max_id,
|
|
19
|
+
)
|
|
20
|
+
from .live_room_data import (
|
|
21
|
+
AnchorInfoData,
|
|
22
|
+
LiveRoomData,
|
|
23
|
+
NoticeBoardData,
|
|
24
|
+
RoomInfoData,
|
|
25
|
+
WatchedShowData,
|
|
26
|
+
)
|
|
27
|
+
from .video_part import (
|
|
28
|
+
VideoPartData,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AnchorInfoData",
|
|
33
|
+
"ArticleData",
|
|
34
|
+
"AuthorData",
|
|
35
|
+
"BlindGiftData",
|
|
36
|
+
"DanmakuGiftData",
|
|
37
|
+
"DanmakuGuardData",
|
|
38
|
+
"DanmakuMsgData",
|
|
39
|
+
"DynamicData",
|
|
40
|
+
"GiftMedalData",
|
|
41
|
+
"LiveRcmdData",
|
|
42
|
+
"LiveRoomData",
|
|
43
|
+
"MedalData",
|
|
44
|
+
"MusicData",
|
|
45
|
+
"NoticeBoardData",
|
|
46
|
+
"RoomInfoData",
|
|
47
|
+
"StatData",
|
|
48
|
+
"VideoData",
|
|
49
|
+
"VideoPartData",
|
|
50
|
+
"WatchedShowData",
|
|
51
|
+
"get_max_id",
|
|
52
|
+
]
|