#![cfg(any(
feature = "native-tls",
feature = "rustls-native-roots",
feature = "rustls-webpki-roots"
))]
use bytes::Bytes;
use crate::WebsocketError;
use self::{
engineio::{
packet::{Packet as EnginePacket, PacketId as EnginePacketId},
EngineIo,
},
event::RawEvent,
packet::{Packet, PacketKind},
reconnect::Reconnect,
};
mod engineio;
mod packet;
mod reconnect;
pub mod error;
pub mod event;
pub struct OrdrWebsocket {
engineio: EngineIo,
reconnect: Reconnect,
auth: Option<Box<str>>,
}
impl OrdrWebsocket {
pub async fn connect() -> Result<Self, WebsocketError> {
let engineio = EngineIo::connect().await?;
let mut this = Self {
engineio,
reconnect: Reconnect::default(),
auth: None,
};
this.open().await?;
Ok(this)
}
pub async fn next_event(&mut self) -> Result<RawEvent, WebsocketError> {
loop {
let Some(bytes) = self.engineio.next_message().await? else {
self.reconnect().await?;
continue;
};
let packet = Packet::from_bytes(&bytes)?;
match packet.kind {
PacketKind::Event => {}
PacketKind::Ack => self.ack(&packet).await?,
PacketKind::Connect => continue,
PacketKind::Disconnect | PacketKind::ConnectError => {
self.reconnect().await?;
continue;
}
}
if let Some(data) = packet.data {
if data.starts_with(b"[\"bot_auth\"") {
continue;
}
return RawEvent::from_bytes(data);
}
}
}
pub async fn authenticate(&mut self, key: &str) -> Result<(), WebsocketError> {
self.auth = Some(key.into());
self.emit_auth(key).await?;
loop {
let Some(bytes) = self.engineio.next_message().await? else {
self.reconnect().await?;
continue;
};
let packet = Packet::from_bytes(&bytes)?;
match packet.kind {
PacketKind::Event => {
let Some(data) = packet.data else {
continue;
};
let Some(message) = bot_auth_message(&data) else {
continue;
};
if message.starts_with("Authentication successful") {
return Ok(());
}
return Err(WebsocketError::BotAuth { message });
}
PacketKind::Ack => self.ack(&packet).await?,
PacketKind::Connect => {}
PacketKind::Disconnect | PacketKind::ConnectError => self.reconnect().await?,
}
}
}
pub async fn disconnect(self) -> Result<(), WebsocketError> {
self.engineio
.disconnect()
.await
.map_err(WebsocketError::EngineIo)
}
async fn reconnect(&mut self) -> Result<(), WebsocketError> {
if let Some(delay) = self.reconnect.delay() {
trace!(?delay, "Delaying reconnect...");
tokio::time::sleep(delay).await;
}
let err = match self.engineio.reconnect().await {
Ok(()) => match self.open().await {
Ok(()) => {
self.reconnect.reset();
match self.auth.clone() {
Some(key) => match self.emit_auth(&key).await {
Ok(()) => return Ok(()),
Err(err) => err,
},
None => return Ok(()),
}
}
Err(err) => err,
},
Err(err) => WebsocketError::EngineIo(err),
};
self.reconnect.backoff();
Err(err)
}
async fn emit_auth(&mut self, key: &str) -> Result<(), WebsocketError> {
let payload = serde_json::to_string(&["bot_auth", key])
.expect("a &str always serializes to a JSON string");
self.emit(Packet::new_event(Bytes::from(payload))).await
}
async fn emit(&mut self, packet: Packet) -> Result<(), WebsocketError> {
let msg = EnginePacket::new(EnginePacketId::Message, packet.to_bytes());
self.engineio
.emit(msg)
.await
.map_err(WebsocketError::EngineIo)
}
async fn open(&mut self) -> Result<(), WebsocketError> {
self.emit(Packet::new(PacketKind::Connect, None)).await
}
async fn ack(&mut self, packet: &Packet) -> Result<(), WebsocketError> {
let Some(id) = packet.id else { return Ok(()) };
self.emit(Packet::new_ack(id)).await
}
}
fn bot_auth_message(data: &[u8]) -> Option<Box<str>> {
let (name, message) = serde_json::from_slice::<(String, Box<str>)>(data).ok()?;
(name == "bot_auth").then_some(message)
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use super::{bot_auth_message, Packet};
#[test]
fn bot_auth_emit_frame() {
let payload = serde_json::to_string(&["bot_auth", "secret-key"]).unwrap();
let frame = Packet::new_event(Bytes::from(payload)).to_bytes();
assert_eq!(frame.as_ref(), &br#"2["bot_auth","secret-key"]"#[..]);
}
#[test]
fn bot_auth_reply_frame() {
let frame = br#"2["bot_auth","Authentication successful for bathbot"]"#;
let packet = Packet::from_bytes(&Bytes::from_static(frame)).unwrap();
let data = packet.data.unwrap();
assert_eq!(
bot_auth_message(&data),
Some("Authentication successful for bathbot".into()),
);
}
#[test]
fn bot_auth_failure_message_is_captured() {
assert_eq!(
bot_auth_message(br#"["bot_auth","Invalid API key"]"#),
Some("Invalid API key".into()),
);
}
#[test]
fn bot_auth_ignores_other_events() {
let data = br#"["render_done_json",{"renderID":1}]"#;
assert_eq!(bot_auth_message(data), None);
}
}