1use std::process;
2use std::str::FromStr;
3use futures_util::SinkExt;
4use tokio_websockets::{ClientBuilder, Message};
5use reqwest::Client;
6use serde::{Deserialize, Serialize};
7use http::Uri;
8use std::error::Error;
9use tokio::time::Duration;
10use tokio::time::sleep;
11
12#[derive(Serialize, Deserialize)]
13pub struct LoginRequest {
14 pub username: String,
15 pub password: String,
16 pub hwid: String,
17}
18
19pub struct LicenseAPI {
20 url: String,
21 client: Client,
22 token: String,
23}
24
25impl LicenseAPI {
26 pub fn new(url: impl Into<String>) -> Self {
27 LicenseAPI {
28 url: url.into(),
29 client: Client::new(),
30 token: String::new(),
31 }
32 }
33
34 pub async fn login(
35 &mut self,
36 creds: &LoginRequest,
37 ) -> Result<bool, Box<dyn Error + Send + Sync>> {
38 let login_resp = self
39 .client
40 .post(&format!("{}/auth/login", self.url.trim_end_matches('/')))
41 .json(creds)
42 .send()
43 .await?
44 .error_for_status()?;
45
46 self.token = login_resp
47 .json::<serde_json::Value>()
48 .await?
49 .get("access_token")
50 .and_then(|v| v.as_str().map(String::from))
51 .ok_or("missing access_token in response")?;
52
53 Ok(true)
54 }
55
56 pub async fn connect_to_websocket(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
57 if self.token.is_empty() {
58 return Err("You must login before connecting to the WebSocket.".into());
59 }
60
61 let base_ws = self
62 .url
63 .trim_end_matches('/')
64 .replace("http://", "ws://")
65 .replace("https://", "wss://");
66 let url_str = format!("{}/ws/notify?token={}", base_ws, self.token);
67 let uri = Uri::from_str(&url_str)?;
68
69 let (mut client, _) = ClientBuilder::from_uri(uri).connect().await?;
70
71 loop {
72 sleep(Duration::from_secs(30)).await;
73
74 if let Err(e) = client.send(Message::ping(Vec::new())).await {
75 eprintln!("Ping failed: {}. Exiting.", e);
76 process::exit(1);
77 }
78 }
79 }
80}