Skip to main content

rosu_render/websocket/
mod.rs

1#![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
28/// Connection to the o!rdr websocket.
29///
30/// Await events with [`OrdrWebsocket::next_event`].
31///
32/// To gracefully shut the connection down, use [`OrdrWebsocket::disconnect`].
33///
34/// Verified bots that should also receive events about their private and
35/// unlisted renders must call [`OrdrWebsocket::authenticate`] after connecting.
36pub struct OrdrWebsocket {
37    engineio: EngineIo,
38    reconnect: Reconnect,
39}
40
41impl OrdrWebsocket {
42    /// Connect to the o!rdr websocket.
43    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    /// Await the next o!rdr websocket event.
57    ///
58    /// This can block indefinitely when the connection is alive but no events
59    /// arrive, so apply an idle timeout around it if that should count as a
60    /// dead connection.
61    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    /// Authenticate this connection as a verified bot.
89    ///
90    /// Emits the `bot_auth` event with the bot's API key and waits for the
91    /// server's reply on the same event. Once authenticated, events about the
92    /// bot's private and unlisted renders are included in the stream.
93    ///
94    /// Like [`OrdrWebsocket::next_event`], this can block indefinitely while
95    /// the connection is alive. Events that arrive before the authentication
96    /// reply are dropped.
97    #[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                    // The server replies on the same `bot_auth` event, other
116                    // events arriving during authentication are dropped.
117                    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    /// Gracefully disconnect from the websocket.
139    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
189/// The message of a `bot_auth` reply, if the event data is one.
190///
191/// The server answers the `bot_auth` event with
192/// `["bot_auth","Authentication successful for <bot name>"]`.
193fn 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}