该提交实现了完整的B站渠道插件,包含以下核心功能: 1. 支持B站直播弹幕监听与处理,包含弹幕、SC、礼物等多种直播间事件 2. 支持B站私信的轮询接收与发送 3. 内置WBI签名算法,适配B站API鉴权要求 4. 提供账号配对、黑白名单等弹幕私信权限控制 5. 集成速率限制与防风险机制,降低账号封禁风险 6. 完善的配置管理与状态监控能力
158 lines
5.1 KiB
Python
158 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import time
|
|
|
|
import httpx
|
|
|
|
from yuxi.channel.extensions.bilibili.types import BilibiliConfig
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BilibiliOutbound:
|
|
SEND_DM_URL = "https://api.vc.bilibili.com/web_im/v1/web_im/send_msg"
|
|
|
|
def __init__(self, credential, anti_risk, config: BilibiliConfig):
|
|
self._credential = credential
|
|
self._anti_risk = anti_risk
|
|
self._config = config
|
|
self._client: httpx.AsyncClient | None = None
|
|
|
|
async def _ensure_client(self) -> httpx.AsyncClient:
|
|
if self._client is None:
|
|
self._client = httpx.AsyncClient()
|
|
return self._client
|
|
|
|
async def close(self) -> None:
|
|
if self._client:
|
|
await self._client.aclose()
|
|
self._client = None
|
|
|
|
async def send_text(
|
|
self,
|
|
*,
|
|
to: str,
|
|
text: str,
|
|
account_id: str = "default",
|
|
**kwargs,
|
|
) -> dict:
|
|
prefix, destination = self._parse_target(to)
|
|
|
|
if prefix == "dm":
|
|
return await self._send_dm(destination, text)
|
|
elif prefix == "danmaku":
|
|
return await self._send_danmaku(destination, text)
|
|
elif prefix == "comment":
|
|
return await self._send_comment(destination, text)
|
|
else:
|
|
return {"ok": False, "error": f"Unknown target type: {prefix}"}
|
|
|
|
async def _send_dm(self, receiver_uid: str, text: str) -> dict:
|
|
await self._anti_risk.throttle("dm_send")
|
|
|
|
client = await self._ensure_client()
|
|
|
|
content_json = json.dumps({"content": text})
|
|
form_data = {
|
|
"msg[sender_uid]": self._credential.dedeuserid,
|
|
"msg[receiver_id]": receiver_uid,
|
|
"msg[receiver_type]": 1,
|
|
"msg[msg_type]": 1,
|
|
"msg[content]": content_json,
|
|
"msg[timestamp]": int(time.time()),
|
|
"csrf": self._credential.bili_jct,
|
|
}
|
|
|
|
resp = await client.post(
|
|
self.SEND_DM_URL,
|
|
cookies=self._credential.get_cookies(),
|
|
data=form_data,
|
|
)
|
|
result = resp.json()
|
|
return {
|
|
"ok": result.get("code") == 0,
|
|
"message_id": result.get("data", {}).get("msg_key", ""),
|
|
"error": result.get("message") if result.get("code") != 0 else None,
|
|
}
|
|
|
|
async def _send_danmaku(self, room_id: str, text: str) -> dict:
|
|
account = self._config.accounts.get("default")
|
|
if account is None or not account.danmaku_reply_enabled:
|
|
return {"ok": False, "error": "Danmaku reply is disabled"}
|
|
|
|
from bilibili_api.live import LiveDanmaku
|
|
|
|
lm = LiveDanmaku(int(room_id), credential=self._credential)
|
|
result = await lm.send_danmaku(text[:50])
|
|
return {"ok": result, "message_id": ""}
|
|
|
|
async def _send_comment(self, oid: str, text: str) -> dict:
|
|
await self._anti_risk.throttle("comment_reply")
|
|
|
|
client = await self._ensure_client()
|
|
resp = await client.post(
|
|
"https://api.bilibili.com/x/v2/reply/add",
|
|
cookies=self._credential.get_cookies(),
|
|
data={
|
|
"oid": oid,
|
|
"type": 1,
|
|
"message": text,
|
|
"plat": 1,
|
|
"csrf": self._credential.bili_jct,
|
|
},
|
|
)
|
|
result = resp.json()
|
|
return {
|
|
"ok": result.get("code") == 0,
|
|
"message_id": str(result.get("data", {}).get("rpid", "")),
|
|
"error": result.get("message") if result.get("code") != 0 else None,
|
|
}
|
|
|
|
@staticmethod
|
|
def _parse_target(to: str) -> tuple[str, str]:
|
|
if ":" not in to:
|
|
raise ValueError(f"Invalid target format: {to}")
|
|
prefix, _, destination = to.partition(":")
|
|
return prefix, destination
|
|
|
|
async def upload_image(self, source: str) -> str | None:
|
|
if source.startswith(("http://", "https://")):
|
|
client = await self._ensure_client()
|
|
resp = await client.get(source)
|
|
resp.raise_for_status()
|
|
file_data = resp.content
|
|
else:
|
|
import aiofiles
|
|
|
|
async with aiofiles.open(source, "rb") as f:
|
|
file_data = await f.read()
|
|
|
|
client = await self._ensure_client()
|
|
resp = await client.post(
|
|
"https://api.vc.bilibili.com/api/v1/image/upload",
|
|
cookies=self._credential.get_cookies(),
|
|
files={"file_up": ("image.png", file_data, "image/png")},
|
|
data={"csrf": self._credential.bili_jct},
|
|
)
|
|
result = resp.json()
|
|
if result.get("code") == 0:
|
|
return result.get("data", {}).get("image_url")
|
|
return None
|
|
|
|
async def follow_user(self, uid: str) -> bool:
|
|
client = await self._ensure_client()
|
|
resp = await client.post(
|
|
"https://api.bilibili.com/x/relation/modify",
|
|
cookies=self._credential.get_cookies(),
|
|
data={
|
|
"fid": uid,
|
|
"act": 1,
|
|
"re_src": 11,
|
|
"csrf": self._credential.bili_jct,
|
|
},
|
|
)
|
|
result = resp.json()
|
|
return result.get("code") == 0
|