tnnl 0.1.17

tnnl gives you full control over whether and when your IoT devices should be reachable from the internet
//! The main entrypoint when using tnnl as a lib.
//! You can use the builder to configure the tnnl connection.

use std::time::Duration;

use tokio::{
    io::{AsyncRead, AsyncWrite},
    sync::oneshot::Sender,
    time,
};
use tokio_tungstenite::{connect_async, tungstenite::protocol::WebSocketConfig};

use crate::{
    apperror::AppError,
    rules::Rule,
    session::{LoginInfo, Session, start},
};

/// Gets the default hostname of the [tnnl server](https://tnnl.de/).
///
/// ```rust
/// use tnnl::builder::*;
/// let tnnl_server = get_default_hostname();
/// ```
pub fn get_default_hostname() -> &'static str {
    "access.websof.de"
}

/// Gets the url for a given [tnnl server](https://tnnl.de/).
///
/// ```rust
/// use tnnl::builder::*;
/// let tls = true;
/// let port = 443;
/// let hostname = get_default_hostname();
/// let tnnl_server_url = get_url(hostname, tls, Some(port));
///
/// assert_eq!(tnnl_server_url, "wss://access.websof.de:443/api/websocket")
/// ```
pub fn get_url(hostname: &str, tls: bool, port: Option<u16>) -> String {
    let scheme = if tls { "wss://" } else { "ws://" };
    let port = port.unwrap_or(if tls { 443 } else { 80 });
    String::new() + scheme + hostname + ":" + &port.to_string() + "/api/websocket"
}

/// The main entrypoint when using tnnl as a lib.
/// You can use the builder to configure the tnnl connection.
/// ```rust,no_run
/// use tnnl::builder::*;
/// # use tnnl::apperror::*;
/// # #[tokio::main]
/// # async fn main() -> Result<(), AppError> {
/// let builder = Builder::new(&"MY_SECRET_TOKEN");
/// builder.connect().await?;
/// # Ok(())
/// # }
/// ```
pub struct Builder {
    token: String,
    client_name: Option<String>,
    client_id: Option<String>,
    mac: Option<String>,
    rule: Rule,
    tls: bool,
    event_poll_interval: Duration,
    handshake_timeout: Duration,
    on_session: Option<Sender<Session>>,
    websocket_config: WebSocketConfig,
    hostname: String,
    port: Option<u16>,
}

impl Builder {
    /// Creates a new [builder](Builder).
    /// ```rust
    /// use tnnl::builder::*;
    ///
    /// let builder = Builder::new(&"MY_SECRET_TOKEN");
    /// ```
    pub fn new<T: ToString>(token: &T) -> Self {
        Builder {
            token: token.to_string(),
            client_name: None,
            client_id: None,
            mac: None,
            rule: Rule::unrestricted(),
            tls: true,
            event_poll_interval: Duration::from_secs(1),
            handshake_timeout: Duration::from_secs(60),
            on_session: None,
            websocket_config: WebSocketConfig::default(),
            hostname: get_default_hostname().to_string(),
            port: None,
        }
    }

    /// Specifies the name of the client application.
    pub fn with_client_name<T: ToString>(mut self, client_name: &T) -> Self {
        self.client_name = Some(client_name.to_string());
        self
    }

    /// Specifies the name of the client application.
    pub fn with_client_name_opt(mut self, client_name: &Option<String>) -> Self {
        self.client_name = client_name.clone();
        self
    }

    /// The client id must be unique but should be reused when reconnectig.
    /// This can be used to outdate non-gracefull terminated previous connections of the same device.
    pub fn with_client_id<T: ToString>(mut self, client_id: &T) -> Self {
        self.client_id = Some(client_id.to_string());
        self
    }

    /// The client id must be unique but should be reused when reconnectig
    /// This can be used to outdate non-gracefull terminated previous connections of the same device.
    pub fn with_client_id_opt(mut self, client_id: &Option<String>) -> Self {
        self.client_id = client_id.clone();
        self
    }

    /// Configures the [rule](Rule) for incoming connections to the tnnl client.
    pub fn with_rule(mut self, rule: Rule) -> Self {
        self.rule = rule;
        self
    }

