frakti 0.1.0

Telegram bot API client for Rust, with a focus on single-threaded async runtime support
Documentation
use std::{fmt::Debug, path::PathBuf};

use bon::Builder;
use compio::fs::File;
use cyper::{
    multipart::{Form, Part},
    Client, Error as CyperError, Response,
};
use serde::{de::DeserializeOwned, Serialize};

use super::{json::encode_object, trait_async::AsyncTelegramApi, Error, BASE_API_URL};

/// Asynchronous [`AsyncTelegramApi`] implementation with [`cyper`]
#[derive(Debug, Clone, Builder)]
#[must_use = "Bot needs to be used in order to be useful"]
pub struct Bot {
    #[builder(into)]
    pub api_url: String,

    #[builder(default = Client::new())]
    pub client: Client,
}

impl Bot {
    /// Create a new `Bot`. You can use [`Bot::new_url`] or [`Bot::builder`] for more options.
    pub fn new(api_key: &str) -> Self {
        Self::new_url(format!("{BASE_API_URL}{api_key}"))
    }

    /// Create a new `Bot`. You can use [`Bot::builder`] for more options.
    pub fn new_url<S: Into<String>>(api_url: S) -> Self {
        Self::builder().api_url(api_url).build()
    }

    async fn decode_response<Output>(response: Response) -> Result<Output, Error>
    where
        Output: DeserializeOwned,
    {
        let success = response.status().is_success();
        if success {
            Ok(response.json().await?)
        } else {
            Err(Error::Api(response.json().await?))
        }
    }
}

impl From<CyperError> for Error {
    fn from(error: CyperError) -> Self {
        Self::HttpCyper(error)
    }
}

impl AsyncTelegramApi for Bot {
    type Error = Error;

    async fn request<Params, Output>(
        &self,
        method: &str,
        params: Option<Params>,
    ) -> Result<Output, Self::Error>
    where
        Params: Serialize + Debug + Send,
        Output: DeserializeOwned,
    {
        let url = format!("{}/{method}", self.api_url);
        let mut prepared_request = self
            .client
            .post(url)?
            .header("Content-Type", "application/json")?;
        if let Some(params) = params {
            prepared_request = prepared_request.json(&params)?;
        }
        let response = prepared_request.send().await?;
        Self::decode_response(response).await
    }

    async fn request_with_form_data<Params, Output>(
        &self,
        method: &str,
        params: Params,
        files: Vec<(&str, PathBuf)>,
    ) -> Result<Output, Self::Error>
    where
        Params: Serialize + Debug + Send,
        Output: DeserializeOwned,
    {
        let json_struct = encode_object(&params)?;
        let file_keys: Vec<&str> = files.iter().map(|(key, _)| *key).collect();

        let mut form = Form::new();
        for (key, val) in json_struct {
            if !file_keys.contains(&key.as_str()) {
                form = match val {
                    serde_json::Value::String(val) => form.text(key, val),
                    other => form.text(key, other.to_string()),
                };
            }
        }

        for (parameter_name, file_path) in files {
            let file = File::open(&file_path).await.map_err(Error::ReadFile)?;
            let file_name = file_path.file_name().unwrap().to_string_lossy().to_string();
            let part = Part::stream(file).file_name(file_name);
            form = form.part(parameter_name.to_owned(), part);
        }

        let url = format!("{}/{method}", self.api_url);

        let response = self.client.post(url)?.multipart(form)?.send().await?;
        Self::decode_response(response).await
    }
}