All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			
		
			
				
	
	
		
			231 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
use crate::cli_args::cli_args;
 | 
						|
use crate::server;
 | 
						|
use futures::stream::{SplitSink, SplitStream};
 | 
						|
use futures::{SinkExt, StreamExt};
 | 
						|
use hyper_rustls::ConfigBuilderExt;
 | 
						|
use sea_battle_backend::data::*;
 | 
						|
use sea_battle_backend::human_player_ws::{ClientMessage, ServerMessage};
 | 
						|
use sea_battle_backend::server::{
 | 
						|
    AcceptInviteQuery, BotPlayQuery, CreateInviteQuery, PlayRandomQuery,
 | 
						|
};
 | 
						|
use sea_battle_backend::utils::res_utils::{boxed_error, Res};
 | 
						|
use std::error::Error;
 | 
						|
use std::fmt::Display;
 | 
						|
use std::sync::mpsc::TryRecvError;
 | 
						|
use std::sync::{mpsc, Arc};
 | 
						|
use tokio::net::TcpStream;
 | 
						|
use tokio_tungstenite::tungstenite::Message;
 | 
						|
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
 | 
						|
 | 
						|
type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
 | 
						|
 | 
						|
pub enum GetRemoteVersionError {
 | 
						|
    ConnectionFailed,
 | 
						|
    Other(Box<dyn Error>),
 | 
						|
}
 | 
						|
 | 
						|
/// Connection client
 | 
						|
///
 | 
						|
/// This structure acts as a wrapper around websocket connection that handles automatically parsing
 | 
						|
/// of incoming messages and encoding of outgoing messages
 | 
						|
pub struct Client {
 | 
						|
    sink: SplitSink<WsStream, Message>,
 | 
						|
    receiver: mpsc::Receiver<ServerMessage>,
 | 
						|
}
 | 
						|
 | 
						|
