use crate::adapter::OneBotAdapter;
use futures_util::{SinkExt, StreamExt};
use nagisa_core::ShutdownToken;
use nagisa_types::error::Result;
use nagisa_types::event::Event;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response};
use tokio_tungstenite::tungstenite::http::StatusCode;
use tokio_tungstenite::tungstenite::Message as WsMessage;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ClientRole {
Universal,
Event,
Api,
}
impl ClientRole {
fn parse(raw: Option<&str>) -> Option<Self> {
match raw {
None => Some(ClientRole::Universal),
Some(v) => match v.trim().to_ascii_lowercase().as_str() {
"universal" => Some(ClientRole::Universal),
"event" => Some(ClientRole::Event),
"api" => Some(ClientRole::Api),
_ => None,
},
}
}
fn carries_actions(self) -> bool {
matches!(self, ClientRole::Universal | ClientRole::Api)
}
fn carries_events(self) -> bool {
matches!(self, ClientRole::Universal | ClientRole::Event)
}
}
pub async fn run_reverse_ws(
adapter: Arc<OneBotAdapter>,
bind: SocketAddr,
path: String,
sink: mpsc::Sender<Event>,
shutdown: ShutdownToken,
) -> Result<()> {
let listener = TcpListener::bind(bind).await.map_err(|e| {
nagisa_types::error::Error::Transport(nagisa_types::error::TransportError::WebSocket(e.to_string()))
})?;
tracing::info!(%bind, %path, "onebot reverse-ws listening");
loop {
tokio::select! {
biased;
_ = shutdown.cancelled() => return Ok(()),
accepted = listener.accept() => {
let (stream, peer) = match accepted {
Ok(v) => v,
Err(e) => {
tracing::warn!(error = %e, "reverse-ws accept failed");
continue;
}
};
let adapter = Arc::clone(&adapter);
let sink = sink.clone();
let shutdown = shutdown.clone();
let want_path = path.clone();
tokio::spawn(async move {
if let Err(e) = serve_conn(adapter, stream, peer, want_path, sink, shutdown).await {
tracing::debug!(error = %e, "reverse-ws connection ended");
}
});
}
}
}
}
#[allow(clippy::result_large_err)]
async fn serve_conn(
adapter: Arc<OneBotAdapter>,
stream: tokio::net::TcpStream,
peer: SocketAddr,
want_path: String,
sink: mpsc::Sender<Event>,
shutdown: ShutdownToken,
) -> Result<()> {
let want_token = adapter_token(&adapter);
let handshake_meta = std::sync::Mutex::new((ClientRole::Universal, None::<String>));
let callback = |req: &Request, resp: Response| -> std::result::Result<Response, ErrorResponse> {
if req.uri().path() != want_path {
return Err(err_response(StatusCode::NOT_FOUND));
}
if let Some(want) = &want_token {
let header_ok = req
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.map(|v| v == format!("Bearer {want}"))
.unwrap_or(false);
let query_ok =
req.uri().query().map(|q| q.split('&').any(|kv| kv == format!("access_token={want}"))).unwrap_or(false);
if !header_ok && !query_ok {
return Err(err_response(StatusCode::UNAUTHORIZED));
}
}
let role_raw = req.headers().get("X-Client-Role").and_then(|v| v.to_str().ok());
let role = match ClientRole::parse(role_raw) {
Some(role) => role,
None => return Err(err_response(StatusCode::BAD_REQUEST)),
};
let self_id = req.headers().get("X-Self-ID").and_then(|v| v.to_str().ok()).map(str::to_string);
*handshake_meta.lock().expect("handshake_meta poisoned") = (role, self_id);
Ok(resp)
};
let ws = tokio_tungstenite::accept_hdr_async(stream, callback).await.map_err(|e| {
nagisa_types::error::Error::Transport(nagisa_types::error::TransportError::WebSocket(e.to_string()))
})?;
let (role, self_id) = handshake_meta.into_inner().expect("handshake_meta poisoned");
tracing::info!(
%peer,
?role,
carries_events = role.carries_events(),
self_id = self_id.as_deref().unwrap_or("-"),
"reverse-ws client connected"
);
let carries_events = role.carries_events();
if carries_events {
let _ = sink.try_send(nagisa_types::event::Event::Meta(nagisa_types::event::Meta::Connect));
}
let (mut write, mut read) = ws.split();
let installed_action_route = role.carries_actions();
let (out_tx, mut out_rx) = mpsc::channel::<String>(256);
let _idle_out_tx_guard = if installed_action_route {
*adapter.server_outbound.lock().expect("server_outbound poisoned") = Some(out_tx);
None
} else {
Some(out_tx)
};
let res = loop {
tokio::select! {
biased;
_ = shutdown.cancelled() => {
let _ = write.close().await;
break Ok(());
}
maybe_frame = out_rx.recv() => {
match maybe_frame {
Some(frame) => {
if write.send(WsMessage::text(frame)).await.is_err() {
break Ok(());
}
}
None => break Ok(()),
}
}
maybe_msg = read.next() => {
match maybe_msg {
Some(Ok(WsMessage::Text(txt))) => {
if adapter.handle_inbound_public(txt.as_str(), &sink).await {
break Ok(());
}
}
Some(Ok(WsMessage::Binary(bin))) => {
if let Ok(txt) = std::str::from_utf8(&bin) {
if adapter.handle_inbound_public(txt, &sink).await {
break Ok(());
}
}
}
Some(Ok(WsMessage::Ping(p))) => { let _ = write.send(WsMessage::Pong(p)).await; }
Some(Ok(WsMessage::Close(_))) | None => break Ok(()),
Some(Ok(_)) => {}
Some(Err(e)) => {
tracing::warn!(error = %e, "reverse-ws read error");
break Ok(());
}
}
}
}
};
if installed_action_route {
*adapter.server_outbound.lock().expect("server_outbound poisoned") = None;
adapter.clear_pending_public();
}
if carries_events {
let _ = sink.try_send(nagisa_types::event::Event::Meta(nagisa_types::event::Meta::Disconnect { reason: None }));
}
res
}
fn adapter_token(adapter: &OneBotAdapter) -> Option<String> {
adapter.access_token().map(str::to_string)
}
fn err_response(code: StatusCode) -> ErrorResponse {
let mut resp = ErrorResponse::new(None);
*resp.status_mut() = code;
resp
}