Compare commits

..

12 Commits

Author SHA1 Message Date
Akulij
0c71fd3796 delete unused block in matching callback 2025-04-30 13:43:30 +03:00
Akulij
5192b43e0b create show_projects button on /start 2025-04-30 13:42:10 +03:00
Akulij
8c2d2425c4 create create_callback_button function
description: utils.rs will have shortcuts for frequently used code
2025-04-30 13:38:57 +03:00
Akulij
36c729e57b rename DemoProject callback to more convinient ProjectPage 2025-04-30 12:56:36 +03:00
Akulij
cbe24cc134 use Callbackinfo with internal Callback struct to handle telegram callbacks 2025-04-29 19:53:01 +03:00
Akulij
58a16a5927 make CallbackInfo::store to consume self and return 2025-04-29 19:51:54 +03:00
Akulij
a00c0017bb create CallbackInfo::get_callback method 2025-04-29 19:51:17 +03:00
Akulij
48bb7b133b make DbError a custom error type to handle internal errors 2025-04-29 19:50:42 +03:00
Akulij
221fb87c8f create query_call_consume macro 2025-04-29 19:31:23 +03:00
Akulij
078e2fd62a create CallbackInfo and its tests 2025-04-29 17:37:20 +03:00
Akulij
0973499652 make query_call macro public 2025-04-29 17:35:49 +03:00
Akulij
b56f07e6be rename db/tests.rs to dt/tests.mod.rs
reason: there will be way more tests, so its better to use multiple
files for them
2025-04-29 11:51:24 +03:00
6 changed files with 198 additions and 22 deletions

62
src/db/callback_info.rs Normal file
View File

@ -0,0 +1,62 @@
use crate::query_call_consume;
use crate::CallDB;
use bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
use super::DbResult;
use bson::doc;
#[derive(Serialize, Deserialize, Default)]
pub struct CallbackInfo<C>
where
C: Serialize,
{
pub _id: bson::oid::ObjectId,
#[serde(flatten)]
pub callback: C,
}
impl<C> CallbackInfo<C>
where
C: Serialize + for<'a> Deserialize<'a> + Send + Sync,
{
pub fn new(callback: C) -> Self {
Self {
_id: Default::default(),
callback,
}
}
pub fn get_id(&self) -> String {
self._id.to_hex()
}
query_call_consume!(store, self, db, Self, {
let db = db.get_database().await;
let ci = db.collection::<Self>("callback_info");
ci.insert_one(&self).await?;
Ok(self)
});
pub async fn get<D: CallDB>(db: &mut D, id: &str) -> DbResult<Option<Self>> {
let db = db.get_database().await;
let ci = db.collection::<Self>("callback_info");
let id = match ObjectId::parse_str(id) {
Ok(id) => id,
Err(_) => return Ok(None),
};
Ok(ci
.find_one(doc! {
"_id": id
})
.await?)
}
pub async fn get_callback<D: CallDB>(db: &mut D, id: &str) -> DbResult<Option<C>> {
Self::get(db, id).await.map(|co| co.map(|c| c.callback))
}
}

View File