    /// Configures whether tls should be used for the connection to the tnnl server.
    pub fn with_tls(mut self, tls: bool) -> Self {
        self.tls = tls;
        self
    }

    /// Configures the mac address that should be sent to the tnnl server for this connection.
    pub fn with_mac<T: ToString>(mut self, mac: &T) -> Self {
        self.mac = Some(mac.to_string());
        self
    }

    /// Configures an interval that will be used to poll the tnnl eventloop.
    /// Usually not required but can help with problems on ESP.
    pub fn with_event_poll_interval(mut self, event_poll_interval: Duration) -> Self {
        self.event_poll_interval = event_poll_interval;
        self
    }

    /// Configures the timeout for the WebSocket handshake.
    /// Usually not required but can help with problems on ESP.
    pub fn with_handshake_timeout(mut self, handshake_timeout: Duration) -> Self {
        self.handshake_timeout = handshake_timeout;
        self
    }

    /// Configures a sender that will be triggered when a tnnl session is established.
    pub fn with_on_session(mut self, on_session: Sender<Session>) -> Self {
        self.on_session = Some(on_session);
        self
    }

    /// Configures the max write buffer size for the websocket.
    pub fn with_write_buffer_size(mut self, write_buffer_size: usize) -> Self {
        self.websocket_config = self.websocket_config.write_buffer_size(write_buffer_size);
        self
    }

    /// Configures the read buffer size for the websocket.
    pub fn with_read_buffer_size(mut self, read_buffer_size: usize) -> Self {
        self.websocket_config = self.websocket_config.read_buffer_size(read_buffer_size);
        self
    }

    /// Configures the max message size for the websocket.
    pub fn with_max_message_size(mut self, max_message_size: usize) -> Self {
        self.websocket_config = self
            .websocket_config
            .max_message_size(Some(max_message_size));
        self
    }

    /// Configures the max framesize of the websocket frames.
    pub fn with_max_frame_size(mut self, max_frame_size: usize) -> Self {
        self.websocket_config = self.websocket_config.max_frame_size(Some(max_frame_size));
        self
    }

    /// Configures the hostname of the tnnl server that is to be used.
    pub fn with_hostname(mut self, hostname: &str) -> Self {
        self.hostname = hostname.to_string();
        self
    }

    /// Configures the port of the tnnl server that is to be used.
    pub fn with_port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }

    /// Returns the url that is currently configured in the [builder](Builder).
    pub fn get_url(&self) -> String {
        get_url(&self.hostname, self.tls, self.port)
    }

    /// Starts the tnnl client that is configured through the [builder](Builder) using an already established connection.
    /// This can be usefull when using a custom tls implementation.
    pub async fn start_connected<S: AsyncRead + AsyncWrite + Unpin>(
        self,
        stream: S,
    ) -> Result<(), AppError> {
        let (socket, _) = time::timeout(
            self.handshake_timeout,
            tokio_tungstenite::client_async_with_config(
                get_url(&self.hostname, self.tls, self.port),
                stream,
                Some(self.websocket_config),
            ),
        )
        .await
        .map_err(|_| AppError::new("timeout while handshaking"))?
        .map_err(|_| AppError::new("failed to create client async"))?;

        start(
            LoginInfo::new(self.token, self.client_name, self.client_id, self.mac),
            self.rule,
            socket,
            self.event_poll_interval,
            self.on_session,
        )
        .await
    }

    /// Connects the tnnl client that is configured through the [builder](Builder)
    /// ```rust,no_run
    /// # use tnnl::builder::*;
    /// # #[tokio::main]
    /// # async fn main() {
    /// let builder = Builder::new(&"MY_SECRET_TOKEN");
    /// builder.connect().await;
    /// # }
    /// ```
    pub async fn connect(self) -> Result<(), AppError> {
        let (socket, _) = connect_async(&self.get_url())
            .await
            .map_err(|e| AppError::new(format!("Can't connect: {e}")))?;
        start(
            LoginInfo::new(self.token, self.client_name, self.client_id, self.mac),
            self.rule,
            socket,
            self.event_poll_interval,
            self.on_session,
        )
        .await
    }
}