该提交实现了完整的B站渠道插件,包含以下核心功能: 1. 支持B站直播弹幕监听与处理,包含弹幕、SC、礼物等多种直播间事件 2. 支持B站私信的轮询接收与发送 3. 内置WBI签名算法,适配B站API鉴权要求 4. 提供账号配对、黑白名单等弹幕私信权限控制 5. 集成速率限制与防风险机制,降低账号封禁风险 6. 完善的配置管理与状态监控能力
357 lines
14 KiB
Python
357 lines
14 KiB
Python
from __future__ import annotations
|
||
|
||
import logging
|
||
import time
|
||
|
||
from yuxi.channel.extensions.bilibili.danmaku.types import DanmakuMessage, SuperChatEvent
|
||
from yuxi.channel.extensions.bilibili.types import BilibiliAccountConfig
|
||
from yuxi.channel.message.models import (
|
||
GroupContext,
|
||
MessageType,
|
||
PeerInfo,
|
||
UnifiedMessage,
|
||
)
|
||
from yuxi.channel.routing.models import PeerKind
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_GUARD_LEVEL_NAME = {1: "总督", 2: "提督", 3: "舰长"}
|
||
|
||
|
||
class DanmakuEventHandler:
|
||
def __init__(self, room_id: int, account, runtime, account_id: str = "default"):
|
||
self._room_id = room_id
|
||
self._account = account
|
||
self._runtime = runtime
|
||
self._account_id = account_id
|
||
self._bot_uid = str(account.dedeuserid)
|
||
self._room_users_seen: set[str] = set()
|
||
|
||
async def handle_danmaku(self, data: dict) -> None:
|
||
info = data["info"]
|
||
text = info[1]
|
||
user_info = info[2]
|
||
uid = str(user_info[0])
|
||
uname = user_info[1]
|
||
|
||
msg = DanmakuMessage(
|
||
room_id=self._room_id,
|
||
uid=uid,
|
||
uname=uname,
|
||
text=text,
|
||
timestamp=time.time(),
|
||
fans_medal_name=info[3][1] if len(info[3]) > 1 else "",
|
||
fans_medal_level=info[3][0] if len(info[3]) > 0 else 0,
|
||
ul_level=info[5][0] if len(info[5]) > 0 else 0,
|
||
)
|
||
|
||
if uid == self._bot_uid:
|
||
return
|
||
|
||
if self._account.require_mention and not self._should_respond(msg):
|
||
return
|
||
|
||
await self._dispatch_to_agent(msg)
|
||
|
||
async def handle_super_chat(self, data: dict) -> None:
|
||
event = SuperChatEvent(
|
||
room_id=self._room_id,
|
||
uid=str(data["uid"]),
|
||
uname=data["user_info"]["uname"],
|
||
message=data["message"],
|
||
price=data["price"],
|
||
start_time=data["start_time"],
|
||
end_time=data["end_time"],
|
||
timestamp=time.time(),
|
||
)
|
||
await self._dispatch_to_agent(event)
|
||
|
||
async def handle_gift(self, data: dict) -> None:
|
||
uname = data.get("uname", "")
|
||
uid = str(data.get("uid", ""))
|
||
num = data.get("num", 0)
|
||
gift_name = data.get("giftName", "")
|
||
price = data.get("price", 0)
|
||
|
||
logger.info("直播间 %s 礼物: %s 送出 %sx %s", self._room_id, uname, num, gift_name)
|
||
|
||
content = f"[礼物通知] {uname} 送出了 {num}x {gift_name}"
|
||
if price:
|
||
content += f" (总价 {price} 电池)"
|
||
|
||
await self._dispatch_event("gift", uid, uname, content)
|
||
|
||
async def handle_guard(self, data: dict) -> None:
|
||
username = data.get("username", "")
|
||
uid = str(data.get("uid", ""))
|
||
guard_level = data.get("guard_level", 0)
|
||
guard_name = _GUARD_LEVEL_NAME.get(guard_level, f"舰队Lv{guard_level}")
|
||
|
||
logger.info("直播间 %s 大航海: %s 开通 %s", self._room_id, username, guard_name)
|
||
|
||
content = f"[舰队通知] {username} 开通了{guard_name}!"
|
||
await self._dispatch_event("guard", uid, username, content)
|
||
|
||
async def handle_interact(self, data: dict) -> None:
|
||
msg_type = data.get("msg_type", 0)
|
||
if msg_type != 1:
|
||
return
|
||
|
||
uid = str(data.get("uid", ""))
|
||
uname = data.get("uname", "")
|
||
|
||
if uid in self._room_users_seen:
|
||
return
|
||
self._room_users_seen.add(uid)
|
||
|
||
content = f"[进房通知] {uname} 进入了直播间"
|
||
await self._dispatch_event("enter", uid, uname, content)
|
||
|
||
async def handle_live(self, data: dict) -> None:
|
||
logger.info("直播间 %s 开播", self._room_id)
|
||
await self._dispatch_status_event("live", "[直播状态] 开播了")
|
||
|
||
async def handle_preparing(self, data: dict) -> None:
|
||
logger.info("直播间 %s 下播", self._room_id)
|
||
await self._dispatch_status_event("preparing", "[直播状态] 下播了")
|
||
|
||
async def handle_cut_off(self, data: dict) -> None:
|
||
logger.warning("直播间 %s 被超管切断", self._room_id)
|
||
await self._dispatch_status_event("cut_off", "[直播状态] 直播间被超管切断")
|
||
|
||
async def handle_warning(self, data: dict) -> None:
|
||
logger.warning("直播间 %s 收到管理员警告", self._room_id)
|
||
await self._dispatch_status_event("warning", "[直播状态] 收到管理员警告")
|
||
|
||
async def handle_combo_send(self, data: dict) -> None:
|
||
uname = data.get("uname", "")
|
||
uid = str(data.get("uid", ""))
|
||
combo_num = data.get("combo_num", 0)
|
||
gift_name = data.get("gift_name", "")
|
||
|
||
logger.info("直播间 %s 连击: %s 连送 %s 个 %s", self._room_id, uname, combo_num, gift_name)
|
||
|
||
content = f"[连击礼物] {uname} 连送 {combo_num} 个 {gift_name}"
|
||
await self._dispatch_event("combo_send", uid, uname, content)
|
||
|
||
async def handle_sc_delete(self, data: dict) -> None:
|
||
message_id = data.get("message_id", "")
|
||
logger.info("直播间 %s SC 删除: message_id=%s", self._room_id, message_id)
|
||
content = f"[SC 删除] 消息 #{message_id} 已被删除"
|
||
await self._dispatch_status_event("sc_delete", content)
|
||
|
||
async def handle_user_toast(self, data: dict) -> None:
|
||
uid = str(data.get("uid", ""))
|
||
uname = data.get("username", "")
|
||
toast_msg = data.get("toast_msg", "")
|
||
guard_level = data.get("guard_level", 0)
|
||
price = data.get("price", 0)
|
||
num = data.get("num", 0)
|
||
gift_name = data.get("gift_name", "")
|
||
|
||
logger.info("直播间 %s Toast: %s - %s", self._room_id, uname, toast_msg)
|
||
|
||
if guard_level:
|
||
guard_name = _GUARD_LEVEL_NAME.get(guard_level, f"舰队Lv{guard_level}")
|
||
content = f"[上舰感谢] {uname} 开通{guard_name}!"
|
||
elif gift_name:
|
||
content = f"[礼物感谢] {uname} 赠送 {num}x {gift_name}"
|
||
if price:
|
||
content += f" (总价 {price} 电池)"
|
||
else:
|
||
content = f"[系统通知] {toast_msg}"
|
||
|
||
await self._dispatch_event("toast", uid, uname, content)
|
||
|
||
async def handle_welcome_guard(self, data: dict) -> None:
|
||
uid = str(data.get("uid", ""))
|
||
username = data.get("username", "")
|
||
guard_level = data.get("guard_level", 0)
|
||
guard_name = _GUARD_LEVEL_NAME.get(guard_level, f"舰队Lv{guard_level}")
|
||
|
||
logger.info("直播间 %s 舰长入场: %s (%s)", self._room_id, username, guard_name)
|
||
content = f"[舰长入场] {username} ({guard_name}) 进入了直播间"
|
||
await self._dispatch_event("welcome_guard", uid, username, content)
|
||
|
||
async def handle_entry_effect(self, data: dict) -> None:
|
||
uid = str(data.get("uid", ""))
|
||
uname = data.get("uname", "")
|
||
effect_id = data.get("effect_id", 0)
|
||
|
||
logger.info("直播间 %s 进场特效: %s (effect_id=%s)", self._room_id, uname, effect_id)
|
||
content = f"[进场特效] {uname} 进入了直播间"
|
||
await self._dispatch_event("entry_effect", uid, uname, content)
|
||
|
||
async def handle_super_chat_jpn(self, data: dict) -> None:
|
||
event = SuperChatEvent(
|
||
room_id=self._room_id,
|
||
uid=str(data.get("uid", "")),
|
||
uname=data.get("user_info", {}).get("uname", ""),
|
||
message=data.get("message", ""),
|
||
price=data.get("price", 0),
|
||
start_time=data.get("start_time", 0),
|
||
end_time=data.get("end_time", 0),
|
||
timestamp=time.time(),
|
||
)
|
||
await self._dispatch_to_agent(event)
|
||
|
||
async def handle_pk_start(self, data: dict) -> None:
|
||
pk_id = data.get("pk_id", "")
|
||
logger.info("直播间 %s PK 开始: pk_id=%s", self._room_id, pk_id)
|
||
content = "[PK通知] 大乱斗开始了!"
|
||
await self._dispatch_status_event("pk_start", content)
|
||
|
||
async def handle_pk_end(self, data: dict) -> None:
|
||
pk_id = data.get("pk_id", "")
|
||
logger.info("直播间 %s PK 结束: pk_id=%s", self._room_id, pk_id)
|
||
content = "[PK通知] 大乱斗结束了"
|
||
await self._dispatch_status_event("pk_end", content)
|
||
|
||
async def handle_pk_process(self, data: dict) -> None:
|
||
logger.debug("直播间 %s PK 进行中", self._room_id)
|
||
|
||
async def handle_anchor_lot_start(self, data: dict) -> None:
|
||
award_name = data.get("award_name", "")
|
||
logger.info("直播间 %s 天选时刻开始: %s", self._room_id, award_name)
|
||
content = f"[天选时刻] 抽奖开始 - {award_name}"
|
||
await self._dispatch_status_event("anchor_lot_start", content)
|
||
|
||
async def handle_anchor_lot_end(self, data: dict) -> None:
|
||
logger.info("直播间 %s 天选时刻结束", self._room_id)
|
||
content = "[天选时刻] 抽奖已结束"
|
||
await self._dispatch_status_event("anchor_lot_end", content)
|
||
|
||
async def handle_anchor_lot_award(self, data: dict) -> None:
|
||
award_name = data.get("award_name", "")
|
||
winners = data.get("award_users", [])
|
||
winner_names = [w.get("uname", "") for w in winners]
|
||
logger.info("直播间 %s 天选时刻开奖: %s -> %s", self._room_id, award_name, winner_names)
|
||
content = f"[天选时刻] 恭喜 {', '.join(winner_names)} 获得 {award_name}"
|
||
await self._dispatch_status_event("anchor_lot_award", content)
|
||
|
||
async def handle_notice_msg(self, data: dict) -> None:
|
||
msg_common = data.get("msg_common", "")
|
||
business_id = data.get("business_id", "")
|
||
msg_type = data.get("msg_type", 0)
|
||
|
||
logger.info("直播间 %s 系统通知: type=%s business=%s", self._room_id, msg_type, business_id)
|
||
content = f"[系统通知] {msg_common}"
|
||
await self._dispatch_status_event("notice", content)
|
||
|
||
async def handle_welcome(self, data: dict) -> None:
|
||
uname = data.get("uname", "")
|
||
logger.debug("直播间 %s 老爷入场: %s", self._room_id, uname)
|
||
|
||
async def handle_online_rank(self, data: dict) -> None:
|
||
logger.debug("直播间 %s 高能榜更新 (count=...)", self._room_id)
|
||
|
||
async def handle_watched_change(self, data: dict) -> None:
|
||
num = data.get("num", 0)
|
||
text_large = data.get("text_large", "")
|
||
logger.debug("直播间 %s 观看人数: %s (%s)", self._room_id, num, text_large)
|
||
|
||
async def handle_like_info(self, data: dict) -> None:
|
||
logger.debug("直播间 %s 点赞更新", self._room_id)
|
||
|
||
async def handle_room_change(self, data: dict) -> None:
|
||
title = data.get("title", "")
|
||
area_name = data.get("area_name", "")
|
||
logger.info("直播间 %s 信息变更: title=%s area=%s", self._room_id, title, area_name)
|
||
|
||
async def handle_voice_join(self, data: dict) -> None:
|
||
logger.debug("直播间 %s 语音连麦状态变更", self._room_id)
|
||
|
||
async def handle_red_pocket_start(self, data: dict) -> None:
|
||
logger.debug("直播间 %s 红包活动开始", self._room_id)
|
||
|
||
async def handle_red_pocket_winner(self, data: dict) -> None:
|
||
logger.debug("直播间 %s 红包中奖名单", self._room_id)
|
||
|
||
def _should_respond(self, msg: DanmakuMessage) -> bool:
|
||
text = msg.text.strip()
|
||
keywords = self._account.bot_keywords or []
|
||
return any(kw in text for kw in keywords) or len(text) > 0
|
||
|
||
async def _dispatch_to_agent(self, msg) -> None:
|
||
content = msg.text if hasattr(msg, "text") else msg.message
|
||
unified = UnifiedMessage(
|
||
msg_id=f"danmaku_{msg.room_id}_{msg.uid}_{int(msg.timestamp)}",
|
||
channel_type="bilibili",
|
||
account_id=self._account_id,
|
||
content=content,
|
||
sender=PeerInfo(
|
||
kind=PeerKind.GROUP,
|
||
id=msg.uid,
|
||
display_name=msg.uname,
|
||
),
|
||
group=GroupContext(
|
||
id=str(msg.room_id),
|
||
route_peer_kind="group",
|
||
route_peer_id=str(msg.room_id),
|
||
),
|
||
message_type=MessageType.TEXT,
|
||
timestamp=time.time(),
|
||
metadata={"source": "bilibili_danmaku"},
|
||
)
|
||
|
||
queue = getattr(self._runtime, "queue", None)
|
||
if queue:
|
||
await queue.put(unified)
|
||
else:
|
||
logger.warning("运行时队列不可用,无法分发弹幕消息")
|
||
|
||
async def _dispatch_event(self, event_type: str, uid: str, uname: str, content: str) -> None:
|
||
unified = UnifiedMessage(
|
||
msg_id=f"{event_type}_{self._room_id}_{uid}_{int(time.time())}",
|
||
channel_type="bilibili",
|
||
account_id=self._account_id,
|
||
content=content,
|
||
sender=PeerInfo(kind=PeerKind.GROUP, id=uid, display_name=uname),
|
||
group=GroupContext(
|
||
id=str(self._room_id),
|
||
route_peer_kind="group",
|
||
route_peer_id=str(self._room_id),
|
||
),
|
||
message_type=MessageType.EVENT,
|
||
timestamp=time.time(),
|
||
metadata={"source": "bilibili_danmaku", "event_type": event_type},
|
||
)
|
||
|
||
queue = getattr(self._runtime, "queue", None)
|
||
if queue:
|
||
await queue.put(unified)
|
||
else:
|
||
logger.warning("运行时队列不可用,无法分发 %s 事件", event_type)
|
||
|
||
async def _dispatch_status_event(self, event_type: str, content: str) -> None:
|
||
unified = UnifiedMessage(
|
||
msg_id=f"live_status_{self._room_id}_{event_type}_{int(time.time())}",
|
||
channel_type="bilibili",
|
||
account_id=self._account_id,
|
||
content=content,
|
||
sender=PeerInfo(kind=PeerKind.GROUP, id="system", display_name="B站系统"),
|
||
group=GroupContext(
|
||
id=str(self._room_id),
|
||
route_peer_kind="group",
|
||
route_peer_id=str(self._room_id),
|
||
),
|
||
message_type=MessageType.EVENT,
|
||
timestamp=time.time(),
|
||
metadata={"source": "bilibili_danmaku", "event_type": event_type},
|
||
)
|
||
|
||
queue = getattr(self._runtime, "queue", None)
|
||
if queue:
|
||
await queue.put(unified)
|
||
else:
|
||
logger.warning("运行时队列不可用,无法分发 %s 状态事件", event_type)
|
||
|
||
|
||
def resolve_reply_target(msg: DanmakuMessage, account: BilibiliAccountConfig) -> str | None:
|
||
if account.danmaku_reply_enabled:
|
||
return f"danmaku:{msg.room_id}"
|
||
elif account.dm_enabled:
|
||
return f"dm:{msg.uid}"
|
||
else:
|
||
return None
|