use crate::{
client::{telegram::APIServer, Bot},
errors::{SessionErrorKind, TelegramErrorKind},
methods::{Response, TelegramMethod},
};
use serde::de::DeserializeOwned;
use std::{
fmt::{self, Display, Formatter},
future::Future,
ops::RangeInclusive,
};
use tracing::{debug_span, event, instrument, Level, Span};
pub const DEFAULT_TIMEOUT: f32 = 60.0;
#[derive(Debug)]
pub struct StatusCode(u16);
impl StatusCode {
const SUCCESS_STATUS_CODE_RANGE: RangeInclusive<u16> = 200..=226;
#[must_use]
pub fn new(status_code: u16) -> Self {
Self(status_code)
}
#[must_use]
pub fn is_success(&self) -> bool {
Self::SUCCESS_STATUS_CODE_RANGE.contains(&self.0)
}
#[must_use]
pub fn is_error(&self) -> bool {
!self.is_success()
}
#[must_use]
pub const fn as_u16(&self) -> u16 {
self.0
}
}
impl Display for StatusCode {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl PartialEq<u16> for StatusCode {
fn eq(&self, other: &u16) -> bool {
self.0 == *other
}
}
impl From<u16> for StatusCode {
fn from(status_code: u16) -> Self {
Self::new(status_code)
}
}
#[derive(Debug)]
pub struct ClientResponse {
pub status_code: StatusCode,
pub content: Box<str>,
}
impl ClientResponse {
#[must_use]
pub fn new(status_code: impl Into<StatusCode>, content: impl Into<Box<str>>) -> Self {
Self {
status_code: status_code.into(),
content: content.into(),
}
}
}
pub trait Session: Send + Sync {
#[must_use]
fn api(&self) -> &APIServer;
#[must_use]
fn send_request<Client, T>(
&self,
bot: &Bot<Client>,
method: T,
timeout: Option<f32>,
) -> impl Future<Output = Result<ClientResponse, anyhow::Error>> + Send
where
Client: Session,
T: TelegramMethod + Send + Sync,
T::Method: Send + Sync;
#[allow(clippy::redundant_else)]
#[instrument(name = "check", skip_all, fields(ok, error_message))]
fn check_response(
&self,
response: &Response<impl DeserializeOwned>,
status_code: StatusCode,
) -> Result<(), TelegramErrorKind> {
if status_code.is_success() && response.ok {
Span::current().record("ok", true);
if response.result.is_none() {
event!(
Level::ERROR,
"Contract violation: result is empty in success response"
);
return Err(anyhow::Error::msg(
"Contract violation: result is empty in success response",
)
.into());
}
return Ok(());
} else {
Span::current().record("ok", false);
}
let Some(message) = response.description.clone() else {
event!(
Level::ERROR,
error_code = ?response.error_code,
parameters = ?response.parameters,
"Contract violation: description is empty in error response",
);
return Err(anyhow::Error::msg(
"Contract violation: description is empty in error response",
)
.into());
};
Span::current().record("error_message", message.as_ref());
if let Some(ref parameters) = response.parameters {
if let Some(retry_after) = parameters.retry_after {
let err = TelegramErrorKind::RetryAfter {
url: "https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this",
message,
retry_after,
};
event!(Level::ERROR, error = %err);
return Err(err);
}
if let Some(migrate_to_chat_id) = parameters.migrate_to_chat_id {
let err = TelegramErrorKind::MigrateToChat {
url: "https://core.telegram.org/bots/api#responseparameters",
message,
migrate_to_chat_id,
};
event!(Level::ERROR, error = %err);
return Err(err);
}
}
let err = match status_code.as_u16() {
400 => TelegramErrorKind::BadRequest {
message,
},
401 => TelegramErrorKind::Unauthorized {
message,
},
403 => TelegramErrorKind::Forbidden {
message,
},
404 => TelegramErrorKind::NotFound {
message,
},
409 => TelegramErrorKind::ConflictError {
message,
},
413 => TelegramErrorKind::EntityTooLarge {
url: "https://core.telegram.org/bots/api#sending-files",
message,
},
500 => {
if message.contains("restart") {
TelegramErrorKind::RestartingTelegram {
message,
}
} else {
TelegramErrorKind::ServerError {
message,
}
}
}
_ => {
event!(
Level::ERROR,
%status_code,
message,
"Error with unknown status code",
);
return Err(anyhow::Error::msg(message).into());
}
};
event!(Level::ERROR, error = %err);
Err(err)
}
fn make_request<Client, T>(
&self,
bot: &Bot<Client>,
method: T,
timeout: Option<f32>,
) -> impl Future<Output = Result<Response<T::Return>, SessionErrorKind>> + Send
where
Client: Session,
T: TelegramMethod + Send + Sync,
T::Method: Send + Sync,
{
async move {
let response = self.send_request(bot, method, timeout).await?;
debug_span!("response", status_code = response.status_code.as_u16()).in_scope(|| {
let resp = T::build_response(&response.content)?;
self.check_response(&resp, response.status_code)?;
Ok(resp)
})
}
}
fn make_request_and_get_result<Client, T>(
&self,
bot: &Bot<Client>,
method: T,
timeout: Option<f32>,
) -> impl Future<Output = Result<T::Return, SessionErrorKind>> + Send
where
Client: Session,
T: TelegramMethod + Send + Sync,
T::Method: Send + Sync,
{
async move {
let resp = self.make_request(bot, method, timeout).await?;
Ok(resp.result.unwrap())
}
}
fn close(&self) -> impl Future<Output = Result<(), anyhow::Error>> + Send {
async move { Ok(()) }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::methods::SendMessage;
use serde_json::json;
#[test]
fn build_response() {
let content = json!(
{
"ok": true,
"result": {
"date": 1,
"message_id": 1,
"text": "test",
"from": {
"id": 1i64,
"is_bot": true,
"first_name": "test",
"username": "test"
},
"chat":{
"id": 1,
"first_name": "test",
"last_name": "test",
"username": "test",
"type": "private",
},
"date": 1,
"reply_to_message": {
"message_id": 2,
"text": "/start",
"from": {
"id": 1,
"is_bot": false,
"first_name": "test",
"username": "test",
},
"chat":{
"id": 1,
"first_name": "test",
"last_name": "test",
"username": "test",
"type": "private",
},
"date": 1,
"entities":[
{
"offset": 0,
"length": 6,
"type": "bot_command",
},
],
},
},
"statud_code": 200,
});
let result = SendMessage::build_response(&content.to_string())
.unwrap()
.result
.unwrap();
assert_eq!(result.message_id(), 1);
}
}