create custom type to return all errors

This commit is contained in:
Akulij 2025-04-28 22:43:18 +03:00
parent 1c9ab867ed
commit ffe2fd8bc1

View File

@ -11,10 +11,11 @@ use crate::mongodb_storage::MongodbStorage;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use chrono_tz::Asia; use chrono_tz::Asia;
use db::DbError;
use envconfig::Envconfig; use envconfig::Envconfig;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use teloxide::dispatching::dialogue::serializer::Json; use teloxide::dispatching::dialogue::serializer::Json;
use teloxide::dispatching::dialogue::GetChatId; use teloxide::dispatching::dialogue::{GetChatId, Serializer};
use teloxide::types::{ use teloxide::types::{
InlineKeyboardButton, InlineKeyboardMarkup, InputFile, InputMedia, MediaKind, MessageKind, InlineKeyboardButton, InlineKeyboardMarkup, InputFile, InputMedia, MediaKind, MessageKind,
ParseMode, ReplyMarkup, ParseMode, ReplyMarkup,
@ -82,6 +83,21 @@ impl BotController {
} }
} }
#[derive(thiserror::Error, Debug)]
pub enum BotError {
DBError(#[from] DbError),
TeloxideError(#[from] teloxide::RequestError),
StorageError(#[from] mongodb_storage::MongodbStorageError<<Json as Serializer<State>>::Error>),
}
pub type BotResult<T> = Result<T, BotError>;
impl std::fmt::Display for BotError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv()?; dotenvy::dotenv()?;
@ -91,6 +107,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let state_mgr = MongodbStorage::open(config.db_url.clone().as_ref(), "gongbot", Json).await?; let state_mgr = MongodbStorage::open(config.db_url.clone().as_ref(), "gongbot", Json).await?;
// TODO: delete this in production // TODO: delete this in production
// allow because values are hardcoded and if they will be unparsable
// we should panic anyway
#[allow(clippy::unwrap_used)]
let events: Vec<DateTime<Utc>> = ["2025-04-09T18:00:00+04:00", "2025-04-11T16:00:00+04:00"] let events: Vec<DateTime<Utc>> = ["2025-04-09T18:00:00+04:00", "2025-04-11T16:00:00+04:00"]
.iter() .iter()
.map(|d| DateTime::parse_from_rfc3339(d).unwrap().into()) .map(|d| DateTime::parse_from_rfc3339(d).unwrap().into())
@ -113,11 +132,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.branch( .branch(
Update::filter_message() Update::filter_message()
.filter_async(async |msg: Message, mut db: DB| { .filter_async(async |msg: Message, mut db: DB| {
let tguser = msg.from.unwrap(); let tguser = match msg.from.clone() {
Some(user) => user,
None => return false, // do nothing, cause its not usecase of function
};
let user = db let user = db
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name) .get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
.await; .await;
user.is_admin user.map(|u| u.is_admin).unwrap_or(false)
}) })
.enter_dialogue::<Message, MongodbStorage<Json>, State>() .enter_dialogue::<Message, MongodbStorage<Json>, State>()
.branch( .branch(
@ -148,11 +170,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
async fn callback_handler( async fn callback_handler(bot: Bot, mut db: DB, q: CallbackQuery) -> BotResult<()> {
bot: Bot,
mut db: DB,
q: CallbackQuery,
) -> Result<(), teloxide::RequestError> {
bot.answer_callback_query(&q.id).await?; bot.answer_callback_query(&q.id).await?;
if let Some(ref data) = q.data { if let Some(ref data) = q.data {
@ -179,16 +197,12 @@ async fn edit_msg_cmd_handler(
mut db: DB, mut db: DB,
dialogue: BotDialogue, dialogue: BotDialogue,
msg: Message, msg: Message,
) -> Result<(), teloxide::RequestError> { ) -> BotResult<()> {
match msg.reply_to_message() { match msg.reply_to_message() {
Some(replied) => { Some(replied) => {
let msgid = replied.id; let msgid = replied.id;
// look for message in db and set text // look for message in db and set text
let literal = match db let literal = match db.get_message_literal(msg.chat.id.0, msgid.0).await? {
.get_message_literal(msg.chat.id.0, msgid.0)
.await
.unwrap()
{
Some(l) => l, Some(l) => l,
None => { None => {
bot.send_message(msg.chat.id, "No such message found to edit. Look if you replying bot's message and this message is supposed to be editable").await?; bot.send_message(msg.chat.id, "No such message found to edit. Look if you replying bot's message and this message is supposed to be editable").await?;
@ -203,8 +217,7 @@ async fn edit_msg_cmd_handler(
lang, lang,
is_caption_set: false, is_caption_set: false,
}) })
.await .await?;
.unwrap();
bot.send_message( bot.send_message(
msg.chat.id, msg.chat.id,
"Ok, now you have to send message text (formatting supported)", "Ok, now you have to send message text (formatting supported)",
@ -225,7 +238,7 @@ async fn edit_msg_handler(
dialogue: BotDialogue, dialogue: BotDialogue,
(literal, lang, is_caption_set): (String, String, bool), (literal, lang, is_caption_set): (String, String, bool),
msg: Message, msg: Message,
) -> Result<(), teloxide::RequestError> { ) -> BotResult<()> {
use teloxide::utils::render::Renderer; use teloxide::utils::render::Renderer;
let chat_id = msg.chat.id; let chat_id = msg.chat.id;
@ -239,31 +252,30 @@ async fn edit_msg_handler(
match msg.media_kind { match msg.media_kind {
MediaKind::Text(text) => { MediaKind::Text(text) => {
db.drop_media(&literal).await.unwrap(); db.drop_media(&literal).await?;
if is_caption_set { if is_caption_set {
return Ok(()); return Ok(());
}; };
let html_text = Renderer::new(&text.text, &text.entities).as_html(); let html_text = Renderer::new(&text.text, &text.entities).as_html();
db.set_literal(&literal, &html_text).await.unwrap(); db.set_literal(&literal, &html_text).await?;
bot.send_message(chat_id, "Updated text of message!") bot.send_message(chat_id, "Updated text of message!")
.await?; .await?;
dialogue.exit().await.unwrap(); dialogue.exit().await?;
} }
MediaKind::Photo(photo) => { MediaKind::Photo(photo) => {
let group = photo.media_group_id; let group = photo.media_group_id;
if let Some(group) = group.clone() { if let Some(group) = group.clone() {
db.drop_media_except(&literal, &group).await.unwrap(); db.drop_media_except(&literal, &group).await?;
} else { } else {
db.drop_media(&literal).await.unwrap(); db.drop_media(&literal).await?;
} }
let file_id = photo.photo[0].file.id.clone(); let file_id = photo.photo[0].file.id.clone();
db.add_media(&literal, "photo", &file_id, group.as_deref()) db.add_media(&literal, "photo", &file_id, group.as_deref())
.await .await?;
.unwrap();
match photo.caption { match photo.caption {
Some(text) => { Some(text) => {
let html_text = Renderer::new(&text, &photo.caption_entities).as_html(); let html_text = Renderer::new(&text, &photo.caption_entities).as_html();
db.set_literal(&literal, &html_text).await.unwrap(); db.set_literal(&literal, &html_text).await?;
bot.send_message(chat_id, "Updated photo caption!").await?; bot.send_message(chat_id, "Updated photo caption!").await?;
} }
None => { None => {
@ -272,10 +284,9 @@ async fn edit_msg_handler(
// set text empty // set text empty
if !db if !db
.is_media_group_exists(group.as_deref().unwrap_or("")) .is_media_group_exists(group.as_deref().unwrap_or(""))
.await .await?
.unwrap()
{ {
db.set_literal(&literal, "").await.unwrap(); db.set_literal(&literal, "").await?;
bot.send_message(chat_id, "Set photo without caption") bot.send_message(chat_id, "Set photo without caption")
.await?; .await?;
}; };
@ -293,8 +304,7 @@ async fn edit_msg_handler(
lang, lang,
is_caption_set: true, is_caption_set: true,
}) })
.await .await?;
.unwrap();
tokio::spawn(async move { tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(200)).await; tokio::time::sleep(Duration::from_millis(200)).await;
dialogue.exit().await.unwrap_or(()); dialogue.exit().await.unwrap_or(());
@ -303,18 +313,17 @@ async fn edit_msg_handler(
MediaKind::Video(video) => { MediaKind::Video(video) => {
let group = video.media_group_id; let group = video.media_group_id;
if let Some(group) = group.clone() { if let Some(group) = group.clone() {
db.drop_media_except(&literal, &group).await.unwrap(); db.drop_media_except(&literal, &group).await?;
} else { } else {
db.drop_media(&literal).await.unwrap(); db.drop_media(&literal).await?;
} }
let file_id = video.video.file.id; let file_id = video.video.file.id;
db.add_media(&literal, "video", &file_id, group.as_deref()) db.add_media(&literal, "video", &file_id, group.as_deref())
.await .await?;
.unwrap();
match video.caption { match video.caption {
Some(text) => { Some(text) => {
let html_text = Renderer::new(&text, &video.caption_entities).as_html(); let html_text = Renderer::new(&text, &video.caption_entities).as_html();
db.set_literal(&literal, &html_text).await.unwrap(); db.set_literal(&literal, &html_text).await?;
bot.send_message(chat_id, "Updated video caption!").await?; bot.send_message(chat_id, "Updated video caption!").await?;
} }
None => { None => {
@ -323,10 +332,9 @@ async fn edit_msg_handler(
// set text empty // set text empty
if !db if !db
.is_media_group_exists(group.as_deref().unwrap_or("")) .is_media_group_exists(group.as_deref().unwrap_or(""))
.await .await?
.unwrap()
{ {
db.set_literal(&literal, "").await.unwrap(); db.set_literal(&literal, "").await?;
bot.send_message(chat_id, "Set video without caption") bot.send_message(chat_id, "Set video without caption")
.await?; .await?;
}; };
@ -344,8 +352,7 @@ async fn edit_msg_handler(
lang, lang,
is_caption_set: true, is_caption_set: true,
}) })
.await .await?;
.unwrap();
tokio::spawn(async move { tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(200)).await; tokio::time::sleep(Duration::from_millis(200)).await;
dialogue.exit().await.unwrap_or(()); dialogue.exit().await.unwrap_or(());
@ -362,12 +369,7 @@ async fn edit_msg_handler(
fn command_handler( fn command_handler(
config: Config, config: Config,
) -> Handler< ) -> Handler<'static, DependencyMap, BotResult<()>, teloxide::dispatching::DpHandlerDescription> {
'static,
DependencyMap,
Result<(), teloxide::RequestError>,
teloxide::dispatching::DpHandlerDescription,
> {
Update::filter_message() Update::filter_message()
.branch( .branch(
dptree::entry() dptree::entry()
@ -383,11 +385,14 @@ fn command_handler(
.branch( .branch(
dptree::entry() dptree::entry()
.filter_async(async |msg: Message, mut db: DB| { .filter_async(async |msg: Message, mut db: DB| {
let tguser = msg.from.unwrap(); let tguser = match msg.from.clone() {
Some(user) => user,
None => return false, // do nothing, cause its not usecase of function
};
let user = db let user = db
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name) .get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
.await; .await;
user.is_admin user.map(|u| u.is_admin).unwrap_or(false)
}) })
.filter_command::<AdminCommands>() .filter_command::<AdminCommands>()
.endpoint(admin_command_handler), .endpoint(admin_command_handler),
@ -399,14 +404,20 @@ async fn user_command_handler(
bot: Bot, bot: Bot,
msg: Message, msg: Message,
cmd: UserCommands, cmd: UserCommands,
) -> Result<(), teloxide::RequestError> { ) -> BotResult<()> {
let tguser = msg.from.clone().unwrap(); let tguser = match msg.from.clone() {
Some(user) => user,
None => return Ok(()), // do nothing, cause its not usecase of function
};
let user = db let user = db
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name) .get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
.await; .await?;
let user = update_user_tg(user, msg.from.as_ref().unwrap()); let user = update_user_tg(user, &tguser);
user.update_user(&mut db).await.unwrap(); user.update_user(&mut db).await?;
println!("MSG: {}", msg.html_text().unwrap()); println!(
"MSG: {}",
msg.html_text().unwrap_or("|EMPTY_MESSAGE|".into())
);
match cmd { match cmd {
UserCommands::Start => { UserCommands::Start => {
let mut db2 = db.clone(); let mut db2 = db.clone();
@ -415,9 +426,10 @@ async fn user_command_handler(
msg.chat.id.0, msg.chat.id.0,
&mut db, &mut db,
"start", "start",
Some(make_start_buttons(&mut db2).await), Some(make_start_buttons(&mut db2).await?),
) )
.await .await?;
Ok(())
} }
UserCommands::Help => { UserCommands::Help => {
bot.send_message(msg.chat.id, UserCommands::descriptions().to_string()) bot.send_message(msg.chat.id, UserCommands::descriptions().to_string())
@ -433,13 +445,12 @@ async fn answer_message<RM: Into<ReplyMarkup>>(
db: &mut DB, db: &mut DB,
literal: &str, literal: &str,
keyboard: Option<RM>, keyboard: Option<RM>,
) -> Result<(), teloxide::RequestError> { ) -> BotResult<()> {
let text = db let text = db
.get_literal_value(literal) .get_literal_value(literal)
.await .await?
.unwrap()
.unwrap_or("Please, set content of this message".into()); .unwrap_or("Please, set content of this message".into());
let media = db.get_media(literal).await.unwrap(); let media = db.get_media(literal).await?;
let (chat_id, msg_id) = match media.len() { let (chat_id, msg_id) = match media.len() {
// just a text // just a text
0 => { 0 => {
@ -551,16 +562,14 @@ async fn answer_message<RM: Into<ReplyMarkup>>(
(msg[0].chat.id.0, msg[0].id.0) (msg[0].chat.id.0, msg[0].id.0)
} }
}; };
db.set_message_literal(chat_id, msg_id, literal) db.set_message_literal(chat_id, msg_id, literal).await?;
.await
.unwrap();
Ok(()) Ok(())
} }
async fn make_start_buttons(db: &mut DB) -> InlineKeyboardMarkup { async fn make_start_buttons(db: &mut DB) -> BotResult<InlineKeyboardMarkup> {
let mut buttons: Vec<Vec<InlineKeyboardButton>> = db let mut buttons: Vec<Vec<InlineKeyboardButton>> = db
.get_all_events() .get_all_events()
.await .await?
.iter() .iter()
.map(|e| { .map(|e| {
vec![InlineKeyboardButton::callback( vec![InlineKeyboardButton::callback(
@ -574,10 +583,10 @@ async fn make_start_buttons(db: &mut DB) -> InlineKeyboardMarkup {
"more_info", "more_info",
)]); )]);
InlineKeyboardMarkup::new(buttons) Ok(InlineKeyboardMarkup::new(buttons))
} }
async fn echo(bot: Bot, msg: Message) -> Result<(), teloxide::RequestError> { async fn echo(bot: Bot, msg: Message) -> BotResult<()> {
if let Some(photo) = msg.photo() { if let Some(photo) = msg.photo() {
println!("File ID: {}", photo[0].file.id); println!("File ID: {}", photo[0].file.id);
} }