Skip to main content

rusty_lcu/
events.rs

1use base64::{Engine, engine::general_purpose::STANDARD};
2use futures_util::{
3    SinkExt, StreamExt,
4    stream::{SplitSink, SplitStream},
5};
6use http::{HeaderValue, Request, header::AUTHORIZATION};
7use serde::{Deserialize, Serialize, de::DeserializeOwned};
8use serde_json::Value;
9use tokio::net::TcpStream;
10use tokio_tungstenite::{
11    Connector, MaybeTlsStream, WebSocketStream, connect_async_tls_with_config, tungstenite::Message,
12};
13
14use crate::{Credentials, Result};
15
16type Socket = WebSocketStream<MaybeTlsStream<TcpStream>>;
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct LcuEvent {
21    pub data: Value,
22    pub event_type: String,
23    pub uri: String,
24}
25
26impl LcuEvent {
27    pub fn data_as<T>(&self) -> Result<T>
28    where
29        T: DeserializeOwned,
30    {
31        Ok(serde_json::from_value(self.data.clone())?)
32    }
33}
34
35#[derive(Debug, Clone, Default, PartialEq, Eq)]
36pub struct EventFilter {
37    pub uri: Option<String>,
38    pub event_types: Vec<String>,
39    pub name: Option<String>,
40}
41
42impl EventFilter {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    pub fn uri(mut self, uri: impl Into<String>) -> Self {
48        self.uri = Some(uri.into());
49        self
50    }
51
52    pub fn event_type(mut self, event_type: impl Into<String>) -> Self {
53        self.event_types.push(event_type.into());
54        self
55    }
56
57    pub fn name(mut self, name: impl Into<String>) -> Self {
58        self.name = Some(name.into());
59        self
60    }
61
62    pub fn matches(&self, event: &LcuEvent) -> bool {
63        if self.uri.as_deref().is_some_and(|uri| uri != event.uri) {
64            return false;
65        }
66
67        if !self.event_types.is_empty()
68            && !self
69                .event_types
70                .iter()
71                .any(|event_type| event_type == &event.event_type)
72        {
73            return false;
74        }
75
76        if let Some(name) = &self.name {
77            let event_name = event
78                .data
79                .get("name")
80                .and_then(Value::as_str)
81                .or_else(|| event.data.get("eventName").and_then(Value::as_str));
82
83            if event_name != Some(name.as_str()) {
84                return false;
85            }
86        }
87
88        true
89    }
90}
91
92pub struct EventStream {
93    write: SplitSink<Socket, Message>,
94    read: SplitStream<Socket>,
95}
96
97impl EventStream {
98    pub async fn connect(credentials: &Credentials) -> Result<Self> {
99        let authorization = STANDARD.encode(format!("riot:{}", credentials.password));
100        let request = Request::builder()
101            .uri(credentials.websocket_url())
102            .header(
103                AUTHORIZATION,
104                HeaderValue::from_str(&format!("Basic {authorization}"))?,
105            )
106            .body(())?;
107
108        let connector = native_tls::TlsConnector::builder()
109            .danger_accept_invalid_certs(true)
110            .build()
111            .map_err(|error| std::io::Error::other(error.to_string()))?;
112
113        let (socket, _) = connect_async_tls_with_config(
114            request,
115            None,
116            false,
117            Some(Connector::NativeTls(connector)),
118        )
119        .await?;
120
121        let (write, read) = socket.split();
122        Ok(Self { write, read })
123    }
124
125    pub async fn subscribe_all(&mut self) -> Result<()> {
126        self.subscribe("OnJsonApiEvent").await
127    }
128
129    pub async fn subscribe(&mut self, event_name: &str) -> Result<()> {
130        self.send_subscription_message(5, event_name).await
131    }
132
133    pub async fn unsubscribe(&mut self, event_name: &str) -> Result<()> {
134        self.send_subscription_message(6, event_name).await
135    }
136
137    pub async fn next_event(&mut self) -> Result<Option<LcuEvent>> {
138        while let Some(message) = self.read.next().await {
139            let message = message?;
140            if !message.is_text() {
141                continue;
142            }
143
144            let payload: Value = serde_json::from_str(message.to_text()?)?;
145            let Some(event) = payload.get(2) else {
146                continue;
147            };
148
149            return Ok(Some(serde_json::from_value(event.clone())?));
150        }
151
152        Ok(None)
153    }
154
155    pub async fn next_matching_event(&mut self, filter: &EventFilter) -> Result<Option<LcuEvent>> {
156        while let Some(event) = self.next_event().await? {
157            if filter.matches(&event) {
158                return Ok(Some(event));
159            }
160        }
161
162        Ok(None)
163    }
164
165    pub async fn next_matching_event_as<T>(&mut self, filter: &EventFilter) -> Result<Option<T>>
166    where
167        T: DeserializeOwned,
168    {
169        Ok(self
170            .next_matching_event(filter)
171            .await?
172            .map(|event| event.data_as())
173            .transpose()?)
174    }
175
176    async fn send_subscription_message(&mut self, opcode: u8, event_name: &str) -> Result<()> {
177        self.write
178            .send(Message::Text(
179                serde_json::json!([opcode, event_name]).to_string().into(),
180            ))
181            .await?;
182        Ok(())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use serde::Deserialize;
189    use serde_json::json;
190
191    use super::*;
192
193    #[test]
194    fn event_filter_matches_uri_type_and_name() {
195        let event = LcuEvent {
196            data: json!({ "name": "OnJsonApiEvent" }),
197            event_type: "Update".to_string(),
198            uri: "/lol-gameflow/v1/gameflow-phase".to_string(),
199        };
200
201        let filter = EventFilter::new()
202            .uri("/lol-gameflow/v1/gameflow-phase")
203            .event_type("Update")
204            .name("OnJsonApiEvent");
205
206        assert!(filter.matches(&event));
207        assert!(!EventFilter::new().event_type("Delete").matches(&event));
208        assert!(!EventFilter::new().uri("/other").matches(&event));
209    }
210
211    #[test]
212    fn event_data_deserializes_to_typed_payload() {
213        #[derive(Debug, Deserialize, PartialEq)]
214        struct PhasePayload {
215            phase: String,
216        }
217
218        let event = LcuEvent {
219            data: json!({ "phase": "ReadyCheck" }),
220            event_type: "Update".to_string(),
221            uri: "/lol-gameflow/v1/gameflow-phase".to_string(),
222        };
223
224        let payload: PhasePayload = event.data_as().unwrap();
225        assert_eq!(
226            payload,
227            PhasePayload {
228                phase: "ReadyCheck".to_string()
229            }
230        );
231    }
232}