@ -1,3 +1,5 @@
pub mod callback_info;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use enum_stringify::EnumStringify;
@ -35,6 +37,7 @@ pub struct User {
pub language_code: Option<String>,
}
#[macro_export]
macro_rules! query_call {
($func_name:ident, $self:ident, $db:ident, $return_type:ty, $body:block) => {
pub async fn $func_name<D: CallDB>(&$self, $db: &mut D)
@ -42,6 +45,14 @@ macro_rules! query_call {
};
}
#[macro_export]
macro_rules! query_call_consume {
($func_name:ident, $self:ident, $db:ident, $return_type:ty, $body:block) => {
pub async fn $func_name<D: CallDB>($self, $db: &mut D)
-> DbResult<$return_type> $body
};
}
impl User {
query_call!(update_user, self, db, (), {
let db_collection = db.get_database().await.collection::<Self>("users");
@ -136,7 +147,11 @@ impl CallDB for DB {
}
}
pub type DbError = mongodb::error::Error;
#[derive(thiserror::Error, Debug)]
pub enum DbError {
#[error("error while processing mongodb query: {0}")]
MongodbError(#[from] mongodb::error::Error),
}
pub type DbResult<T> = Result<T, DbError>;
#[async_trait]
@ -148,7 +163,7 @@ pub trait CallDB {
let db = self.get_database().await;
let users = db.collection::<User>("users");
users.find(doc! {}).await?.try_collect().await
Ok(users.find(doc! {}).await?.try_collect().await?)
}
async fn set_admin(&mut self, userid: i64, isadmin: bool) -> DbResult<()> {
@ -268,7 +283,7 @@ pub trait CallDB {
let db = self.get_database().await;
let events = db.collection::<Event>("events");
events.find(doc! {}).await?.try_collect().await
Ok(events.find(doc! {}).await?.try_collect().await?)
}
async fn create_event(&mut self, event_datetime: chrono::DateTime<Utc>) -> DbResult<Event> {

View File

@ -0,0 +1,28 @@
use super::super::callback_info::CallbackInfo;
use super::setup_db;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Default)]
#[serde(tag = "type")]
#[serde(rename = "snake_case")]
pub enum Callback {
#[default]
MoreInfo,
NextPage,
}
type CI = CallbackInfo<Callback>;
#[tokio::test]
async fn test_store() {
let mut db = setup_db().await;
let ci = CI::new(Default::default());
let ci = ci.store(&mut db).await.unwrap();
let ci = CI::get(&mut db, &ci.get_id()).await.unwrap();
assert!(ci.is_some());
}

View File

@ -1,5 +1,6 @@
#![allow(clippy::unwrap_used)]
mod callback_info_tests;
use dotenvy;
use super::CallDB;

View File

@ -1,9 +1,12 @@
pub mod admin;
pub mod db;
pub mod mongodb_storage;
pub mod utils;
use log::info;
use db::callback_info::CallbackInfo;
use log::{info, warn};
use std::time::Duration;
use utils::create_callback_button;
use crate::admin::{admin_command_handler, AdminCommands};
use crate::admin::{secret_command_handler, SecretCommands};
@ -70,6 +73,16 @@ pub enum State {
},
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename = "snake_case")]
pub enum Callback {
MoreInfo,
ProjectPage { id: u32 },
}
type CallbackStore = CallbackInfo<Callback>;
pub struct BotController {
pub bot: Bot,
pub db: DB,
@ -175,21 +188,39 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn callback_handler(bot: Bot, mut db: DB, q: CallbackQuery) -> BotResult<()> {
bot.answer_callback_query(&q.id).await?;
if let Some(ref data) = q.data {
match data.as_str() {
"more_info" => {
answer_message(
&bot,
q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64),
&mut db,
"more_info",
None as Option<InlineKeyboardMarkup>,
)
.await?
}
_ => {} // do nothing, yet
let data = match q.data {
Some(ref data) => data,
None => {
// not really our case to handle
return Ok(());
}
}
};
let callback = match CallbackStore::get_callback(&mut db, data).await? {
Some(callback) => callback,
None => {
warn!("Not found callback for data: {data}");
// doing this silently beacuse end user shouldn't know about backend internal data
return Ok(());
}
};
match callback {
Callback::MoreInfo => {
answer_message(
&bot,
q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64),
&mut db,
"more_info",
None as Option<InlineKeyboardMarkup>,
)
.await?
}
Callback::ProjectPage { id } => {
bot.send_message(q.from.id, format!("Some project No: {id}"))
.await?;
}
};
Ok(())
}
@ -580,10 +611,21 @@ async fn make_start_buttons(db: &mut DB) -> BotResult<InlineKeyboardMarkup> {
)]
})
.collect();
buttons.push(vec![InlineKeyboardButton::callback(
"More info",
"more_info",
)]);
buttons.push(vec![
InlineKeyboardButton::callback(
"More info",
CallbackStore::new(Callback::MoreInfo)
.store(db)
.await?
.get_id(),
),
create_callback_button(
"show_projects",
CallbackStore::new(Callback::ProjectPage { id: 1 }),
db,
)
.await?,
]);
Ok(InlineKeyboardMarkup::new(buttons))
}

28
src/utils.rs Normal file
View File

@ -0,0 +1,28 @@
use serde::{Deserialize, Serialize};
use teloxide::types::InlineKeyboardButton;
use crate::{
db::{callback_info::CallbackInfo, CallDB},
BotResult,
};
pub async fn create_callback_button<C, D>(
literal: &str,
ci: CallbackInfo<C>,
db: &mut D,
) -> BotResult<InlineKeyboardButton>
where
C: Serialize + for<'a> Deserialize<'a> + Send + Sync,
D: CallDB + Send,
{
let text = db
.get_literal_value(literal)
.await?
.unwrap_or("Please, set content of this message".into());
let ci = ci.store(db).await?;
Ok(InlineKeyboardButton::new(
text,
teloxide::types::InlineKeyboardButtonKind::CallbackData(ci.get_id()),
))
}