Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions ncatbot/adapter/bilibili/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@

class BilibiliAdapter(BaseAdapter):
name = "bilibili"
description = "Bilibili 适配器 (直播/私信/评论)"
supported_protocols = ["bilibili_live", "bilibili_session", "bilibili_comment"]
description = "Bilibili 适配器 (直播/私信/评论/动态)"
supported_protocols = [
"bilibili_live",
"bilibili_session",
"bilibili_comment",
"bilibili_dynamic",
]
platform = "bilibili"
pip_dependencies = {"bilibili-api-python": ">=17.0.0"}

Expand Down Expand Up @@ -90,6 +95,7 @@ async def connect(self) -> None:
retry_after=self._config.retry_after,
session_poll_interval=self._config.session_poll_interval,
comment_poll_interval=self._config.comment_poll_interval,
dynamic_poll_interval=self._config.dynamic_poll_interval,
)

self._api = BiliBotAPI(self._credential, self._source_manager)
Expand All @@ -106,12 +112,16 @@ async def connect(self) -> None:
watch.id, watch.type, self._credential
)

for watch in self._config.dynamic_watches:
await self._source_manager.add_dynamic_watch(watch.uid, self._credential)

self._connected = True
LOG.info(
"Bilibili 适配器已连接 (直播间: %d, 私信: %s, 评论: %d)",
"Bilibili 适配器已连接 (直播间: %d, 私信: %s, 评论: %d, 动态: %d)",
len(self._config.live_rooms),
self._config.enable_session,
len(self._config.comment_watches),
len(self._config.dynamic_watches),
)

async def listen(self) -> None:
Expand Down
2 changes: 2 additions & 0 deletions ncatbot/adapter/bilibili/api/bot_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .room_manage import RoomManageAPIMixin
from .session import SessionAPIMixin
from .comment import CommentAPIMixin
from .dynamic import DynamicAPIMixin
from .query import QueryAPIMixin