impl Client {
 | 
						|
    /// Get remote server version
 | 
						|
    pub async fn get_server_version() -> Result<VersionInfo, GetRemoteVersionError> {
 | 
						|
        let url = format!("{}/version", cli_args().remote_server);
 | 
						|
        log::debug!("Getting remote information from {} ...", url);
 | 
						|
 | 
						|
        let res = match reqwest::get(url).await {
 | 
						|
            Ok(r) => r,
 | 
						|
            Err(e) if e.is_timeout() || e.is_connect() => {
 | 
						|
                return Err(GetRemoteVersionError::ConnectionFailed)
 | 
						|
            }
 | 
						|
            Err(e) => return Err(GetRemoteVersionError::Other(Box::new(e))),
 | 
						|
        };
 | 
						|
 | 
						|
        res.json()
 | 
						|
            .await
 | 
						|
            .map_err(|e| GetRemoteVersionError::Other(Box::new(e)))
 | 
						|
    }
 | 
						|
 | 
						|
    /// Start to play against a bot
 | 
						|
    ///
 | 
						|
    /// When playing against a bot, local server is always used
 | 
						|
    pub async fn start_bot_play(rules: &GameRules) -> Res<Self> {
 | 
						|
        server::start_server_if_missing().await;
 | 
						|
 | 
						|
        Self::connect_url(
 | 
						|
            &cli_args().local_server_address(),
 | 
						|
            &format!(
 | 
						|
                "/play/bot?{}",
 | 
						|
                serde_urlencoded::to_string(BotPlayQuery {
 | 
						|
                    rules: rules.clone(),
 | 
						|
                    player_name: "Human".to_string()
 | 
						|
                })
 | 
						|
                .unwrap()
 | 
						|
            ),
 | 
						|
        )
 | 
						|
        .await
 | 
						|
    }
 | 
						|
 | 
						|
    /// Start to play against a random player
 | 
						|
    pub async fn start_random_play<D: Display>(player_name: D) -> Res<Self> {
 | 
						|
        Self::connect_url(
 | 
						|
            &cli_args().remote_server,
 | 
						|
            &format!(
 | 
						|
                "/play/random?{}",
 | 
						|
                serde_urlencoded::to_string(PlayRandomQuery {
 | 
						|
                    player_name: player_name.to_string()
 | 
						|
                })
 | 
						|
                .unwrap()
 | 
						|
            ),
 | 
						|
        )
 | 
						|
        .await
 | 
						|
    }
 | 
						|
 | 
						|
    /// Start a play by creating an invite
 | 
						|
    pub async fn start_create_invite<D: Display>(rules: &GameRules, player_name: D) -> Res<Self> {
 | 
						|
        Self::connect_url(
 | 
						|
            &cli_args().remote_server,
 | 
						|
            &format!(
 | 
						|
                "/play/create_invite?{}",
 | 
						|
                serde_urlencoded::to_string(CreateInviteQuery {
 | 
						|
                    rules: rules.clone(),
 | 
						|
                    player_name: player_name.to_string()
 | 
						|
                })
 | 
						|
                .unwrap()
 | 
						|
            ),
 | 
						|
        )
 | 
						|
        .await
 | 
						|
    }
 | 
						|
 | 
						|
    /// Start a play by accepting an invite
 | 
						|
    pub async fn start_accept_invite<D: Display>(code: String, player_name: D) -> Res<Self> {
 | 
						|
        Self::connect_url(
 | 
						|
            &cli_args().remote_server,
 | 
						|
            &format!(
 | 
						|
                "/play/accept_invite?{}",
 | 
						|
                serde_urlencoded::to_string(AcceptInviteQuery {
 | 
						|
                    code,
 | 
						|
                    player_name: player_name.to_string()
 | 
						|
                })
 | 
						|
                .unwrap()
 | 
						|
            ),
 | 
						|
        )
 | 
						|
        .await
 | 
						|
    }
 | 
						|
 | 
						|
    /// Do connect to a server, returning
 | 
						|
    async fn connect_url(server: &str, uri: &str) -> Res<Self> {
 | 
						|
        let mut ws_url = server.replace("http", "ws");
 | 
						|
        ws_url.push_str(uri);
 | 
						|
        log::debug!("Connecting to {}", ws_url);
 | 
						|
 | 
						|
        let (socket, _) = if ws_url.starts_with("wss") {
 | 
						|
            // Perform a connection over TLS
 | 
						|
            let config = rustls::ClientConfig::builder()
 | 
						|
                .with_safe_defaults()
 | 
						|
                .with_native_roots()
 | 
						|
                .with_no_client_auth();
 | 
						|
            let connector = tokio_tungstenite::Connector::Rustls(Arc::new(config));
 | 
						|
 | 
						|
            tokio_tungstenite::connect_async_tls_with_config(ws_url, None, false,Some(connector)).await?
 | 
						|
        } else {
 | 
						|
            // Perform an unsecure connection
 | 
						|
            tokio_tungstenite::connect_async(ws_url).await?
 | 
						|
        };
 | 
						|
 | 
						|
        let (sink, mut stream) = socket.split();
 | 
						|
 | 
						|
        // Receive server message on a separate task
 | 
						|
        let (sender, receiver) = mpsc::channel();
 | 
						|
        tokio::task::spawn(async move {
 | 
						|
            loop {
 | 
						|
                match Self::recv_next_msg(&mut stream).await {
 | 
						|
                    Ok(msg) => {
 | 
						|
                        if let Err(e) = sender.send(msg.clone()) {
 | 
						|
                            log::debug!("Failed to forward ws message! {} (msg={:?})", e, msg);
 | 
						|
                            break;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    Err(e) => {
 | 
						|
                        log::debug!("Failed receive next message from websocket! {}", e);
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        Ok(Self { sink, receiver })
 | 
						|
    }
 | 
						|
 | 
						|
    /// Receive next message from stream
 | 
						|
    async fn recv_next_msg(stream: &mut SplitStream<WsStream>) -> Res<ServerMessage> {
 | 
						|
        loop {
 | 
						|
            let chunk = match stream.next().await {
 | 
						|
                None => return Err(boxed_error("No more message in queue!")),
 | 
						|
                Some(d) => d,
 | 
						|
            };
 | 
						|
 | 
						|
            match chunk? {
 | 
						|
                Message::Text(t) => {
 | 
						|
                    log::debug!("TEXT Got a text message from server!");
 | 
						|
                    let msg: ServerMessage = serde_json::from_str(&t)?;
 | 
						|
                    return Ok(msg);
 | 
						|
                }
 | 
						|
                Message::Binary(_) => {
 | 
						|
                    log::debug!("BINARY Got an unexpected binary message");
 | 
						|
                    return Err(boxed_error("Received an unexpected binary message!"));
 | 
						|
                }
 | 
						|
                Message::Ping(_) => {
 | 
						|
                    log::debug!("PING Got a ping message from server");
 | 
						|
                }
 | 
						|
                Message::Pong(_) => {
 | 
						|
                    log::debug!("PONG Got a pong message");
 | 
						|
                }
 | 
						|
                Message::Close(_) => {
 | 
						|
                    log::debug!("CLOSE Got a close websocket message");
 | 
						|
                    return Err(boxed_error("Server requested to close connection!"));
 | 
						|
                }
 | 
						|
                Message::Frame(_) => {
 | 
						|
                    log::debug!("FRAME Got an unexpected frame from server!");
 | 
						|
                    return Err(boxed_error("Got an unexpected frame!"));
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// Send a message through the stream
 | 
						|
    pub async fn send_message(&mut self, msg: &ClientMessage) -> Res {
 | 
						|
        self.sink
 | 
						|
            .send(Message::Text(serde_json::to_string(&msg)?))
 | 
						|
            .await?;
 | 
						|
        Ok(())
 | 
						|
    }
 | 
						|
 | 
						|
    /// Try to receive next message from websocket, in a non-blocking way
 | 
						|
    pub async fn try_recv_next_message(&self) -> Res<Option<ServerMessage>> {
 | 
						|
        match self.receiver.try_recv() {
 | 
						|
            Ok(msg) => Ok(Some(msg)),
 | 
						|
            Err(TryRecvError::Empty) => Ok(None),
 | 
						|
            Err(TryRecvError::Disconnected) => Err(boxed_error("Receiver channel disconnected!")),
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// Block until the next message from websocket is availabl
 | 
						|
    pub async fn recv_next_message(&self) -> Res<ServerMessage> {
 | 
						|
        Ok(self.receiver.recv()?)
 | 
						|
    }
 | 
						|
 | 
						|
    /// Close connection
 | 
						|
    pub async fn close_connection(&mut self) {
 | 
						|
        if let Err(e) = self.sink.send(Message::Close(None)).await {
 | 
						|
            log::debug!("Failed to close WS connection! {:?}", e);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |