use std::error::Error; use futures::stream::{SplitSink, SplitStream}; use futures::StreamExt; use log::{debug, error, info, Level, trace, warn}; use wasm_bindgen::prelude::*; use ws_stream_wasm::{WsErr, WsMessage, WsMeta, WsStream}; use protocol::State; use protocol::PROTOCOL_VERSION; use protocol::MessageS2C; use protocol::MessageC2S; use futures::SinkExt; use lazy_static::lazy_static; use std::sync::Arc; use std::sync::RwLock; use std::thread; use std::time::Duration; use async_recursion::async_recursion; use futures::FutureExt; use js_sys::Math::sqrt; use wasm_bindgen_futures::JsFuture; use web_sys::Window; #[macro_use] pub mod macros; pub mod chat; #[wasm_bindgen] extern { pub fn alert(s: &str); } #[wasm_bindgen] pub async fn rust_init(gateway: &str, username: &str) -> Result<(), JsError> { update_status("Starting logger..."); console_log::init_with_level(Level::Debug).unwrap(); info!("Logger setup successfully"); match main(gateway, username, 1).await { Ok(c) => c, Err(e) => { error!("Error initializing gateway client: {}", e); return Err(JsError::new(&e.to_string())); } }; info!("Gateway client set up successfully"); Ok(()) } pub struct Client { pub client_data: Option, } pub struct ClientData { pub state: State, pub tx: SplitSink, pub rx: SplitStream } lazy_static! { pub static ref CLIENT: Arc> = Arc::new(RwLock::new(Client { client_data: None })); } pub const MAX_CONNECTION_TRIES: i32 = 10; #[async_recursion(?Send)] #[allow(clippy::only_used_in_recursion)] pub async fn main(gateway: &str, username: &str, backoff: i32) -> Result<(), Box> { info!("Backing off connection: waiting {} seconds", backoff * backoff); wait_for(sleep(backoff * backoff * 1000)).await; if backoff > MAX_CONNECTION_TRIES { update_status("Connection to server failed"); return Err("Hit backoff limit during reconnection attempt".into()); } if backoff == 1 { update_status("Connecting to server..."); } else { update_status(&format!("Connection failed, retrying... (try {}/10)", backoff)); } info!("FAST CONNECT: {}", gateway); let gateway_url = url::Url::parse(gateway)?; trace!("Gateway URL parsed"); let (_ws, ws_stream) = match WsMeta::connect(gateway_url, None).await { Ok(r) => r, Err(e) => { return match e { WsErr::ConnectionFailed { .. } => { main(gateway, username, backoff + 1).await }, _ => { Err(e.into()) } } } }; trace!("Connected to gateway socket"); let (tx, rx) = ws_stream.split(); let mut client_data = ClientData { state: State::Handshake, tx, rx }; trace!("Split stream, handshaking with server"); update_status("Handshaking with server..."); send!(client_data.tx, &MessageC2S::Hello { next_state: State::Play, version: PROTOCOL_VERSION, requested_username: username.to_string() }).await?; trace!("Sent handshake start packet"); if let Some(msg) = recv_now!(client_data.rx)? { let typed_msg: MessageS2C = msg; match typed_msg { MessageS2C::Hello { version, given_username, next_state } => { info!("FAST CONNECT - connected to server protocol {} given username {}, switching to state {:?}", version, given_username, next_state); client_data.state = next_state; }, MessageS2C::Goodbye { reason } => { error!("server disconnected before finishing handshake: {:?}", reason); return Err(format!("disconnected by server: {:?}", reason).into()); }, _ => { warn!("received unexpected packet from server: {:?}", typed_msg); } } } else { error!("Server closed the connection") } CLIENT.write()?.client_data = Some(client_data); update_status(&format!("Connected! Username: {}", username)); Ok(()) } #[wasm_bindgen] pub async fn update_socket() -> Result<(), JsError> { let mut client = CLIENT.write()?; if client.client_data.is_none() { return Err(JsError::new("Client not yet initialized")); } let client_data = client.client_data.as_mut().unwrap(); let maybe_msg: Option = match recv!(client_data.rx) { Ok(r) => r, Err(e) => { return Err(JsError::new(e)) } }; if let Some(msg) = maybe_msg { match msg { MessageS2C::Goodbye { reason } => { info!("server sent disconnect: {:?}", reason); client.client_data = None; return Err(JsError::new("disconnected by server")); } MessageS2C::Chat { from, message } => { info!("[CHAT] {}: {}", from, message); let window: Window = web_sys::window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); let chatbox = document.get_element_by_id("chats").expect("chatbox does not exist"); let new_elem = document.create_element("div").expect("could not create element"); let msg_formatted = markdown::to_html(&format!("**[{}]** {}", from, message)); new_elem.set_inner_html(&msg_formatted); chatbox.append_child(&new_elem).unwrap(); }, _ => { warn!("server sent unexpected packet {:?}, ignoring", msg); } } } Ok(()) } #[wasm_bindgen] pub fn update_status(new_status: &str) { let window: Window = web_sys::window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); let status = document.get_element_by_id("status").expect("statusbox does not exist"); status.set_inner_html(new_status); } #[wasm_bindgen] pub fn sleep(ms: i32) -> js_sys::Promise { js_sys::Promise::new(&mut |resolve, _| { web_sys::window() .unwrap() .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms) .unwrap(); }) } pub async fn wait_for(promise: js_sys::Promise) -> JsFuture { wasm_bindgen_futures::JsFuture::from(promise) }