use crate::protocol::{SignalEnvelope, SignalPayload};
#[allow(unused_imports)] use futures_util::SinkExt;
use futures_util::StreamExt;
use matchbox_socket::async_trait::async_trait;
use matchbox_socket::{
PeerEvent, PeerId, PeerRequest, PeerSignal, SignalingError, Signaller, SignallerBuilder,
};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::time::Duration;
use uuid::Uuid;
#[cfg(not(target_arch = "wasm32"))]
use async_tungstenite::WebSocketStream;
#[cfg(not(target_arch = "wasm32"))]
use async_tungstenite::async_std::{ConnectStream, connect_async};
#[cfg(not(target_arch = "wasm32"))]
use async_tungstenite::tungstenite;
#[cfg(target_arch = "wasm32")]
use ws_stream_wasm::{WsMessage as WasmWsMessage, WsMeta, WsStream};
const DID_NAMESPACE: Uuid = Uuid::from_bytes([
0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8,
]);
fn session_id_to_peer_id(session_id: &str) -> PeerId {
match Uuid::parse_str(session_id) {
Ok(uuid) => PeerId(uuid),
Err(_) => PeerId(Uuid::new_v5(&DID_NAMESPACE, session_id.as_bytes())),
}
}
#[derive(bevy::prelude::Resource, Clone)]
pub struct TokenSourceRes(pub TokenSource);
pub type TokenSource = Arc<std::sync::RwLock<Option<String>>>;
#[derive(Debug, Clone)]
pub struct SymbiosSignallerBuilder {
access_jwt: Option<String>,
token_source: Option<TokenSource>,
}
impl SymbiosSignallerBuilder {
fn current_token(&self) -> Option<String> {
if let Some(source) = &self.token_source {
source.read().unwrap_or_else(|e| e.into_inner()).clone()
} else {
self.access_jwt.clone()
}
}
}
#[async_trait]
impl SignallerBuilder for SymbiosSignallerBuilder {
async fn new_signaller(
&self,
mut attempts: Option<u16>,
room_url: String,
) -> Result<Box<dyn Signaller>, SignalingError> {
let signaller = 'connect: loop {
let ws = match self.try_connect(&room_url).await {
Ok(stream) => stream,
Err(e) => {
if is_http_client_error(&e) {
return Err(e);
}
if let Some(ref mut remaining) = attempts {
if *remaining <= 1 {
return Err(SignalingError::NegotiationFailed(Box::new(e)));
}
*remaining -= 1;
tracing::warn!(
attempts_remaining = *remaining,
"connection to relay failed, retrying in 3s"
);
futures_timer::Delay::new(Duration::from_secs(3)).await;
continue 'connect;
}
tracing::warn!("connection to relay failed, retrying in 3s");
futures_timer::Delay::new(Duration::from_secs(3)).await;
continue 'connect;
}
};
let mut signaller = SymbiosSignaller {
ws,
local_peer_id: PeerId(Uuid::nil()),
session_to_peer: HashMap::new(),
peer_to_session: HashMap::new(),
pending_events: VecDeque::new(),
};
match signaller.read_welcome().await {
Ok(()) => break signaller,
Err(e) => {
if let Some(ref mut remaining) = attempts {
if *remaining <= 1 {
return Err(e);
}
*remaining -= 1;
tracing::warn!(
attempts_remaining = *remaining,
"welcome handshake failed, retrying in 3s"
);
futures_timer::Delay::new(Duration::from_secs(3)).await;
continue 'connect;
}
tracing::warn!("welcome handshake failed, retrying in 3s");
futures_timer::Delay::new(Duration::from_secs(3)).await;
continue 'connect;
}
}
};
Ok(Box::new(signaller))
}
}
fn is_http_client_error(e: &SignalingError) -> bool {
matches!(e, SignalingError::UserImplementationError(s) if s.starts_with("http_client_error:"))
}
#[cfg(not(target_arch = "wasm32"))]
const WS_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
#[cfg(not(target_arch = "wasm32"))]
impl SymbiosSignallerBuilder {
async fn try_connect(
&self,
room_url: &str,
) -> Result<WebSocketStream<ConnectStream>, SignalingError> {
use futures_util::future::Either;
let token = self.current_token();
let request = build_ws_request(room_url, token.as_deref())
.map_err(|e| SignalingError::UserImplementationError(e.to_string()))?;
let connect_fut = connect_async(request);
let timeout_fut = futures_timer::Delay::new(WS_CONNECT_TIMEOUT);
futures_util::pin_mut!(connect_fut);
futures_util::pin_mut!(timeout_fut);
match futures_util::future::select(connect_fut, timeout_fut).await {
Either::Left((result, _)) => {
let (stream, _) = result.map_err(|e| {
if let tungstenite::Error::Http(ref resp) = e {
let code = resp.status().as_u16();
if resp.status().is_client_error() {
tracing::error!(
status = code,
"relay rejected connection (HTTP 4xx) — not retrying"
);
return SignalingError::UserImplementationError(format!(
"http_client_error:{code}"
));
}
}
SignalingError::from(e)
})?;
Ok(stream)
}
Either::Right(_) => Err(SignalingError::UserImplementationError(
"WebSocket connection timed out".to_string(),
)),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn build_ws_request(
url: &str,
access_jwt: Option<&str>,
) -> Result<tungstenite::http::Request<()>, tungstenite::Error> {
use tungstenite::client::IntoClientRequest;
let mut request = url.into_client_request()?;
if let Some(token) = access_jwt {
let header_value = format!("Bearer {token}")
.parse::<tungstenite::http::HeaderValue>()
.map_err(|e| tungstenite::Error::HttpFormat(e.into()))?;
request.headers_mut().insert("Authorization", header_value);
}
Ok(request)
}
#[cfg(target_arch = "wasm32")]
impl SymbiosSignallerBuilder {
async fn try_connect(&self, room_url: &str) -> Result<WsStream, SignalingError> {
let token = self.current_token();
let protocols: Vec<&str> = match token.as_deref() {
Some(t) => vec!["access_token", t],
None => vec![],
};
let proto_refs: Vec<&str> = protocols.iter().copied().collect();
let (_meta, stream) = WsMeta::connect(room_url, Some(proto_refs.as_slice()))
.await
.map_err(|e| SignalingError::UserImplementationError(e.to_string()))?;
Ok(stream)
}
}
pub struct SymbiosSignaller {
#[cfg(not(target_arch = "wasm32"))]
ws: WebSocketStream<ConnectStream>,
#[cfg(target_arch = "wasm32")]
ws: WsStream,
local_peer_id: PeerId,
session_to_peer: HashMap<String, PeerId>,
peer_to_session: HashMap<PeerId, String>,
pending_events: VecDeque<PeerEvent>,
}
impl SymbiosSignaller {
async fn read_welcome(&mut self) -> Result<(), SignalingError> {
let session_msg = self.read_text().await?;
let session_json: serde_json::Value = serde_json::from_str(&session_msg).map_err(|e| {
SignalingError::UserImplementationError(format!("invalid session_id message: {e}"))
})?;
let session_id = session_json
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| {
SignalingError::UserImplementationError("missing 'id' in session_id message".into())
})?;
self.local_peer_id = session_id_to_peer_id(session_id);
self.track_session(session_id.to_owned(), self.local_peer_id);
self.pending_events
.push_back(PeerEvent::IdAssigned(self.local_peer_id));
let peer_list_msg = self.read_text().await?;
let peer_list_json: serde_json::Value =
serde_json::from_str(&peer_list_msg).map_err(|e| {
SignalingError::UserImplementationError(format!("invalid peer_list message: {e}"))
})?;
if let Some(peers) = peer_list_json.get("peers").and_then(|v| v.as_array()) {
for peer_val in peers {
if let Some(sid) = peer_val.as_str() {
let pid = self.get_or_create_peer_id(sid);
self.pending_events.push_back(PeerEvent::NewPeer(pid));
}
}
}
Ok(())
}
fn get_or_create_peer_id(&mut self, session_id: &str) -> PeerId {
if let Some(&pid) = self.session_to_peer.get(session_id) {
return pid;
}
let pid = session_id_to_peer_id(session_id);
self.track_session(session_id.to_owned(), pid);
pid
}
fn track_session(&mut self, session_id: String, peer_id: PeerId) {
self.session_to_peer.insert(session_id.clone(), peer_id);
self.peer_to_session.insert(peer_id, session_id);
}
fn remove_peer(&mut self, session_id: &str) -> PeerId {
let pid = self
.session_to_peer
.remove(session_id)
.unwrap_or_else(|| session_id_to_peer_id(session_id));
self.peer_to_session.remove(&pid);
pid
}
}
#[cfg(not(target_arch = "wasm32"))]
impl SymbiosSignaller {
async fn read_text(&mut self) -> Result<String, SignalingError> {
loop {
match self.ws.next().await {
Some(Ok(tungstenite::Message::Text(t))) => return Ok(t.to_string()),
Some(Ok(tungstenite::Message::Close(_))) | None => {
return Err(SignalingError::StreamExhausted);
}
Some(Ok(_)) => continue, Some(Err(e)) => return Err(SignalingError::from(e)),
}
}
}
}
#[cfg(target_arch = "wasm32")]
impl SymbiosSignaller {
async fn read_text(&mut self) -> Result<String, SignalingError> {
loop {
match self.ws.next().await {
Some(WasmWsMessage::Text(t)) => return Ok(t),
Some(WasmWsMessage::Binary(_)) => continue,
None => return Err(SignalingError::StreamExhausted),
}
}
}
}
#[async_trait]
impl Signaller for SymbiosSignaller {
async fn send(&mut self, request: PeerRequest) -> Result<(), SignalingError> {
match request {
PeerRequest::Signal { receiver, data } => {
let target_session = match self.peer_to_session.get(&receiver) {
Some(s) => s.clone(),
None => {
tracing::debug!(%receiver, "dropping signal to unknown peer (likely disconnected)");
return Ok(());
}
};
let signal = match data {
PeerSignal::Offer(sdp) => SignalPayload::Offer(sdp),
PeerSignal::Answer(sdp) => SignalPayload::Answer(sdp),
PeerSignal::IceCandidate(c) => SignalPayload::IceCandidate(c),
};
let envelope = SignalEnvelope {
peer_id: target_session,
signal,
};
let json = serde_json::to_string(&envelope)
.map_err(|e| SignalingError::UserImplementationError(e.to_string()))?;
self.send_text(json).await
}
PeerRequest::KeepAlive => self.send_ping().await,
}
}
async fn next_message(&mut self) -> Result<PeerEvent, SignalingError> {
if let Some(event) = self.pending_events.pop_front() {
return Ok(event);
}
loop {
let text = self.read_text().await?;
if let Ok(envelope) = serde_json::from_str::<SignalEnvelope>(&text) {
let sender_id = &envelope.peer_id;
return match envelope.signal {
SignalPayload::Offer(sdp) => {
let pid = self.get_or_create_peer_id(sender_id);
Ok(PeerEvent::Signal {
sender: pid,
data: PeerSignal::Offer(sdp),
})
}
SignalPayload::Answer(sdp) => {
let pid = self.get_or_create_peer_id(sender_id);
Ok(PeerEvent::Signal {
sender: pid,
data: PeerSignal::Answer(sdp),
})
}
SignalPayload::IceCandidate(c) => {
let pid = self.get_or_create_peer_id(sender_id);
Ok(PeerEvent::Signal {
sender: pid,
data: PeerSignal::IceCandidate(c),
})
}
SignalPayload::PeerJoined(ref id) => {
let pid = self.get_or_create_peer_id(id);
Ok(PeerEvent::NewPeer(pid))
}
SignalPayload::PeerLeft(ref id) => {
let pid = self.remove_peer(id);
Ok(PeerEvent::PeerLeft(pid))
}
};
}
tracing::debug!(msg = %text, "ignoring unrecognized relay message");
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl SymbiosSignaller {
async fn send_text(&mut self, text: String) -> Result<(), SignalingError> {
self.ws
.send(tungstenite::Message::Text(text.into()))
.await
.map_err(SignalingError::from)
}
async fn send_ping(&mut self) -> Result<(), SignalingError> {
self.ws
.send(tungstenite::Message::Ping(vec![].into()))
.await
.map_err(SignalingError::from)
}
}
#[cfg(target_arch = "wasm32")]
impl SymbiosSignaller {
async fn send_text(&mut self, text: String) -> Result<(), SignalingError> {
self.ws
.send(WasmWsMessage::Text(text))
.await
.map_err(|e| SignalingError::UserImplementationError(e.to_string()))
}
async fn send_ping(&mut self) -> Result<(), SignalingError> {
Ok(())
}
}
pub fn signaller_for_session(session: &crate::auth::AtprotoSession) -> Arc<dyn SignallerBuilder> {
Arc::new(SymbiosSignallerBuilder {
access_jwt: Some(session.access_jwt.clone()),
token_source: None,
})
}
pub fn signaller_with_token_source(source: TokenSource) -> Arc<dyn SignallerBuilder> {
Arc::new(SymbiosSignallerBuilder {
access_jwt: None,
token_source: Some(source),
})
}
pub fn signaller_anonymous() -> Arc<dyn SignallerBuilder> {
Arc::new(SymbiosSignallerBuilder {
access_jwt: None,
token_source: None,
})
}