if TYPE_CHECKING:
Expand All @@ -30,6 +31,7 @@ class BiliBotAPI(
RoomManageAPIMixin,
SessionAPIMixin,
CommentAPIMixin,
DynamicAPIMixin,
QueryAPIMixin,
IBiliAPIClient,
):
Expand Down
35 changes: 35 additions & 0 deletions ncatbot/adapter/bilibili/api/dynamic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""动态操作 API Mixin"""

from __future__ import annotations

from typing import Optional


class DynamicAPIMixin:
async def get_user_dynamics(self, uid: int, offset: str = "") -> dict:
"""获取用户动态列表(新版接口)"""
from bilibili_api.user import User

user = User(uid=uid, credential=self._credential)
return await user.get_dynamics_new(offset=offset)

async def get_user_latest_dynamic(self, uid: int) -> Optional[dict]:
"""获取用户最新一条动态"""
resp = await self.get_user_dynamics(uid)
items = resp.get("items") or []
if not items:
return None
return max(
items,
key=lambda it: int(
((it.get("modules") or {}).get("module_author") or {}).get("pub_ts", 0)
),
)

async def add_dynamic_watch(self, uid: int) -> None:
"""添加动态监听"""
await self._source_manager.add_dynamic_watch(uid, self._credential)

async def remove_dynamic_watch(self, uid: int) -> None:
"""移除动态监听"""
await self._source_manager.remove_dynamic_watch(uid)
8 changes: 8 additions & 0 deletions ncatbot/adapter/bilibili/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ class CommentWatch(BaseModel):
type: str = "video"


class DynamicWatch(BaseModel):
"""动态监听条目"""

uid: int


class BilibiliConfig(BaseModel):
"""Bilibili 适配器专属配置"""

Expand All @@ -28,10 +34,12 @@ class BilibiliConfig(BaseModel):
live_rooms: List[int] = Field(default_factory=list)
enable_session: bool = False
comment_watches: List[CommentWatch] = Field(default_factory=list)
dynamic_watches: List[DynamicWatch] = Field(default_factory=list)

# 轮询配置
session_poll_interval: float = 6.0
comment_poll_interval: float = 30.0
dynamic_poll_interval: float = 600.0

# 连接
max_retry: int = 5
Expand Down
176 changes: 175 additions & 1 deletion ncatbot/adapter/bilibili/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ncatbot.types.bilibili.events import (
BiliCommentEventData,
BiliConnectionEventData,
BiliDynamicEventData,
BiliPrivateMessageEventData,
BiliPrivateMessageWithdrawEventData,
DanmuAggregationEventData,
Expand All @@ -34,7 +35,14 @@
WatchedChangeEventData,
)
from ncatbot.types.bilibili.enums import BiliLiveEventType, BiliSessionEventType
from ncatbot.types.bilibili.models import LiveRoomInfo
from ncatbot.types.bilibili.models import (
LiveRoomInfo,
DynamicStatInfo,
DynamicVideoInfo,
DynamicMusicInfo,
DynamicArticleInfo,
DynamicLiveRcmdInfo,
)
from ncatbot.types.bilibili.sender import BiliSender
from ncatbot.types.common.base import BaseEventData
from ncatbot.types.common.segment.array import MessageArray
Expand All @@ -57,6 +65,8 @@ def parse(self, source_type: str, raw_data: dict) -> Optional[BaseEventData]:
return self._parse_session(raw_data)
if source_type == "comment":
return self._parse_comment(raw_data)
if source_type == "dynamic":
return self._parse_dynamic(raw_data)
except Exception:
LOG.debug(
"解析 %s 事件失败: %s",
Expand Down Expand Up @@ -177,6 +187,170 @@ def _parse_comment(self, raw: dict) -> Optional[BaseEventData]:
sender=sender,
)

# ==================== 动态解析 ====================

def _parse_dynamic(self, raw: dict) -> Optional[BaseEventData]:
import json as _json
from ncatbot.types.bilibili.enums import BiliDynamicEventType

item = raw.get("dynamic") or {}
dynamic_status = raw.get("status", "new") # "new" | "deleted"
now = int(time.time())

# 基础信息 — 与 DTO.from_raw 保持一致
dynamic_id = item.get("id_str", "")
dynamic_type = item.get("type", "")
modules = item.get("modules") or {}
author_info = modules.get("module_author") or {}
pub_ts = author_info.get("pub_ts", 0)
pub_time = author_info.get("pub_time", "")

uid = str(author_info.get("mid", raw.get("uid", "")))
uname = author_info.get("name", "")
face = author_info.get("face", "")

# 解析动态内容
text = None
pics_url = None
video = None
music = None
article = None
live_rcmd = None
tag = None
stat = None

module_dynamic = modules.get("module_dynamic") or {}
major = module_dynamic.get("major") or {}

# 统计信息
stat_info = modules.get("module_stat")
if stat_info:
stat = DynamicStatInfo(
comment_count=(stat_info.get("comment") or {}).get("count", 0),
like_count=(stat_info.get("like") or {}).get("count", 0),
forward_count=(stat_info.get("forward") or {}).get("count", 0),
)

# 标签(如置顶)
if modules.get("module_tag"):
tag = modules["module_tag"].get("text")

# 文字/图片动态
if dynamic_type in ("DYNAMIC_TYPE_WORD", "DYNAMIC_TYPE_DRAW"):
opus = major.get("opus") or {}
summary = opus.get("summary") or {}
text = summary.get("text", "")
pics = opus.get("pics") or []
if pics:
pics_url = [pic.get("url", "") for pic in pics]

# 视频动态
elif dynamic_type == "DYNAMIC_TYPE_AV":
archive = major.get("archive") or {}
desc_info = module_dynamic.get("desc")
stat_v = archive.get("stat") or {}
video = DynamicVideoInfo(
av_id=str(archive.get("aid", "")),
bv_id=archive.get("bvid", ""),
title=archive.get("title", ""),
cover=archive.get("cover", ""),
desc=archive.get("desc", ""),
duration_text=archive.get("duration_text", ""),
dynamic_text=desc_info.get("text", "") if desc_info else "",
play_count=str(stat_v.get("play", "")),
danmaku_count=str(stat_v.get("danmaku", "")),
)

# 音乐动态
elif dynamic_type == "DYNAMIC_TYPE_MUSIC":
music_info = major.get("music") or {}
desc_info = module_dynamic.get("desc")
music = DynamicMusicInfo(
music_id=str(music_info.get("id", "")),
title=music_info.get("title", ""),
cover=music_info.get("cover", ""),
label=music_info.get("label", ""),
dynamic_text=desc_info.get("text", "") if desc_info else "",
)

# 专栏动态
elif dynamic_type == "DYNAMIC_TYPE_ARTICLE":
opus = major.get("opus") or {}
summary_info = opus.get("summary") or {}
article = DynamicArticleInfo(
title=opus.get("title", ""),
summary=summary_info.get("text", ""),
has_more=summary_info.get("has_more", False),
article_id=int(dynamic_id) if dynamic_id.isdigit() else 0,
)

# 直播推荐动态
elif dynamic_type == "DYNAMIC_TYPE_LIVE_RCMD":
live_rcmd_obj = major.get("live_rcmd") or {}
live_rcmd_content = live_rcmd_obj.get("content", "{}")
try:
live_data = _json.loads(live_rcmd_content)
except Exception:
live_data = {}
live_play_info = live_data.get("live_play_info") or {}
live_rcmd = DynamicLiveRcmdInfo(
room_id=live_play_info.get("room_id", 0),
live_status=live_play_info.get("live_status", 0),
title=live_play_info.get("title", ""),
cover=live_play_info.get("cover", ""),
online=live_play_info.get("online", 0),
area_id=live_play_info.get("area_id", 0),
area_name=live_play_info.get("area_name", ""),
parent_area_id=live_play_info.get("parent_area_id", 0),
parent_area_name=live_play_info.get("parent_area_name", ""),
live_start_time=live_play_info.get("live_start_time", 0),
)

# 转发动态
forward_dynamic_id = None
if dynamic_type == "DYNAMIC_TYPE_FORWARD":
desc_info = module_dynamic.get("desc")
text = desc_info.get("text", "") if desc_info else ""
orig = item.get("orig") or {}
forward_dynamic_id = orig.get("id_str", "")

# 映射 status → dynamic_event_type
if dynamic_status == "deleted":
dyn_event_type = BiliDynamicEventType.DELETED_DYNAMIC
else:
dyn_event_type = BiliDynamicEventType.NEW_DYNAMIC

sender = BiliSender(
user_id=uid,
nickname=uname,
face_url=face,
)

return BiliDynamicEventData(
time=pub_ts or now,
self_id=self._self_id,
platform="bilibili",
dynamic_event_type=dyn_event_type,
dynamic_status=dynamic_status,
dynamic_id=dynamic_id,
dynamic_type=dynamic_type,
uid=uid,
user_name=uname,
face_url=face,
pub_ts=pub_ts,
pub_time=pub_time,
text=text,
pics_url=pics_url,
tag=tag,
stat=stat,
video=video,
music=music,
article=article,
live_rcmd=live_rcmd,
forward_dynamic_id=forward_dynamic_id,
sender=sender,
)


# ==================== 直播事件解析器注册表 ====================

Expand Down
2 changes: 2 additions & 0 deletions ncatbot/adapter/bilibili/source/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
from .live_source import LiveSource
from .session_source import SessionSource
from .comment_source import CommentSource
from .dynamic_source import DynamicSource
from .manager import SourceManager

__all__ = [
"BaseSource",
"LiveSource",
"SessionSource",
"CommentSource",
"DynamicSource",
"SourceManager",
]
Loading
Loading