use reqwest::header::HeaderMap;
use reqwest::header::HeaderValue;
use reqwest::multipart;
use reqwest::Client;
use std::fs;
use std::io::Read;
use std::{sync::Arc, time::Duration};
use crate::{
errors::{ErrorResult, TelegramErrorResult},
types::{RequestObj, SendMessageOption, StatusCode},
utils,
};
pub const TELEGRAM_API_URL: &str = "https://api.telegram.org";
const SEND_MESSAGE_METHOD: &str = "sendMessage";
const SEND_MEDIA_METHOD: &str = "sendMediaGroup";
#[must_use]
#[derive(Debug, Clone)]
pub struct Bot {
pub token: Arc<str>,
pub chat_id: Arc<str>,
pub api_url: Arc<reqwest::Url>,
pub client: Client,
}
impl Bot {
pub fn new<S>(token: S, chat_id: S) -> Self
where
S: Into<String>,
{
let client = default_reqwest_settings()
.build()
.expect("Client creation failed");
Self::with_client(token, chat_id, client)
}
pub fn with_client<S>(token: S, chat_id: S, client: Client) -> Self
where
S: Into<String>,
{
let token = Into::<String>::into(token).into();
let chat_id = Into::<String>::into(chat_id).into();
let api_url = Arc::new(
reqwest::Url::parse(TELEGRAM_API_URL)
.expect("Failed to parse default Telegram bot API url"),
);
Self {
token,
chat_id,
api_url,
client,
}
}
}
impl Bot {
pub async fn send_message(
&self,
msg: &str,
options: Option<SendMessageOption>,
) -> Result<(), ErrorResult> {
let request_json_obj = self.build_request_obj(msg, options);
let response = self
.client
.post(method_url(
self.api_url(),
self.token(),
SEND_MESSAGE_METHOD,
))
.json(&request_json_obj)
.send()
.await;
match response {
Ok(res) => {
if res.status().is_success() {
Ok(())
} else {
match res.json::<TelegramErrorResult>().await {
Ok(err_result) => {
return utils::create_error_result_str(
StatusCode::ErrorInternalError,
&err_result.description.to_owned(),
)
}
Err(_) => {
return utils::create_error_result_str(
StatusCode::ErrorInternalError,
"Error converting telegram error response to json",
)
}
}
}
}
Err(e) => {
return utils::create_error_result_str(
StatusCode::ErrorInternalError,
&format!("Error sending HTTP request; err={}", e),
)
}
}
}
pub async fn send_csv(&self, filepath: &str, caption: &str) -> Result<(), ErrorResult> {
let mut file = fs::File::open(filepath)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
let file_name = std::path::Path::new(filepath)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("file.csv");
let part = multipart::Part::bytes(contents)
.file_name(file_name.to_string())
.mime_str("text/csv")?;
let media = serde_json::json!([
{
"type": "document",
"media": "attach://file",
"caption": caption
}
]);
let form = multipart::Form::new()
.text("chat_id", self.chat_id.to_string())
.text("media", media.to_string())
.part("file", part);
let response = self
.client
.post(method_url(self.api_url(), self.token(), "sendMediaGroup"))
.header(reqwest::header::CONTENT_TYPE, "multipart/form-data")
.multipart(form)
.send()
.await?;
if response.status().is_success() {
Ok(())
} else {
let err_result =
response
.json::<TelegramErrorResult>()
.await
.map_err(|_| ErrorResult {
code: StatusCode::ErrorInternalError.as_u16(),
msg: "Error converting telegram error response to json".to_string(),
})?;
Err(ErrorResult {
code: StatusCode::ErrorInternalError.as_u16(),
msg: err_result.description.to_owned(),
})
}
}
fn build_request_obj(&self, msg: &str, options: Option<SendMessageOption>) -> RequestObj {
let parse_mode = options
.as_ref()
.and_then(|option| option.parse_mode.as_ref()) .map(|mode| utils::get_send_message_parse_mode_str(mode).to_owned());
RequestObj::new(&self.chat_id, msg, parse_mode)
}
}
impl Bot {
#[must_use]
pub fn token(&self) -> &str {
&self.token
}
#[must_use]
pub fn client(&self) -> &Client {
&self.client
}
#[must_use]
pub fn api_url(&self) -> reqwest::Url {
reqwest::Url::clone(&*self.api_url)
}
}
fn default_reqwest_settings() -> reqwest::ClientBuilder {
reqwest::Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(17))
.tcp_nodelay(true)
}
fn method_url(base: reqwest::Url, token: &str, method_name: &str) -> reqwest::Url {
base.join(&format!("/bot{token}/{method_name}"))
.expect("failed to format url")
}
#[cfg(test)]
mod tests {
use crate::bot::{method_url, SEND_MESSAGE_METHOD, TELEGRAM_API_URL};
#[test]
fn method_url_test() {
let url = method_url(
reqwest::Url::parse(TELEGRAM_API_URL).unwrap(),
"535362388:AAF7-g0gYncWnm5IyfZlpPRqRRv6kNAGlao",
SEND_MESSAGE_METHOD,
);
assert_eq!(
url.as_str(),
format!(
"https://api.telegram.org/bot535362388:AAF7-g0gYncWnm5IyfZlpPRqRRv6kNAGlao/{}",
SEND_MESSAGE_METHOD
)
);
}
}