use std::sync::{Mutex, MutexGuard, PoisonError};
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use rand::distr::{Alphanumeric, SampleString};
use teloxide::types::{
FileId, FileUniqueId, InlineKeyboardMarkup, Message, ReplyMarkup, ReplyParameters, User,
};
use crate::state::State;
pub const DEFAULT_TEXT_CHAT_ID: i64 = 123456789;
pub const FILE_ID_LENGTH: usize = 16;
pub const FILE_UNIQUE_ID_LENGTH: usize = 8;
pub const DEFAULT_MEDIA_DIMENSION: u32 = 100;
pub const DEFAULT_MEDIA_DURATION_SECS: u32 = 1;
pub const DEFAULT_VIDEO_MIME_TYPE: &str = "video/mp4";
pub const DEFAULT_AUDIO_MIME_TYPE: &str = "audio/mp3";
#[derive(Debug, Clone)]
pub struct RouteError {
status: StatusCode,
body: String,
}
impl RouteError {
pub fn new(response: HttpResponse) -> Self {
Self {
status: response.status(),
body: r#"{"ok":false,"description":"Unknown error"}"#.to_string(),
}
}
#[allow(dead_code)]
pub fn with_status_and_body(status: StatusCode, body: String) -> Self {
Self { status, body }
}
pub fn bad_request(message: &str) -> Self {
Self {
status: StatusCode::BAD_REQUEST,
body: format!(r#"{{"ok":false,"description":"{}"}}"#, message),
}
}
pub fn internal_error(message: &str) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
body: format!(r#"{{"ok":false,"description":"{}"}}"#, message),
}
}
pub fn from_api_error(error: teloxide::ApiError) -> Self {
Self {
status: StatusCode::BAD_REQUEST,
body: format!(r#"{{"ok":false,"description":"{}"}}"#, error),
}
}
}
impl std::fmt::Display for RouteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Route error: {}", self.body)
}
}
impl ResponseError for RouteError {
fn status_code(&self) -> StatusCode {
self.status
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status)
.content_type("application/json")
.body(self.body.clone())
}
}
pub type RouteResult = Result<HttpResponse, RouteError>;
pub fn lock_state(state: &Mutex<State>) -> Result<MutexGuard<'_, State>, RouteError> {
state.lock().map_err(|_: PoisonError<_>| {
log::error!("State mutex was poisoned");
RouteError::internal_error("Internal server error: state lock poisoned")
})
}
pub fn generate_file_id() -> FileId {
FileId(Alphanumeric.sample_string(&mut rand::rng(), FILE_ID_LENGTH))
}
pub fn generate_file_unique_id() -> FileUniqueId {
FileUniqueId(Alphanumeric.sample_string(&mut rand::rng(), FILE_UNIQUE_ID_LENGTH))
}
pub fn generate_file_ids() -> (FileId, FileUniqueId) {
(generate_file_id(), generate_file_unique_id())
}
pub fn extract_inline_keyboard(markup: Option<&ReplyMarkup>) -> Option<InlineKeyboardMarkup> {
match markup {
Some(ReplyMarkup::InlineKeyboard(kb)) => Some(kb.clone()),
_ => None,
}
}
pub fn setup_reply_to_message(
lock: &MutexGuard<'_, State>,
reply_parameters: Option<&ReplyParameters>,
) -> Result<Option<Box<Message>>, RouteError> {
if let Some(params) = reply_parameters {
let message_id = params.message_id.0;
match lock.messages.get_message(message_id) {
Some(msg) => Ok(Some(Box::new(msg))),
None => Err(RouteError::bad_request(
"Bad Request: message to reply not found",
)),
}
} else {
Ok(None)
}
}
#[allow(dead_code)]
pub fn register_file(
lock: &mut MutexGuard<'_, State>,
file_meta: teloxide::types::FileMeta,
path: String,
) {
lock.files.push(teloxide::types::File {
meta: file_meta,
path,
});
}
pub struct MessageSetup {
pub from: Option<User>,
pub has_protected_content: bool,
pub reply_to_message: Option<Box<Message>>,
pub reply_markup: Option<InlineKeyboardMarkup>,
}
impl MessageSetup {
pub fn new(
me_user: &User,
protect_content: Option<bool>,
reply_parameters: Option<&ReplyParameters>,
reply_markup: Option<&ReplyMarkup>,
state_lock: &MutexGuard<'_, State>,
) -> Result<Self, RouteError> {
let reply_to_message = setup_reply_to_message(state_lock, reply_parameters)?;
Ok(Self {
from: Some(me_user.clone()),
has_protected_content: protect_content.unwrap_or(false),
reply_to_message,
reply_markup: extract_inline_keyboard(reply_markup),
})
}
}