Compare commits

..

14 Commits

Author SHA1 Message Date
Akulij
ec49ab24b6 clippy: fix warning unused result 2025-04-28 22:54:33 +03:00
Akulij
a51344d089 use log crate instead of raw prints 2025-04-28 22:53:58 +03:00
Akulij
fd3ef221c9 cargo add log 2025-04-28 22:44:01 +03:00
Akulij
ffe2fd8bc1 create custom type to return all errors 2025-04-28 22:43:18 +03:00
Akulij
1c9ab867ed follow new db function declarations 2025-04-28 22:43:02 +03:00
Akulij
c11e671b15 return custom error in src/mongodb_storage.rs to support return of serializer error 2025-04-28 22:41:21 +03:00
Akulij
55d393eb2c return result instead of unwraps in db/mod.rs 2025-04-28 22:40:38 +03:00
Akulij
3761cc6fa8 add atribute allow clippy::unwrap_used in tests, since it's what tests should do 2025-04-28 21:07:45 +03:00
Akulij
091c0a436e ignore macos's file 2025-04-28 20:23:43 +03:00
Akulij
982b6c8ff0 set clippy rule unwrap used as warning 2025-04-28 20:23:00 +03:00
Akulij
329ead4f6e add thiserror as a dependency 2025-04-28 20:22:41 +03:00
Akulij
8b371ebb2f clippy: fix warnings in db/tests.rs 2025-04-28 18:34:25 +03:00
Akulij
adfd019166 add clippy lint rule 2025-04-28 18:07:34 +03:00
Akulij
2aff289a96 clippy: fix warnings 2025-04-28 17:47:31 +03:00
8 changed files with 233 additions and 202 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target /target
.env .env
result result
**/.DS_Store

2
Cargo.lock generated
View File

@ -839,9 +839,11 @@ dependencies = [
"enum_stringify", "enum_stringify",
"envconfig", "envconfig",
"futures", "futures",
"log",
"mongodb", "mongodb",
"serde", "serde",
"teloxide", "teloxide",
"thiserror 2.0.12",
"tokio", "tokio",
] ]

View File

