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}