use std::time::Duration;
use futures_util::{future, stream, SinkExt, Stream, StreamExt, TryStreamExt};
use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize};
use tokio::task::JoinHandle;
use tokio::time::sleep;
use tokio_tungstenite::{connect_async, tungstenite::Message};
pub use ws_type::{
DanmuMessage, InteractWord, LiveMessageError, LiveMessageResult, SendGift, SuperChatMessage,
WsStreamMessageType,
};
use log::{debug, info, warn};
use crate::{http_client::HttpClient, pack::build_pack};
use ws_type::WsStreamCtx;
mod http_client;
mod pack;
mod sign;
mod ws_type;
type WsReadType = futures_util::stream::SplitStream<
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
>;
type WsWriteType = futures_util::stream::SplitSink<
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
Message,
>;
#[derive(thiserror::Error, Debug)]
pub enum FelgensError {
#[error(transparent)]
UrlError(#[from] url::ParseError),
#[error("Can not connect any websocket host!")]
FailedConnectWsHost,
#[error("弹幕认证被拒(code={code}):{message}")]
AuthFailed { code: i64, message: String },
#[error("弹幕认证超时:没等到服务端的认证回复")]
AuthTimeout,
#[error(transparent)]
SerdeError(#[from] serde_json::Error),
#[error(transparent)]
TungsteniteError(#[from] tokio_tungstenite::tungstenite::Error),
#[error(transparent)]
LiveMessageError(#[from] Box<LiveMessageError>),
#[error(transparent)]
ReqwestError(#[from] reqwest::Error),
#[error("接口 {what} 返回 code={code}:{message}")]
ApiError {
what: String,
code: i64,
message: String,
},
#[error(transparent)]
ScrollError(#[from] scroll::Error),
#[error(transparent)]
ReadError(#[from] std::io::Error),
#[error("Unsupport proto version! {0}")]
UnsupportProto(String),
#[error(transparent)]
Utf8Error(#[from] std::str::Utf8Error),
}
pub type FelgensResult<T> = Result<T, FelgensError>;
#[derive(Serialize)]
struct WsSend {
uid: u32,
roomid: u64,
key: String,
}
pub async fn stream(
roomid: u64,
cookie: &str,
) -> FelgensResult<impl Stream<Item = FelgensResult<WsStreamMessageType>> + Send> {
ticket(roomid, cookie).await?.stream().await
}
pub async fn raw_stream(
roomid: u64,
cookie: &str,
) -> FelgensResult<impl Stream<Item = FelgensResult<String>> + Send> {
ticket(roomid, cookie).await?.raw_stream().await
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Ticket {
roomid: u64,
uid: u64,
token: String,
hosts: Vec<String>,
}
impl std::fmt::Debug for Ticket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ticket")
.field("roomid", &self.roomid)
.field("uid", &self.uid)
.field("token", &"<略>")
.field("hosts", &self.hosts)
.finish()
}
}
impl Ticket {
pub fn roomid(&self) -> u64 {
self.roomid
}
pub async fn stream(
&self,
) -> FelgensResult<impl Stream<Item = FelgensResult<WsStreamMessageType>> + Send> {
let (write, read) = prepare(self).await?;
let messages = frame_stream(read)
.and_then(|message| future::ready(Ok(typed_messages_of(message))))
.map_ok(|items| stream::iter(items.into_iter().map(Ok)))
.try_flatten();
Ok(with_heartbeat(messages, write))
}
pub async fn raw_stream(
&self,
) -> FelgensResult<impl Stream<Item = FelgensResult<String>> + Send> {
let (write, read) = prepare(self).await?;
let messages = frame_stream(read)
.and_then(|message| future::ready(Ok(raw_messages_of(message))))
.map_ok(|items| stream::iter(items.into_iter().map(Ok)))
.try_flatten();
Ok(with_heartbeat(messages, write))
}
}
pub async fn ticket(roomid: u64, cookie: &str) -> FelgensResult<Ticket> {
let client = HttpClient::new()?;
let roomid = client.get_room_id(roomid).await?;
let mut headers = HeaderMap::new();
headers.insert(
reqwest::header::COOKIE,
cookie.parse().expect("Failed to parse cookie!"),
);
let (_, _, uid) = client.get_nav(headers.clone()).await?;
debug!("uid is: {}", uid);
let dammu_info = client.get_dammu_info(roomid, headers).await?.data;
Ok(Ticket {
roomid,
uid,
token: dammu_info.token,
hosts: dammu_info
.host_list
.into_iter()
.map(|host| host.host)
.collect(),
})
}
fn frame_stream(read: WsReadType) -> impl Stream<Item = FelgensResult<Message>> + Send {
read.map_err(FelgensError::from)
}
fn raw_messages_of(message: Message) -> Vec<String> {
let data = message.into_data();
if data.is_empty() {
return Vec::new();
}
build_pack(&data).unwrap_or_default()
}
fn typed_messages_of(message: Message) -> Vec<WsStreamMessageType> {
let mut messages = Vec::new();
for raw in raw_messages_of(message) {
match WsStreamCtx::new(&raw).and_then(|ctx| ctx.match_msg()) {
Ok(message) => messages.push(message),
Err(e) => debug!("skipping message: {e}"),
}
}
messages
}
fn with_heartbeat<S>(stream: S, write: WsWriteType) -> impl Stream<Item = S::Item> + Send
where
S: Stream + Send,
{
let heartbeat = Heartbeat(tokio::spawn(async move {
if let Err(e) = send_heartbeat_packets(write).await {
debug!("heartbeat task stopped: {e}");
}
}));
stream.map(move |item| {
let _ = &heartbeat;
item
})
}
struct Heartbeat(JoinHandle<()>);
impl Drop for Heartbeat {
fn drop(&mut self) {
self.0.abort();
}
}
const AUTH_TIMEOUT: Duration = Duration::from_secs(10);
async fn prepare(ticket: &Ticket) -> FelgensResult<(WsWriteType, WsReadType)> {
let mut con = None;
debug!("ws host list: {:?}", ticket.hosts);
for host in &ticket.hosts {
let url = format!("wss://{host}/sub");
if let Ok((c, _)) = connect_async(&url).await {
con = Some(c);
info!("Connected ws host: {url}");
break;
} else {
warn!("Connect ws host: {url} has error, trying next host ...");
}
}
let con = con.ok_or_else(|| FelgensError::FailedConnectWsHost)?;
let (mut write, mut read) = con.split();
let json = serde_json::to_string(&WsSend {
roomid: ticket.roomid,
key: ticket.token.clone(),
uid: ticket.uid as u32,
})?;
debug!("Websocket sending json: {json}");
let json = pack::encode(&json, 7);
write.send(Message::binary(json)).await?;
check_auth_reply(&mut read).await?;
Ok((write, read))
}
async fn check_auth_reply(read: &mut WsReadType) -> FelgensResult<()> {
let wait = async {
loop {
match read.next().await {
Some(Ok(message)) if message.is_binary() => {
let data = message.into_data();
if let Some(body) = pack::auth_reply_body(&data)? {
return check_auth_code(body);
}
debug!("认证回复之前先收到别的帧,先放掉");
}
Some(Ok(_)) => {}
Some(Err(e)) => return Err(e.into()),
None => return Err(FelgensError::AuthTimeout),
}
}
};
match tokio::time::timeout(AUTH_TIMEOUT, wait).await {
Ok(result) => result,
Err(_) => Err(FelgensError::AuthTimeout),
}
}
fn check_auth_code(body: &str) -> FelgensResult<()> {
#[derive(serde::Deserialize)]
struct Reply {
code: i64,
#[serde(default)]
message: Option<String>,
}
let reply: Reply = serde_json::from_str(body)?;
if reply.code == 0 {
return Ok(());
}
Err(FelgensError::AuthFailed {
code: reply.code,
message: reply.message.unwrap_or_default(),
})
}
async fn send_heartbeat_packets(mut write: WsWriteType) -> FelgensResult<()> {
loop {
write.send(Message::binary(pack::encode("", 2))).await?;
debug!("Heartbeat packets have been sent!");
sleep(Duration::from_secs(30)).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_reply_code_is_checked() {
let frame = pack::encode(r#"{"code":0}"#, 8);
let body = pack::auth_reply_body(&frame).unwrap().unwrap();
assert!(check_auth_code(body).is_ok());
let frame = pack::encode(r#"{"code":-101,"message":"token 过期"}"#, 8);
let body = pack::auth_reply_body(&frame).unwrap().unwrap();
match check_auth_code(body) {
Err(FelgensError::AuthFailed { code, message }) => {
assert_eq!(code, -101);
assert!(message.contains("过期"));
}
other => panic!("应当报 AuthFailed:{other:?}"),
}
let frame = pack::encode(r#"{"cmd":"DANMU_MSG"}"#, 5);
assert!(pack::auth_reply_body(&frame).unwrap().is_none());
}
}