rosu_render/websocket/
mod.rs1#![cfg(any(
2 feature = "native-tls",
3 feature = "rustls-native-roots",
4 feature = "rustls-webpki-roots"
5))]
6
7use bytes::Bytes;
8
9use crate::WebsocketError;
10
11use self::{
12 engineio::{
13 packet::{Packet as EnginePacket, PacketId as EnginePacketId},
14 EngineIo,
15 },
16 event::RawEvent,
17 packet::{Packet, PacketKind},
18 reconnect::Reconnect,
19};
20
21mod engineio;
22mod packet;
23mod reconnect;
24
25pub mod error;
26pub mod event;
27
28pub struct OrdrWebsocket {
37 engineio: EngineIo,
38 reconnect: Reconnect,
39}
40
41impl OrdrWebsocket {
42 pub async fn connect() -> Result<Self, WebsocketError> {
44 let engineio = EngineIo::connect().await?;
45
46 let mut this = Self {
47 engineio,
48 reconnect: Reconnect::default(),
49 };
50
51 this.open().await?;
52
53 Ok(this)
54 }
55
56 pub async fn next_event(&mut self) -> Result<RawEvent, WebsocketError> {
62 loop {
63 let Some(bytes) = self.engineio.next_message().await? else {
64 self.reconnect().await?;
65
66 continue;
67 };
68
69 let packet = Packet::from_bytes(&bytes)?;
70
71 match packet.kind {
72 PacketKind::Event => {}
73 PacketKind::Ack => self.ack(&packet).await?,
74 PacketKind::Connect => continue,
75 PacketKind::Disconnect | PacketKind::ConnectError => {
76 self.reconnect().await?;
77
78 continue;
79 }
80 }
81
82 if let Some(data) = packet.data {
83 return RawEvent::from_bytes(data);
84 }
85 }
86 }
87
88 #[expect(clippy::missing_panics_doc, reason = "serializing a &str cannot fail")]
98 pub async fn authenticate(&mut self, key: &str) -> Result<(), WebsocketError> {
99 let payload = serde_json::to_string(&["bot_auth", key])
100 .expect("a &str always serializes to a JSON string");
101
102 self.emit(Packet::new_event(Bytes::from(payload))).await?;
103
104 loop {
105 let Some(bytes) = self.engineio.next_message().await? else {
106 self.reconnect().await?;
107
108 continue;
109 };
110
111 let packet = Packet::from_bytes(&bytes)?;
112
113 match packet.kind {
114 PacketKind::Event => {
115 let Some(data) = packet.data else {
118 continue;
119 };
120
121 let Some(message) = bot_auth_message(&data) else {
122 continue;
123 };
124
125 if message.starts_with("Authentication successful") {
126 return Ok(());
127 }
128
129 return Err(WebsocketError::BotAuth { message });
130 }
131 PacketKind::Ack => self.ack(&packet).await?,
132 PacketKind::Connect => {}
133 PacketKind::Disconnect | PacketKind::ConnectError => self.reconnect().await?,
134 }
135 }
136 }
137
138 pub async fn disconnect(self) -> Result<(), WebsocketError> {
140 self.engineio
141 .disconnect()
142 .await
143 .map_err(WebsocketError::EngineIo)
144 }
145
146 async fn reconnect(&mut self) -> Result<(), WebsocketError> {
147 if let Some(delay) = self.reconnect.delay() {
148 trace!(?delay, "Delaying reconnect...");
149 tokio::time::sleep(delay).await;
150 }
151
152 let err = match self.engineio.reconnect().await {
153 Ok(()) => match self.open().await {
154 Ok(()) => {
155 self.reconnect.reset();
156
157 return Ok(());
158 }
159 Err(err) => err,
160 },
161 Err(err) => WebsocketError::EngineIo(err),
162 };
163
164 self.reconnect.backoff();
165
166 Err(err)
167 }
168
169 async fn emit(&mut self, packet: Packet) -> Result<(), WebsocketError> {
170 let msg = EnginePacket::new(EnginePacketId::Message, packet.to_bytes());
171
172 self.engineio
173 .emit(msg)
174 .await
175 .map_err(WebsocketError::EngineIo)
176 }
177
178 async fn open(&mut self) -> Result<(), WebsocketError> {
179 self.emit(Packet::new(PacketKind::Connect, None)).await
180 }
181
182 async fn ack(&mut self, packet: &Packet) -> Result<(), WebsocketError> {
183 let Some(id) = packet.id else { return Ok(()) };
184
185 self.emit(Packet::new_ack(id)).await
186 }
187}
188
189fn bot_auth_message(data: &[u8]) -> Option<Box<str>> {
194 let (name, message) = serde_json::from_slice::<(String, Box<str>)>(data).ok()?;
195
196 (name == "bot_auth").then_some(message)
197}
198
199#[cfg(test)]
200mod tests {
201 use bytes::Bytes;
202
203 use super::{bot_auth_message, Packet};
204
205 #[test]
206 fn bot_auth_emit_frame() {
207 let payload = serde_json::to_string(&["bot_auth", "secret-key"]).unwrap();
208
209 let frame = Packet::new_event(Bytes::from(payload)).to_bytes();
210
211 assert_eq!(frame.as_ref(), &br#"2["bot_auth","secret-key"]"#[..]);
212 }
213
214 #[test]
215 fn bot_auth_reply_frame() {
216 let frame = br#"2["bot_auth","Authentication successful for bathbot"]"#;
217
218 let packet = Packet::from_bytes(&Bytes::from_static(frame)).unwrap();
219 let data = packet.data.unwrap();
220
221 assert_eq!(
222 bot_auth_message(&data),
223 Some("Authentication successful for bathbot".into()),
224 );
225 }
226
227 #[test]
228 fn bot_auth_failure_message_is_captured() {
229 assert_eq!(
230 bot_auth_message(br#"["bot_auth","Invalid API key"]"#),
231 Some("Invalid API key".into()),
232 );
233 }
234
235 #[test]
236 fn bot_auth_ignores_other_events() {
237 let data = br#"["render_done_json",{"renderID":1}]"#;
238
239 assert_eq!(bot_auth_message(data), None);
240 }
241}