#![allow(clippy::too_many_arguments)]
use crate::error::Result;
use crate::request::base::{BaseRequest, TimeoutOverride};
use crate::request::request_data::RequestData;
use crate::request::request_parameter::{InputFileRef, RequestParameter};
use crate::types::files;
use crate::types::link_preview_options;
use crate::types::message;
use crate::types::update;
use crate::types::user;
use crate::types::webhook_info;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::OnceCell;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ChatId {
Id(i64),
Username(String),
}
impl std::fmt::Display for ChatId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChatId::Id(id) => write!(f, "{id}"),
ChatId::Username(u) => write!(f, "{u}"),
}
}
}
impl From<i64> for ChatId {
fn from(id: i64) -> Self {
ChatId::Id(id)
}
}
impl From<String> for ChatId {
fn from(username: String) -> Self {
ChatId::Username(username)
}
}
impl From<&str> for ChatId {
fn from(username: &str) -> Self {
ChatId::Username(username.to_owned())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum MessageOrBool {
Message(Box<message::Message>),
Bool(bool),
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct Defaults {
pub parse_mode: Option<String>,
pub disable_notification: Option<bool>,
pub protect_content: Option<bool>,
pub allow_sending_without_reply: Option<bool>,
pub link_preview_options: Option<link_preview_options::LinkPreviewOptions>,
pub quote: Option<bool>,
}
pub struct Bot {
token: Arc<str>,
base_url: Arc<str>,
base_file_url: Arc<str>,
request: Arc<dyn BaseRequest>,
defaults: Option<Defaults>,
cached_bot_data: Arc<OnceCell<user::User>>,
local_mode: bool,
}
impl std::fmt::Debug for Bot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Bot")
.field("token", &"[REDACTED]")
.field("base_url", &self.base_url)
.field("base_file_url", &self.base_file_url)
.field("defaults", &self.defaults)
.field("local_mode", &self.local_mode)
.finish()
}
}
fn input_file_param(name: &'static str, file: files::input_file::InputFile) -> RequestParameter {
match file {
files::input_file::InputFile::FileId(id) => {
RequestParameter::new(name, serde_json::Value::String(id))
}
files::input_file::InputFile::Url(url) => {
RequestParameter::new(name, serde_json::Value::String(url))
}
files::input_file::InputFile::Bytes { filename, data } => {
let file_ref = InputFileRef {
attach_name: None,
bytes: data,
mime_type: None,
file_name: Some(filename),
};
RequestParameter::file_only(name, file_ref)
}
files::input_file::InputFile::Path(path) => {
let filename = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let path_str = path.to_string_lossy().to_string();
let file_ref = InputFileRef {
attach_name: None,
bytes: Vec::new(),
mime_type: None,
file_name: Some(filename),
};
RequestParameter {
name: std::borrow::Cow::Borrowed(name),
value: Some(serde_json::Value::String(format!(
"__filepath__:{path_str}"
))),
input_files: Some(vec![file_ref]),
}
}
}
}
fn push_opt<T: Serialize>(
params: &mut Vec<RequestParameter>,
name: &'static str,
val: &Option<T>,
) -> std::result::Result<(), serde_json::Error> {
if let Some(v) = val {
params.push(RequestParameter::new(name, serde_json::to_value(v)?));
}
Ok(())
}
fn push_opt_str(params: &mut Vec<RequestParameter>, name: &'static str, val: Option<&str>) {
if let Some(v) = val {
params.push(RequestParameter::new(
name,
serde_json::Value::String(v.to_owned()),
));
}
}
fn push_opt_file(
params: &mut Vec<RequestParameter>,
name: &'static str,
val: Option<files::input_file::InputFile>,
) {
if let Some(f) = val {
params.push(input_file_param(name, f));
}
}
#[allow(dead_code)]
impl Bot {
pub fn new(token: impl Into<String>, request: Arc<dyn BaseRequest>) -> Self {
let token = token.into();
let base_url: Arc<str> = format!("https://api.telegram.org/bot{token}").into();
let base_file_url: Arc<str> = format!("https://api.telegram.org/file/bot{token}").into();
let token: Arc<str> = token.into();
Self {
token,
base_url,
base_file_url,
request,
defaults: None,
cached_bot_data: Arc::new(OnceCell::new()),
local_mode: false,
}
}
pub fn with_options(
token: impl Into<String>,
request: Arc<dyn BaseRequest>,
defaults: Option<Defaults>,
) -> Self {
let token = token.into();
let base_url: Arc<str> = format!("https://api.telegram.org/bot{token}").into();
let base_file_url: Arc<str> = format!("https://api.telegram.org/file/bot{token}").into();
let token: Arc<str> = token.into();
Self {
token,
base_url,
base_file_url,
request,
defaults,
cached_bot_data: Arc::new(OnceCell::new()),
local_mode: false,
}
}
pub fn token(&self) -> &str {
&self.token
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn base_file_url(&self) -> &str {
&self.base_file_url
}
pub fn defaults(&self) -> Option<&Defaults> {
self.defaults.as_ref()
}
pub fn bot_data(&self) -> Option<&user::User> {
self.cached_bot_data.get()
}
pub fn local_mode(&self) -> bool {
self.local_mode
}
#[must_use]
pub fn with_local_mode(mut self) -> Self {
self.local_mode = true;
self
}
fn api_url(&self, method: &str) -> String {
format!("{}/{method}", self.base_url)
}
async fn resolve_file_paths(&self, params: &mut [RequestParameter]) -> Result<()> {
for param in params.iter_mut() {
let path_str = param
.value
.as_ref()
.and_then(|v| v.as_str())
.and_then(|s| s.strip_prefix("__filepath__:"))
.map(str::to_owned);
if let Some(path_str) = path_str {
if self.local_mode {
param.value = Some(serde_json::Value::String(format!("file://{path_str}")));
param.input_files = None;
} else {
let data = tokio::fs::read(&path_str).await?;
param.value = None;
if let Some(ref mut files) = param.input_files {
for f in files.iter_mut() {
if f.bytes.is_empty() {
f.bytes = data.clone();
}
}
}
}
}
}
Ok(())
}
fn apply_defaults(&self, params: &mut Vec<RequestParameter>) {
let defaults = match &self.defaults {
Some(d) => d,
None => return,
};
let existing: std::collections::HashSet<String> =
params.iter().map(|p| p.name.as_ref().to_owned()).collect();
if let Some(ref pm) = defaults.parse_mode {
if !existing.contains("parse_mode") {
params.push(RequestParameter::new(
"parse_mode",
serde_json::Value::String(pm.clone()),
));
}
}
if let Some(dn) = defaults.disable_notification {
if !existing.contains("disable_notification") {
params.push(RequestParameter::new(
"disable_notification",
serde_json::Value::Bool(dn),
));
}
}
if let Some(pc) = defaults.protect_content {
if !existing.contains("protect_content") {
params.push(RequestParameter::new(
"protect_content",
serde_json::Value::Bool(pc),
));
}
}
if let Some(aswr) = defaults.allow_sending_without_reply {
if !existing.contains("allow_sending_without_reply") {
params.push(RequestParameter::new(
"allow_sending_without_reply",
serde_json::Value::Bool(aswr),
));
}
}
if let Some(ref lpo) = defaults.link_preview_options {
if !existing.contains("link_preview_options") {
if let Ok(v) = serde_json::to_value(lpo) {
params.push(RequestParameter::new("link_preview_options", v));
}
}
}
}
async fn do_post<T: serde::de::DeserializeOwned>(
&self,
method: &str,
params: Vec<RequestParameter>,
) -> Result<T> {
self.do_post_inner(method, params, TimeoutOverride::default_none(), None)
.await
}
#[allow(dead_code)]
async fn do_post_with_timeouts<T: serde::de::DeserializeOwned>(
&self,
method: &str,
params: Vec<RequestParameter>,
timeouts: TimeoutOverride,
) -> Result<T> {
self.do_post_inner(method, params, timeouts, None).await
}
fn do_post_inner<'a, T: serde::de::DeserializeOwned + 'a>(
&'a self,
method: &'a str,
mut params: Vec<RequestParameter>,
timeouts: TimeoutOverride,
api_kwargs: Option<HashMap<String, serde_json::Value>>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T>> + Send + 'a>> {
Box::pin(async move {
self.apply_defaults(&mut params);
if let Some(kwargs) = api_kwargs {
let existing: std::collections::HashSet<String> =
params.iter().map(|p| p.name.as_ref().to_owned()).collect();
for (key, value) in kwargs {
if !existing.contains(key.as_str()) {
params.push(RequestParameter::new(key, value));
}
}
}
self.resolve_file_paths(&mut params).await?;
let url = self.api_url(method);
let data = RequestData::from_parameters(params);
let result = self.request.post(&url, Some(&data), timeouts).await?;
serde_json::from_value(result).map_err(Into::into)
})
}
pub(crate) async fn do_post_json<T: serde::de::DeserializeOwned>(
&self,
method: &str,
payload: &[u8],
) -> Result<T> {
let url = self.api_url(method);
let result = self
.request
.post_json(&url, payload, TimeoutOverride::default_none())
.await?;
serde_json::from_value(result).map_err(Into::into)
}
pub async fn download_file_raw(&self, file_path: &str) -> Result<Vec<u8>> {
let url = format!("{}/{file_path}", self.base_file_url);
let bytes = self
.request
.retrieve(&url, TimeoutOverride::default_none())
.await?;
Ok(bytes.to_vec())
}
pub async fn initialize(&mut self) -> Result<()> {
self.request.initialize().await?;
let me = self.get_me_raw().await?;
let _ = self.cached_bot_data.set(me);
Ok(())
}
pub async fn shutdown(&self) -> Result<()> {
self.request.shutdown().await?;
Ok(())
}
pub async fn do_api_request<T: serde::de::DeserializeOwned>(
&self,
method: &str,
params: Vec<RequestParameter>,
) -> Result<T> {
self.do_post(method, params).await
}
pub async fn do_api_request_with_kwargs<T: serde::de::DeserializeOwned>(
&self,
method: &str,
params: Vec<RequestParameter>,
api_kwargs: Option<HashMap<String, serde_json::Value>>,
) -> Result<T> {
self.do_post_inner(method, params, TimeoutOverride::default_none(), api_kwargs)
.await
}
pub async fn get_updates_raw(
&self,
offset: Option<i64>,
limit: Option<i32>,
timeout: Option<i32>,
allowed_updates: Option<Vec<String>>,
) -> Result<Vec<update::Update>> {
let mut params = Vec::new();
push_opt(&mut params, "offset", &offset)?;
push_opt(&mut params, "limit", &limit)?;
push_opt(&mut params, "timeout", &timeout)?;
push_opt(&mut params, "allowed_updates", &allowed_updates)?;
self.apply_defaults(&mut params);
let timeouts = if let Some(t) = timeout {
let effective = Duration::from_secs(t as u64 + 2);
TimeoutOverride {
read: Some(Some(effective)),
..TimeoutOverride::default_none()
}
} else {
TimeoutOverride::default_none()
};
let url = self.api_url("getUpdates");
let data = RequestData::from_parameters(params);
let result = self.request.post(&url, Some(&data), timeouts).await?;
serde_json::from_value(result).map_err(Into::into)
}
pub async fn set_webhook_raw(
&self,
url: &str,
certificate: Option<files::input_file::InputFile>,
ip_address: Option<&str>,
max_connections: Option<i32>,
allowed_updates: Option<Vec<String>>,
drop_pending_updates: Option<bool>,
secret_token: Option<&str>,
) -> Result<bool> {
let mut params = vec![RequestParameter::new(
"url",
serde_json::Value::String(url.to_owned()),
)];
push_opt_file(&mut params, "certificate", certificate);
push_opt_str(&mut params, "ip_address", ip_address);
push_opt(&mut params, "max_connections", &max_connections)?;
push_opt(&mut params, "allowed_updates", &allowed_updates)?;
push_opt(&mut params, "drop_pending_updates", &drop_pending_updates)?;
push_opt_str(&mut params, "secret_token", secret_token);
self.do_post("setWebhook", params).await
}
pub async fn delete_webhook_raw(&self, drop_pending_updates: Option<bool>) -> Result<bool> {
let mut params = Vec::new();
push_opt(&mut params, "drop_pending_updates", &drop_pending_updates)?;
self.do_post("deleteWebhook", params).await
}
pub async fn get_webhook_info_raw(&self) -> Result<webhook_info::WebhookInfo> {
self.do_post("getWebhookInfo", Vec::new()).await
}
pub async fn get_me_raw(&self) -> Result<user::User> {
self.do_post("getMe", Vec::new()).await
}
pub async fn log_out_raw(&self) -> Result<bool> {
self.do_post("logOut", Vec::new()).await
}
pub async fn close_raw(&self) -> Result<bool> {
self.do_post("close", Vec::new()).await
}
}
mod admin;
mod business_methods;
mod chat;
mod editing;
mod forum;
mod games_methods;
mod gifts_methods;
mod inline_methods;
mod keyboard_methods;
mod managed_bots;
mod media;
mod messages;
mod other_content;
mod passport;
mod payments;
mod reactions;
mod stickers;
mod stories;
mod suggested_posts;
mod user_profile;
mod verification;