@ -14,7 +14,13 @@ dotenvy = "0.15.7"
enum_stringify = "0.6.3" enum_stringify = "0.6.3"
envconfig = "0.11.0" envconfig = "0.11.0"
futures = "0.3.31" futures = "0.3.31"
log = "0.4.27"
mongodb = "3.2.3" mongodb = "3.2.3"
serde = { version = "1.0.219", features = ["derive", "serde_derive"] } serde = { version = "1.0.219", features = ["derive", "serde_derive"] }
teloxide = { version = "0.14.0", features = ["macros", "postgres-storage-nativetls"] } teloxide = { version = "0.14.0", features = ["macros", "postgres-storage-nativetls"] }
thiserror = "2.0.12"
tokio = { version = "1.44.1", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.44.1", features = ["rt-multi-thread", "macros"] }
[lints.clippy]
print_stdout = "warn"
unwrap_used = "warn"

View File

@ -3,8 +3,12 @@ use teloxide::{
utils::{command::BotCommands, render::RenderMessageTextHelper}, utils::{command::BotCommands, render::RenderMessageTextHelper},
}; };
use crate::db::{CallDB, DB};
use crate::LogMsg; use crate::LogMsg;
use crate::{
db::{CallDB, DB},
BotResult,
};
use log::info;
// These are should not appear in /help // These are should not appear in /help
#[derive(BotCommands, Clone)] #[derive(BotCommands, Clone)]
@ -30,9 +34,15 @@ pub async fn admin_command_handler(
bot: Bot, bot: Bot,
msg: Message, msg: Message,
cmd: AdminCommands, cmd: AdminCommands,
) -> Result<(), teloxide::RequestError> { ) -> BotResult<()> {
let tguser = msg.from.clone().unwrap(); let tguser = match msg.from.clone() {
println!("MSG: {}", msg.html_text().unwrap()); Some(user) => user,
None => return Ok(()), // do nothing, cause its not usecase of function
};
info!(
"MSG: {}",
msg.html_text().unwrap_or("|EMPTY_MESSAGE|".into())
);
match cmd { match cmd {
AdminCommands::MyId => { AdminCommands::MyId => {
bot.send_message(msg.chat.id, format!("Your ID is: {}", tguser.id)) bot.send_message(msg.chat.id, format!("Your ID is: {}", tguser.id))
@ -54,7 +64,7 @@ pub async fn admin_command_handler(
Ok(()) Ok(())
} }
AdminCommands::Deop => { AdminCommands::Deop => {
db.set_admin(tguser.id.0 as i64, false).await; db.set_admin(tguser.id.0 as i64, false).await?;
bot.send_message(msg.chat.id, "You are not an admin anymore") bot.send_message(msg.chat.id, "You are not an admin anymore")
.await?; .await?;
Ok(()) Ok(())
@ -69,22 +79,27 @@ pub async fn secret_command_handler(
msg: Message, msg: Message,
cmd: SecretCommands, cmd: SecretCommands,
admin_password: String, admin_password: String,
) -> Result<(), teloxide::RequestError> { ) -> BotResult<()> {
println!("Admin Pass: {}", admin_password); info!("Admin Pass: {}", admin_password);
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?;
println!("MSG: {}", msg.html_text().unwrap()); info!(
"MSG: {}",
msg.html_text().unwrap_or("|EMPTY_MESSAGE|".into())
);
match cmd { match cmd {
SecretCommands::Secret { pass } => { SecretCommands::Secret { pass } => {
if user.is_admin == true { if user.is_admin {
bot.send_message(msg.from.unwrap().id, "You are an admin already") bot.send_message(tguser.id, "You are an admin already")
.await?; .await?;
} else if pass == admin_password { } else if pass == admin_password {
db.set_admin(user.id, true).await; db.set_admin(user.id, true).await?;
bot.send_message(msg.from.unwrap().id, "You are an admin now!") bot.send_message(tguser.id, "You are an admin now!").await?;
.await?;
} }
Ok(()) Ok(())
} }

View File

@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use enum_stringify::EnumStringify; use enum_stringify::EnumStringify;
use futures::stream::{StreamExt, TryStreamExt}; use futures::stream::TryStreamExt;
use mongodb::options::IndexOptions; use mongodb::options::IndexOptions;
use mongodb::{bson::doc, options::ClientOptions, Client}; use mongodb::{bson::doc, options::ClientOptions, Client};
@ -38,7 +38,7 @@ pub struct User {
macro_rules! query_call { macro_rules! query_call {
($func_name:ident, $self:ident, $db:ident, $return_type:ty, $body:block) => { ($func_name:ident, $self:ident, $db:ident, $return_type:ty, $body:block) => {
pub async fn $func_name<D: CallDB>(&$self, $db: &mut D) pub async fn $func_name<D: CallDB>(&$self, $db: &mut D)
-> Result<$return_type, Box<dyn std::error::Error>> $body -> DbResult<$return_type> $body
}; };
} }
@ -101,14 +101,14 @@ pub struct DB {
} }
impl DB { impl DB {
pub async fn new<S: Into<String>>(db_url: S) -> Self { pub async fn new<S: Into<String>>(db_url: S) -> DbResult<Self> {
let options = ClientOptions::parse(db_url.into()).await.unwrap(); let options = ClientOptions::parse(db_url.into()).await?;
let client = Client::with_options(options).unwrap(); let client = Client::with_options(options)?;
DB { client } Ok(DB { client })
} }
pub async fn migrate(&mut self) -> Result<(), mongodb::error::Error> { pub async fn migrate(&mut self) -> DbResult<()> {
let events = self.get_database().await.collection::<Event>("events"); let events = self.get_database().await.collection::<Event>("events");
events events
.create_index( .create_index(
@ -121,8 +121,8 @@ impl DB {
Ok(()) Ok(())
} }
pub async fn init<S: Into<String>>(db_url: S) -> Result<Self, mongodb::error::Error> { pub async fn init<S: Into<String>>(db_url: S) -> DbResult<Self> {
let mut db = Self::new(db_url).await; let mut db = Self::new(db_url).await?;
db.migrate().await?; db.migrate().await?;
Ok(db) Ok(db)
@ -136,24 +136,22 @@ impl CallDB for DB {
} }
} }
pub type DbError = mongodb::error::Error;
pub type DbResult<T> = Result<T, DbError>;
#[async_trait] #[async_trait]
pub trait CallDB { pub trait CallDB {
//type C; //type C;
async fn get_database(&mut self) -> Database; async fn get_database(&mut self) -> Database;
//async fn get_pool(&mut self) -> PooledConnection<'_, AsyncDieselConnectionManager<C>>; //async fn get_pool(&mut self) -> PooledConnection<'_, AsyncDieselConnectionManager<C>>;
async fn get_users(&mut self) -> Vec<User> { async fn get_users(&mut self) -> DbResult<Vec<User>> {
let db = self.get_database().await; let db = self.get_database().await;
let users = db.collection::<User>("users"); let users = db.collection::<User>("users");
users
.find(doc! {}) users.find(doc! {}).await?.try_collect().await
.await
.unwrap()
.map(|u| u.unwrap())
.collect()
.await
} }
async fn set_admin(&mut self, userid: i64, isadmin: bool) { async fn set_admin(&mut self, userid: i64, isadmin: bool) -> DbResult<()> {
let db = self.get_database().await; let db = self.get_database().await;
let users = db.collection::<User>("users"); let users = db.collection::<User>("users");
users users
@ -165,11 +163,12 @@ pub trait CallDB {
"$set": { "is_admin": isadmin } "$set": { "is_admin": isadmin }
}, },
) )
.await .await?;
.unwrap();
Ok(())
} }
async fn get_or_init_user(&mut self, userid: i64, firstname: &str) -> User { async fn get_or_init_user(&mut self, userid: i64, firstname: &str) -> DbResult<User> {
let db = self.get_database().await; let db = self.get_database().await;
let users = db.collection::<User>("users"); let users = db.collection::<User>("users");
@ -182,21 +181,15 @@ pub trait CallDB {
}, },
) )
.upsert(true) .upsert(true)
.await .await?;
.unwrap();
users Ok(users
.find_one(doc! { "id": userid }) .find_one(doc! { "id": userid })
.await .await?
.unwrap() .expect("no such user created"))
.expect("no such user created")
} }
async fn get_message( async fn get_message(&mut self, chatid: i64, messageid: i32) -> DbResult<Option<Message>> {
&mut self,
chatid: i64,
messageid: i32,
) -> Result<Option<Message>, Box<dyn std::error::Error>> {
let db = self.get_database().await; let db = self.get_database().await;
let messages = db.collection::<Message>("messages"); let messages = db.collection::<Message>("messages");
@ -211,7 +204,7 @@ pub trait CallDB {
&mut self, &mut self,
chatid: i64, chatid: i64,
messageid: i32, messageid: i32,
) -> Result<Option<String>, Box<dyn std::error::Error>> { ) -> DbResult<Option<String>> {
let msg = self.get_message(chatid, messageid).await?; let msg = self.get_message(chatid, messageid).await?;
Ok(msg.map(|m| m.token)) Ok(msg.map(|m| m.token))
} }
@ -221,7 +214,7 @@ pub trait CallDB {
chatid: i64, chatid: i64,
messageid: i32, messageid: i32,
literal: &str, literal: &str,
) -> Result<(), Box<dyn std::error::Error>> { ) -> DbResult<()> {
let db = self.get_database().await; let db = self.get_database().await;
let messages = db.collection::<Message>("messages"); let messages = db.collection::<Message>("messages");
@ -241,10 +234,7 @@ pub trait CallDB {
Ok(()) Ok(())
} }
async fn get_literal( async fn get_literal(&mut self, literal: &str) -> DbResult<Option<Literal>> {
&mut self,
literal: &str,
) -> Result<Option<Literal>, Box<dyn std::error::Error>> {
let db = self.get_database().await; let db = self.get_database().await;
let messages = db.collection::<Literal>("literals"); let messages = db.collection::<Literal>("literals");
@ -253,20 +243,13 @@ pub trait CallDB {
Ok(literal) Ok(literal)
} }
async fn get_literal_value( async fn get_literal_value(&mut self, literal: &str) -> DbResult<Option<String>> {
&mut self,
literal: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let literal = self.get_literal(literal).await?; let literal = self.get_literal(literal).await?;
Ok(literal.map(|l| l.value)) Ok(literal.map(|l| l.value))
} }
async fn set_literal( async fn set_literal(&mut self, literal: &str, valuestr: &str) -> DbResult<()> {
&mut self,
literal: &str,
valuestr: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let db = self.get_database().await; let db = self.get_database().await;
let literals = db.collection::<Literal>("literals"); let literals = db.collection::<Literal>("literals");
@ -281,23 +264,14 @@ pub trait CallDB {
Ok(()) Ok(())
} }
async fn get_all_events(&mut self) -> Vec<Event> { async fn get_all_events(&mut self) -> DbResult<Vec<Event>> {
let db = self.get_database().await; let db = self.get_database().await;
let events = db.collection::<Event>("events"); let events = db.collection::<Event>("events");
events events.find(doc! {}).await?.try_collect().await
.find(doc! {})
.await
.unwrap()
.map(|e| e.unwrap())
.collect()
.await
} }
async fn create_event( async fn create_event(&mut self, event_datetime: chrono::DateTime<Utc>) -> DbResult<Event> {
&mut self,
event_datetime: chrono::DateTime<Utc>,
) -> Result<Event, Box<dyn std::error::Error>> {
let db = self.get_database().await; let db = self.get_database().await;
let events = db.collection::<Event>("events"); let events = db.collection::<Event>("events");
@ -311,7 +285,7 @@ pub trait CallDB {
Ok(new_event) Ok(new_event)
} }
async fn get_media(&mut self, literal: &str) -> Result<Vec<Media>, Box<dyn std::error::Error>> { async fn get_media(&mut self, literal: &str) -> DbResult<Vec<Media>> {
let db = self.get_database().await; let db = self.get_database().await;
let media = db.collection::<Media>("media"); let media = db.collection::<Media>("media");
@ -324,10 +298,7 @@ pub trait CallDB {
Ok(media_items) Ok(media_items)
} }
async fn is_media_group_exists( async fn is_media_group_exists(&mut self, media_group: &str) -> DbResult<bool> {
&mut self,
media_group: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
let db = self.get_database().await; let db = self.get_database().await;
let media = db.collection::<Media>("media"); let media = db.collection::<Media>("media");
@ -339,7 +310,7 @@ pub trait CallDB {
Ok(is_exists) Ok(is_exists)
} }
async fn drop_media(&mut self, literal: &str) -> Result<usize, Box<dyn std::error::Error>> { async fn drop_media(&mut self, literal: &str) -> DbResult<usize> {
let db = self.get_database().await; let db = self.get_database().await;
let media = db.collection::<Media>("media"); let media = db.collection::<Media>("media");
@ -351,11 +322,7 @@ pub trait CallDB {
Ok(deleted_count as usize) Ok(deleted_count as usize)
} }
async fn drop_media_except( async fn drop_media_except(&mut self, literal: &str, except_group: &str) -> DbResult<usize> {
&mut self,
literal: &str,
except_group: &str,
) -> Result<usize, Box<dyn std::error::Error>> {
let db = self.get_database().await; let db = self.get_database().await;
let media = db.collection::<Media>("media"); let media = db.collection::<Media>("media");
@ -376,7 +343,7 @@ pub trait CallDB {
mediatype: &str, mediatype: &str,
fileid: &str, fileid: &str,
media_group: Option<&str>, media_group: Option<&str>,
) -> Result<Media, Box<dyn std::error::Error>> { ) -> DbResult<Media> {
let db = self.get_database().await; let db = self.get_database().await;
let media = db.collection::<Media>("media"); let media = db.collection::<Media>("media");

View File

@ -1,3 +1,5 @@
#![allow(clippy::unwrap_used)]
use dotenvy; use dotenvy;
use super::CallDB; use super::CallDB;
@ -6,21 +8,20 @@ use super::DB;
async fn setup_db() -> DB { async fn setup_db() -> DB {
dotenvy::dotenv().unwrap(); dotenvy::dotenv().unwrap();
let db_url = std::env::var("DATABASE_URL").unwrap(); let db_url = std::env::var("DATABASE_URL").unwrap();
let db = DB::new(db_url).await;
db DB::new(db_url).await.unwrap()
} }
#[tokio::test] #[tokio::test]
async fn test_get_media() { async fn test_get_media() {
let mut db = setup_db().await; let mut db = setup_db().await;
let result = db.drop_media("test_get_media_literal").await.unwrap(); let _result = db.drop_media("test_get_media_literal").await.unwrap();
let media_items = db.get_media("test_get_media_literal").await.unwrap(); let media_items = db.get_media("test_get_media_literal").await.unwrap();
assert_eq!(media_items.len(), 0); assert_eq!(media_items.len(), 0);
let result = db let _result = db
.add_media("test_get_media_literal", "photo", "file_id_1", None) .add_media("test_get_media_literal", "photo", "file_id_1", None)
.await .await
.unwrap(); .unwrap();
@ -28,7 +29,7 @@ async fn test_get_media() {
let media_items = db.get_media("test_get_media_literal").await.unwrap(); let media_items = db.get_media("test_get_media_literal").await.unwrap();
assert_eq!(media_items.len(), 1); assert_eq!(media_items.len(), 1);
let result = db let _result = db
.add_media("test_get_media_literal", "video", "file_id_2", None) .add_media("test_get_media_literal", "video", "file_id_2", None)
.await .await
.unwrap(); .unwrap();
@ -37,7 +38,7 @@ async fn test_get_media() {
assert_eq!(media_items.len(), 2); assert_eq!(media_items.len(), 2);
// Clean up after test // Clean up after test
let result = db.drop_media("test_get_media_literal").await.unwrap(); let _result = db.drop_media("test_get_media_literal").await.unwrap();
} }
#[tokio::test] #[tokio::test]
@ -48,9 +49,9 @@ async fn test_add_media() {
let media_type = "photo"; let media_type = "photo";
let file_id = "LjaldhAOh"; let file_id = "LjaldhAOh";
let result = db.drop_media(literal).await.unwrap(); let _result = db.drop_media(literal).await.unwrap();
let result = db let _result = db
.add_media(literal, media_type, file_id, None) .add_media(literal, media_type, file_id, None)
.await .await
.unwrap(); .unwrap();
@ -63,14 +64,14 @@ async fn test_add_media() {
assert_eq!(media_items[0].file_id, file_id); assert_eq!(media_items[0].file_id, file_id);
// Clean up after test // Clean up after test
let result = db.drop_media(literal).await.unwrap(); let _result = db.drop_media(literal).await.unwrap();
} }
#[tokio::test] #[tokio::test]
async fn test_drop_media() { async fn test_drop_media() {
let mut db = setup_db().await; let mut db = setup_db().await;
let result = db let _result = db
.add_media("test_drop_media_literal", "photo", "file_id_1", None) .add_media("test_drop_media_literal", "photo", "file_id_1", None)
.await .await
.unwrap(); .unwrap();
@ -79,14 +80,14 @@ async fn test_drop_media() {
let media_items = db.get_media("test_drop_media_literal").await.unwrap(); let media_items = db.get_media("test_drop_media_literal").await.unwrap();
assert_eq!(media_items.len(), 1); assert_eq!(media_items.len(), 1);
let result = db.drop_media("test_drop_media_literal").await.unwrap(); let _result = db.drop_media("test_drop_media_literal").await.unwrap();
// Verify that the media has been dropped // Verify that the media has been dropped
let media_items = db.get_media("test_drop_media_literal").await.unwrap(); let media_items = db.get_media("test_drop_media_literal").await.unwrap();
assert_eq!(media_items.len(), 0); assert_eq!(media_items.len(), 0);
// Clean up after test // Clean up after test
let result = db.drop_media("test_drop_media_literal").await.unwrap(); let _result = db.drop_media("test_drop_media_literal").await.unwrap();
} }
#[tokio::test] #[tokio::test]
@ -135,7 +136,7 @@ async fn test_drop_media_except() {
let media_items = db.get_media(literal).await.unwrap(); let media_items = db.get_media(literal).await.unwrap();
assert_eq!(media_items.len(), 2); assert_eq!(media_items.len(), 2);
let deleted_count = db.drop_media_except(literal, media_group).await.unwrap(); let _deleted_count = db.drop_media_except(literal, media_group).await.unwrap();
let media_items = db.get_media(literal).await.unwrap(); let media_items = db.get_media(literal).await.unwrap();
assert_eq!(media_items.len(), 0); assert_eq!(media_items.len(), 0);
@ -152,7 +153,7 @@ async fn test_drop_media_except() {
.await .await
.unwrap(); .unwrap();
let deleted_count = db.drop_media_except(literal, media_group).await.unwrap(); let _deleted_count = db.drop_media_except(literal, media_group).await.unwrap();
let media_items = db.get_media(literal).await.unwrap(); let media_items = db.get_media(literal).await.unwrap();
assert_eq!(media_items.len(), 1); assert_eq!(media_items.len(), 1);
let _ = db.drop_media(literal).await.unwrap(); let _ = db.drop_media(literal).await.unwrap();
@ -166,7 +167,7 @@ async fn test_drop_media_except() {
.await .await
.unwrap(); .unwrap();
let deleted_count = db.drop_media_except(literal, media_group).await.unwrap(); let _deleted_count = db.drop_media_except(literal, media_group).await.unwrap();
let media_items = db.get_media(literal).await.unwrap(); let media_items = db.get_media(literal).await.unwrap();
assert_eq!(media_items.len(), 2); assert_eq!(media_items.len(), 2);

View File

@ -2,6 +2,7 @@ pub mod admin;
pub mod db; pub mod db;
pub mod mongodb_storage; pub mod mongodb_storage;
use log::info;
use std::time::Duration; use std::time::Duration;
use crate::admin::{admin_command_handler, AdminCommands}; use crate::admin::{admin_command_handler, AdminCommands};
@ -11,10 +12,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,
@ -52,7 +54,7 @@ trait LogMsg {
impl LogMsg for <teloxide::Bot as teloxide::prelude::Requester>::SendMessage { impl LogMsg for <teloxide::Bot as teloxide::prelude::Requester>::SendMessage {
fn log(self) -> Self { fn log(self) -> Self {
println!("msg: {}", self.text); info!("msg: {}", self.text);
self self
} }
} }
@ -82,6 +84,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,33 +108,39 @@ 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
let events: Vec<DateTime<Utc>> = vec!["2025-04-09T18:00:00+04:00", "2025-04-11T16:00:00+04:00"] // 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"]
.iter() .iter()
.map(|d| DateTime::parse_from_rfc3339(d).unwrap().into()) .map(|d| DateTime::parse_from_rfc3339(d).unwrap().into())
.collect(); .collect();
for event in events { for event in events {
match bc.db.create_event(event).await { match bc.db.create_event(event).await {
Ok(e) => println!("Created event {}", e._id), Ok(e) => info!("Created event {}", e._id),
Err(err) => println!("Failed to create event, error: {}", err), Err(err) => info!("Failed to create event, error: {}", err),
} }
} }
// //
let handler = dptree::entry() let handler = dptree::entry()
.inspect(|u: Update| { .inspect(|u: Update| {
eprintln!("{u:#?}"); // Print the update to the console with inspect info!("{u:#?}"); // Print the update to the console with inspect
}) })
.branch(Update::filter_callback_query().endpoint(callback_handler)) .branch(Update::filter_callback_query().endpoint(callback_handler))
.branch(command_handler(config)) .branch(command_handler(config))
.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 +171,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 {
@ -160,10 +179,7 @@ async fn callback_handler(
"more_info" => { "more_info" => {
answer_message( answer_message(
&bot, &bot,
q.chat_id() q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64),
.clone()
.map(|i| i.0)
.unwrap_or(q.from.id.0 as i64),
&mut db, &mut db,
"more_info", "more_info",
None as Option<InlineKeyboardMarkup>, None as Option<InlineKeyboardMarkup>,
@ -182,16 +198,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?;
@ -206,8 +218,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)",
@ -228,45 +239,44 @@ 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;
println!("Type: {:#?}", msg.kind); info!("Type: {:#?}", msg.kind);
let msg = if let MessageKind::Common(msg) = msg.kind { let msg = if let MessageKind::Common(msg) = msg.kind {
msg msg
} else { } else {
println!("Not a Common, somehow"); info!("Not a Common, somehow");
return Ok(()); return Ok(());
}; };
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 => {
@ -275,10 +285,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?;
}; };
@ -296,8 +305,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(());
@ -306,18 +314,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 => {
@ -326,10 +333,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?;
}; };
@ -347,8 +353,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(());
@ -365,12 +370,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()
@ -386,11 +386,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),
@ -402,14 +405,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()); info!(
"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();
@ -418,9 +427,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())
@ -436,13 +446,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 => {
@ -452,7 +461,7 @@ async fn answer_message<RM: Into<ReplyMarkup>>(
None => msg, None => msg,
}; };
let msg = msg.parse_mode(teloxide::types::ParseMode::Html); let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
println!("ENTS: {:?}", msg.entities); info!("ENTS: {:?}", msg.entities);
let msg = msg.await?; let msg = msg.await?;
(msg.chat.id.0, msg.id.0) (msg.chat.id.0, msg.id.0)
@ -554,16 +563,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(
@ -577,12 +584,12 @@ 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); info!("File ID: {}", photo[0].file.id);
} }
bot.send_message(msg.chat.id, msg.html_text().unwrap_or("UNWRAP".into())) bot.send_message(msg.chat.id, msg.html_text().unwrap_or("UNWRAP".into()))
.parse_mode(teloxide::types::ParseMode::Html) .parse_mode(teloxide::types::ParseMode::Html)

View File

@ -36,6 +36,26 @@ pub struct Dialogue {
dialogue: Vec<u32>, dialogue: Vec<u32>,
} }
#[derive(Debug, thiserror::Error)]
pub enum MongodbStorageError<SE>
where
SE: Debug + Display,
{
MongodbError(#[from] mongodb::error::Error),
SerdeError(SE),
}
pub type MongodbStorageResult<T, SE> = Result<T, MongodbStorageError<SE>>;
impl<SE> std::fmt::Display for MongodbStorageError<SE>
where
SE: Debug + Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl<S, D> Storage<D> for MongodbStorage<S> impl<S, D> Storage<D> for MongodbStorage<S>
where where
S: Send + Sync + Serializer<D> + 'static, S: Send + Sync + Serializer<D> + 'static,
@ -43,7 +63,7 @@ where
<S as Serializer<D>>::Error: Debug + Display, <S as Serializer<D>>::Error: Debug + Display,
{ {
type Error = mongodb::error::Error; type Error = MongodbStorageError<<S as Serializer<D>>::Error>;
fn remove_dialogue( fn remove_dialogue(
self: std::sync::Arc<Self>, self: std::sync::Arc<Self>,
@ -56,7 +76,8 @@ where
let d = self.database.collection::<Dialogue>("dialogues"); let d = self.database.collection::<Dialogue>("dialogues");
d.delete_one(doc! { "chat_id": chat_id.0 }) d.delete_one(doc! { "chat_id": chat_id.0 })
.await .await
.map(|_| ()) .map(|_| ())?;
Ok(())
}) })
} }
@ -76,9 +97,14 @@ where
}, },
doc! { doc! {
"$set": doc! { "$set": doc! {
"dialogue": self.serializer.serialize(&dialogue).unwrap().into_iter().map(|v| v as u32).collect::<Vec<u32>>() "dialogue": self.serializer.serialize(&dialogue)
.map_err(MongodbStorageError::SerdeError)?
.into_iter().map(|v| v as u32).collect::<Vec<u32>>()
} }
}).upsert(true).await?; },
)
.upsert(true)
.await?;
Ok(()) Ok(())
}) })
} }
@ -89,8 +115,13 @@ where
) -> BoxFuture<'static, Result<Option<D>, Self::Error>> { ) -> BoxFuture<'static, Result<Option<D>, Self::Error>> {
Box::pin(async move { Box::pin(async move {
let d = self.database.collection::<Dialogue>("dialogues"); let d = self.database.collection::<Dialogue>("dialogues");
Ok(d.find_one(doc! { "chat_id": chat_id.0 }).await?.map(|d| { let d = d.find_one(doc! { "chat_id": chat_id.0 }).await?;
self.serializer let d = match d {
Some(d) => d,
None => return Ok(None),
};
let d = self
.serializer
.deserialize( .deserialize(
d.dialogue d.dialogue
.into_iter() .into_iter()
@ -98,8 +129,9 @@ where
.collect::<Vec<_>>() .collect::<Vec<_>>()
.as_slice(), .as_slice(),
) )
.unwrap() .map_err(MongodbStorageError::SerdeError)?;
}))
Ok(Some(d))
}) })
} }
} }