该提交实现了完整的B站渠道插件,包含以下核心功能: 1. 支持B站直播弹幕监听与处理,包含弹幕、SC、礼物等多种直播间事件 2. 支持B站私信的轮询接收与发送 3. 内置WBI签名算法,适配B站API鉴权要求 4. 提供账号配对、黑白名单等弹幕私信权限控制 5. 集成速率限制与防风险机制,降低账号封禁风险 6. 完善的配置管理与状态监控能力
153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
from yuxi.channel.extensions.bilibili.anti_risk import AntiRisk
|
|
from yuxi.channel.extensions.bilibili.client import BilibiliClientManager
|
|
from yuxi.channel.extensions.bilibili.config import BilibiliConfigAdapter
|
|
from yuxi.channel.extensions.bilibili.types import BilibiliAccountConfig
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BilibiliGateway:
|
|
def __init__(self):
|
|
self._config_adapter = BilibiliConfigAdapter()
|
|
self._client_manager: BilibiliClientManager | None = None
|
|
self._anti_risk: AntiRisk | None = None
|
|
self._danmaku_tasks: dict[int, asyncio.Task] = {}
|
|
self._dm_poll_task: asyncio.Task | None = None
|
|
self._running = False
|
|
self._abort_signal: asyncio.Event | None = None
|
|
self._queue: asyncio.Queue | None = None
|
|
|
|
@property
|
|
def credential(self):
|
|
if self._client_manager is None:
|
|
return None
|
|
return self._client_manager.credential
|
|
|
|
@property
|
|
def anti_risk(self) -> AntiRisk | None:
|
|
return self._anti_risk
|
|
|
|
async def start(self, ctx) -> object:
|
|
account, account_id = await self._resolve_account(ctx)
|
|
|
|
if not self._config_adapter.is_configured(account):
|
|
logger.warning("B站账户 %s 未配置,跳过启动", account_id)
|
|
return {"running": False, "reason": "not-configured"}
|
|
|
|
self._abort_signal = asyncio.Event()
|
|
self._queue = asyncio.Queue(maxsize=2000)
|
|
self._running = True
|
|
|
|
acct_config = self._config_adapter.get_account_config(ctx.config or {}, account_id)
|
|
|
|
self._client_manager = BilibiliClientManager(acct_config, ctx.logger or logger)
|
|
await self._client_manager.setup()
|
|
|
|
if not await self._client_manager.verify_credential():
|
|
raise RuntimeError("B站 Cookie 验证失败,可能已过期")
|
|
|
|
anti_risk_level = self._config_adapter.get_anti_risk_level(ctx.config or {})
|
|
self._anti_risk = AntiRisk(
|
|
level=anti_risk_level,
|
|
multiplier=acct_config.rate_limit_multiplier,
|
|
)
|
|
|
|
for room_id in acct_config.room_ids:
|
|
task = asyncio.create_task(
|
|
self._run_danmaku_monitor(room_id, acct_config, ctx, account_id),
|
|
name=f"bilibili-danmaku:{room_id}",
|
|
)
|
|
self._danmaku_tasks[room_id] = task
|
|
|
|
if acct_config.dm_enabled:
|
|
self._dm_poll_task = asyncio.create_task(
|
|
self._run_dm_poller(acct_config, ctx, account_id),
|
|
name="bilibili-dm-poller",
|
|
)
|
|
|
|
ctx.cancel_event_monitor = self._abort_signal
|
|
|
|
logger.info(
|
|
"B站网关已启动 account=%s room_count=%s dm=%s",
|
|
account_id,
|
|
len(acct_config.room_ids),
|
|
acct_config.dm_enabled,
|
|
)
|
|
return {"running": True, "account_id": account_id, "queue": self._queue}
|
|
|
|
async def stop(self, ctx) -> None:
|
|
self._running = False
|
|
|
|
if self._abort_signal:
|
|
self._abort_signal.set()
|
|
|
|
for room_id, task in self._danmaku_tasks.items():
|
|
task.cancel()
|
|
if self._dm_poll_task:
|
|
self._dm_poll_task.cancel()
|
|
|
|
for room_id, task in self._danmaku_tasks.items():
|
|
try:
|
|
await asyncio.wait_for(task, timeout=5.0)
|
|
except (TimeoutError, asyncio.CancelledError):
|
|
pass
|
|
if self._dm_poll_task:
|
|
try:
|
|
await asyncio.wait_for(self._dm_poll_task, timeout=5.0)
|
|
except (TimeoutError, asyncio.CancelledError):
|
|
pass
|
|
|
|
self._danmaku_tasks.clear()
|
|
self._dm_poll_task = None
|
|
|
|
if self._abort_signal:
|
|
self._abort_signal = None
|
|
self._queue = None
|
|
|
|
logger.info("B站网关已停止")
|
|
|
|
async def _run_danmaku_monitor(
|
|
self,
|
|
room_id: int,
|
|
account: BilibiliAccountConfig,
|
|
ctx,
|
|
account_id: str,
|
|
) -> None:
|
|
from yuxi.channel.extensions.bilibili.danmaku.monitor import DanmakuMonitor
|
|
|
|
monitor = DanmakuMonitor(
|
|
credential=self._client_manager.credential,
|
|
room_id=room_id,
|
|
account=account,
|
|
anti_risk=self._anti_risk,
|
|
runtime=ctx,
|
|
abort_signal=self._abort_signal,
|
|
account_id=account_id,
|
|
)
|
|
await monitor.run()
|
|
|
|
async def _run_dm_poller(self, account: BilibiliAccountConfig, ctx, account_id: str) -> None:
|
|
from yuxi.channel.extensions.bilibili.private_msg.poller import DMPoller
|
|
|
|
poller = DMPoller(
|
|
credential=self._client_manager.credential,
|
|
account=account,
|
|
anti_risk=self._anti_risk,
|
|
runtime=ctx,
|
|
abort_signal=self._abort_signal,
|
|
account_id=account_id,
|
|
)
|
|
await poller.run()
|
|
|
|
async def _resolve_account(self, ctx) -> tuple[dict, str]:
|
|
config = getattr(ctx, "config", {}) or {}
|
|
account_id = getattr(ctx, "account_id", "default")
|
|
self._config_adapter.list_account_ids(config)
|
|
account = await self._config_adapter.resolve_account(account_id)
|
|
return account, account_id
|