Switched to inline buttons when getting the account name, finished decrypt and delete Callback commands

This commit is contained in:
StNicolay 2023-07-29 15:21:40 +03:00
parent 5c14a77f29
commit 0139963459
Signed by: StNicolay
GPG Key ID: 9693D04DCD962B0D
17 changed files with 198 additions and 216 deletions

14
Cargo.lock generated
View File

@ -1666,7 +1666,7 @@ checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata 0.3.3", "regex-automata 0.3.4",
"regex-syntax 0.7.4", "regex-syntax 0.7.4",
] ]
@ -1681,9 +1681,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.3.3" version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -2118,18 +2118,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.177" version = "1.0.178"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63ba2516aa6bf82e0b19ca8b50019d52df58455d3cf9bdaf6315225fdd0c560a" checksum = "60363bdd39a7be0266a520dab25fdc9241d2f987b08a01e01f0ec6d06a981348"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.177" version = "1.0.178"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "401797fe7833d72109fedec6bfcbe67c0eed9b99772f26eb8afd261f0abc6fd3" checksum = "f28482318d6641454cb273da158647922d1be6b5a2fcc6165cd89ebdd7ed576b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

68
src/callbacks/decrypt.rs Normal file
View File

@ -0,0 +1,68 @@
use crate::{change_state, prelude::*};
use teloxide::types::ParseMode;
use tokio::task::spawn_blocking;
async fn get_master_pass(
bot: Throttle<Bot>,
msg: Message,
db: DatabaseConnection,
dialogue: MainDialogue,
mut ids: MessageIds,
name: String,
master_pass: String,
) -> crate::Result<()> {
dialogue.exit().await?;
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let account = match Account::get(user_id, &name, &db).await? {
Some(account) => account,
None => {
bot.send_message(msg.chat.id, "Account not found")
.reply_markup(deletion_markup())
.await?;
return Ok(());
}
};
let (login, password) = spawn_blocking(move || account.decrypt(&master_pass)).await??;
let text = format!("Name:\n`{name}`\nLogin:\n`{login}`\nPassword:\n`{password}`");
ids.alter_message(
&bot,
text,
account_markup(&name, false),
ParseMode::MarkdownV2,
)
.await?;
Ok(())
}
pub async fn decrypt(
bot: Throttle<Bot>,
q: CallbackQuery,
db: DatabaseConnection,
dialogue: MainDialogue,
hash: super::NameHash,
) -> crate::Result<()> {
let mut msg: MessageIds = q.message.as_ref().unwrap().into();
let user_id = q.from.id.0;
let name = match name_from_hash(&db, user_id, &hash).await? {
Some(name) => name,
None => {
msg.alter_message(
&bot,
"Account wan't found. Select another one",
menu_markup("decrypt", user_id, &db).await?,
None,
)
.await?;
return Ok(());
}
};
msg.alter_message(&bot, "Send master password", None, None)
.await?;
change_state!(dialogue, msg, (name), State::GetMasterPass, get_master_pass)
}

63
src/callbacks/delete.rs Normal file
View File

@ -0,0 +1,63 @@
use crate::{change_state, prelude::*};
async fn get_master_pass(
bot: Throttle<Bot>,
msg: Message,
db: DatabaseConnection,
dialogue: MainDialogue,
mut ids: MessageIds,
name: String,
_: String,
) -> crate::Result<()> {
dialogue.exit().await?;
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
Account::delete_by_id((user_id, name)).exec(&db).await?;
ids.alter_message(
&bot,
"The account is successfully deleted",
deletion_markup(),
None,
)
.await?;
Ok(())
}
pub async fn delete(
bot: Throttle<Bot>,
q: CallbackQuery,
db: DatabaseConnection,
dialogue: MainDialogue,
hash: super::NameHash,
) -> crate::Result<()> {
let mut msg: MessageIds = q.message.as_ref().unwrap().into();
let user_id = q.from.id.0;
let name = match name_from_hash(&db, user_id, &hash).await? {
Some(name) => name,
None => {
msg.alter_message(
&bot,
"Account wan't found. Select another one",
menu_markup("delete", user_id, &db).await?,
None,
)
.await?;
return Ok(());
}
};
let previous = bot.send_message(
msg.0,
"Send master password. Once you send correct master password the account is unrecoverable"
).await?;
change_state!(
dialogue,
&previous,
(name),
State::GetMasterPass,
get_master_pass
)
}

View File

@ -10,8 +10,7 @@ pub async fn get(
let user_id = q.from.id.0; let user_id = q.from.id.0;
let msg = q.message.as_ref().unwrap(); let msg = q.message.as_ref().unwrap();
let hash = hex::encode(hash); let name = match name_from_hash(&db, user_id, &hash).await? {
let name = match Account::get_name_by_hash(user_id, hash, &db).await? {
Some(name) => name, Some(name) => name,
None => { None => {
bot.edit_message_text( bot.edit_message_text(
@ -19,13 +18,13 @@ pub async fn get(
msg.id, msg.id,
"Account wan't found. Select another one", "Account wan't found. Select another one",
) )
.reply_markup(menu_markup(user_id, &db).await?) .reply_markup(menu_markup("get", user_id, &db).await?)
.await?; .await?;
return Ok(()); return Ok(());
} }
}; };
let text = format!("Name:\n`{name}`\nLogin:\n***\nPassword:\n***"); let text = format!("Name:\n`{name}`\nLogin:\n\\*\\*\\*\nPassword:\n\\*\\*\\*");
bot.send_message(msg.chat.id, text) bot.send_message(msg.chat.id, text)
.reply_markup(account_markup(&name, true)) .reply_markup(account_markup(&name, true))
.parse_mode(ParseMode::MarkdownV2) .parse_mode(ParseMode::MarkdownV2)

View File

@ -18,9 +18,10 @@ pub async fn get_menu(
bot.edit_message_text(msg.chat.id, msg.id, "You don't have any accounts") bot.edit_message_text(msg.chat.id, msg.id, "You don't have any accounts")
.reply_markup(deletion_markup()) .reply_markup(deletion_markup())
.await?; .await?;
return Ok(());
} }
let markup = spawn_blocking(|| menu_markup_sync(names)).await?; let markup = spawn_blocking(|| menu_markup_sync("get", names)).await?;
bot.edit_message_text(msg.chat.id, msg.id, "Choose your account") bot.edit_message_text(msg.chat.id, msg.id, "Choose your account")
.reply_markup(markup) .reply_markup(markup)
.await?; .await?;

View File

@ -1,9 +1,13 @@
//! This module consists of endpoints to handle callbacks //! This module consists of endpoints to handle callbacks
mod decrypt;
mod delete;
mod delete_message; mod delete_message;
mod get; mod get;
mod get_menu; mod get_menu;
pub use decrypt::decrypt;
pub use delete::delete;
pub use delete_message::delete_message; pub use delete_message::delete_message;
pub use get::get; pub use get::get;
pub use get_menu::get_menu; pub use get_menu::get_menu;
@ -36,7 +40,7 @@ pub enum CallbackCommand {
impl CallbackCommand { impl CallbackCommand {
pub fn from_query(q: CallbackQuery) -> Option<Self> { pub fn from_query(q: CallbackQuery) -> Option<Self> {
q.message.as_ref()?; q.message.as_ref()?;
q.data.and_then(|text| dbg!(text.parse()).ok()) q.data.and_then(|data| data.parse().ok())
} }
} }
@ -47,8 +51,6 @@ impl FromStr for CallbackCommand {
use AlterableField::*; use AlterableField::*;
use CallbackCommand::*; use CallbackCommand::*;
println!("{s}");
match s { match s {
"delete_message" => return Ok(DeleteMessage), "delete_message" => return Ok(DeleteMessage),
"get_menu" => return Ok(GetMenu), "get_menu" => return Ok(GetMenu),

View File

@ -1,40 +1,11 @@
use crate::prelude::*; use crate::prelude::*;
/// Gets the master password and deletes the account. pub async fn delete(bot: Throttle<Bot>, msg: Message, db: DatabaseConnection) -> crate::Result<()> {
/// Although it doesn't use the master password, we get it to be sure that it's the user who used that command
async fn get_master_pass(
bot: Throttle<Bot>,
msg: Message,
db: DatabaseConnection,
dialogue: MainDialogue,
mut ids: MessageIds,
name: String,
_: String,
) -> crate::Result<()> {
dialogue.exit().await?;
let user_id = msg.from().ok_or(NoUserInfo)?.id.0; let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
Account::delete_by_id((user_id, name)).exec(&db).await?;
ids.alter_message( bot.send_message(msg.chat.id, "Choose the account to delete")
&bot, .reply_markup(menu_markup("delete", user_id, &db).await?)
"The account is successfully deleted", .await?;
deletion_markup(),
None,
)
.await?;
Ok(()) Ok(())
} }
handler!(
get_account_name(name: String),
"Send master password. Once you send correct master password the account is unrecoverable",
State::GetMasterPass,
get_master_pass
);
ask_name_handler!(
delete,
"Send the name of the account to delete",
get_account_name
);

View File

@ -1,42 +1,15 @@
use crate::prelude::*; use crate::prelude::*;
use teloxide::types::ParseMode;
use tokio::task::spawn_blocking;
/// Gets the master password, decryptes the account and sends it to the user with copyable fields pub async fn get_account(
async fn get_master_pass(
bot: Throttle<Bot>, bot: Throttle<Bot>,
msg: Message, msg: Message,
db: DatabaseConnection, db: DatabaseConnection,
dialogue: MainDialogue,
mut ids: MessageIds,
name: String,
master_pass: String,
) -> crate::Result<()> { ) -> crate::Result<()> {
dialogue.exit().await?;
let user_id = msg.from().ok_or(NoUserInfo)?.id.0; let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let account = match Account::get(user_id, &name, &db).await? { bot.send_message(msg.chat.id, "Choose the account to get")
Some(account) => account, .reply_markup(menu_markup("decrypt", user_id, &db).await?)
None => {
bot.send_message(msg.chat.id, "Account not found")
.reply_markup(deletion_markup())
.await?;
return Ok(());
}
};
let (login, password) = spawn_blocking(move || account.decrypt(&master_pass)).await??;
let text = format!("Name:\n`{name}`\nLogin:\n`{login}`\nPassword:\n`{password}`");
ids.alter_message(&bot, text, deletion_markup(), ParseMode::MarkdownV2)
.await?; .await?;
Ok(()) Ok(())
} }
handler!(get_account_name(name:String), "Send master password", State::GetMasterPass, get_master_pass);
ask_name_handler!(
get_account,
"Send the name of the account to get",
get_account_name
);

View File

@ -15,7 +15,7 @@ pub async fn menu(bot: Throttle<Bot>, msg: Message, db: DatabaseConnection) -> c
.await?; .await?;
} }
let markup = spawn_blocking(|| menu_markup_sync(names)).await?; let markup = spawn_blocking(|| menu_markup_sync("get", names)).await?;
bot.send_message(msg.chat.id, "Choose your account") bot.send_message(msg.chat.id, "Choose your account")
.reply_markup(markup) .reply_markup(markup)
.await?; .await?;

View File

@ -34,7 +34,7 @@ macro_rules! handler {
#[inline] #[inline]
async fn $function_name( async fn $function_name(
bot: Throttle<Bot>, bot: Throttle<Bot>,
msg: Message, _: Message,
_: DatabaseConnection, _: DatabaseConnection,
dialogue: MainDialogue, dialogue: MainDialogue,
mut ids: MessageIds, mut ids: MessageIds,
@ -47,34 +47,6 @@ macro_rules! handler {
}; };
} }
#[macro_export]
macro_rules! ask_name_handler {
($function_name: ident, $message: literal, $next_func: ident) => {
#[inline]
pub async fn $function_name(
bot: Throttle<Bot>,
msg: Message,
dialogue: MainDialogue,
db: DatabaseConnection,
) -> $crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let markup = account_list_markup(user_id, &db).await?;
if markup.keyboard.is_empty() {
bot.send_message(msg.chat.id, "No accounts found")
.reply_markup(deletion_markup())
.await?;
return Ok(());
}
let previous = bot
.send_message(msg.chat.id, $message)
.reply_markup(markup)
.await?;
$crate::change_state!(dialogue, &previous, (), State::GetExistingName, $next_func)
}
};
}
#[macro_export] #[macro_export]
macro_rules! simple_state_handler { macro_rules! simple_state_handler {
($function_name: ident, $check: ident, $no_text_message: literal) => { ($function_name: ident, $check: ident, $no_text_message: literal) => {

View File

@ -1,5 +1,3 @@
#![allow(unused)]
mod callbacks; mod callbacks;
mod commands; mod commands;
mod default; mod default;
@ -49,8 +47,6 @@ fn get_dispatcher(
let message_handler = Update::filter_message() let message_handler = Update::filter_message()
.map_async(utils::delete_message) .map_async(utils::delete_message)
.enter_dialogue::<Update, InMemStorage<State>, State>()
.branch(case![State::GetExistingName(next)].endpoint(state::get_existing_name))
.branch(case![State::GetNewName(next)].endpoint(state::get_new_name)) .branch(case![State::GetNewName(next)].endpoint(state::get_new_name))
.branch(case![State::GetMasterPass(next)].endpoint(state::get_master_pass)) .branch(case![State::GetMasterPass(next)].endpoint(state::get_master_pass))
.branch(case![State::GetNewMasterPass(next)].endpoint(state::get_new_master_pass)) .branch(case![State::GetNewMasterPass(next)].endpoint(state::get_new_master_pass))
@ -64,9 +60,12 @@ fn get_dispatcher(
.filter_map(CallbackCommand::from_query) .filter_map(CallbackCommand::from_query)
.branch(case![CallbackCommand::GetMenu].endpoint(callbacks::get_menu)) .branch(case![CallbackCommand::GetMenu].endpoint(callbacks::get_menu))
.branch(case![CallbackCommand::DeleteMessage].endpoint(callbacks::delete_message)) .branch(case![CallbackCommand::DeleteMessage].endpoint(callbacks::delete_message))
.branch(case![CallbackCommand::Get(hash)].endpoint(callbacks::get)); .branch(case![CallbackCommand::Get(hash)].endpoint(callbacks::get))
.branch(case![CallbackCommand::Decrypt(hash)].endpoint(callbacks::decrypt))
.branch(case![CallbackCommand::DeleteAccount(hash)].endpoint(callbacks::delete));
let handler = dptree::entry() let handler = dptree::entry()
.enter_dialogue::<Update, InMemStorage<State>, State>()
.branch(message_handler) .branch(message_handler)
.branch(callback_handler); .branch(callback_handler);

View File

@ -2,36 +2,21 @@ use crate::prelude::*;
use base64::{engine::general_purpose::STANDARD_NO_PAD as B64_ENGINE, Engine as _}; use base64::{engine::general_purpose::STANDARD_NO_PAD as B64_ENGINE, Engine as _};
use itertools::Itertools; use itertools::Itertools;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, KeyboardMarkup}; use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup};
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
/// Creates a markup of all user's account names
#[inline] #[inline]
pub async fn account_list_markup( pub fn menu_markup_sync(
user_id: u64, command: &str,
db: &DatabaseConnection, names: impl IntoIterator<Item = String>,
) -> crate::Result<KeyboardMarkup> { ) -> InlineKeyboardMarkup {
let account_names: Vec<Vec<KeyboardButton>> = Account::get_names(user_id, db)
.await?
.map_ok(KeyboardButton::new)
.try_chunks(3)
.map_err(|err| err.1)
.try_collect()
.await?;
let markup = KeyboardMarkup::new(account_names)
.resize_keyboard(true)
.one_time_keyboard(true);
Ok(markup)
}
#[inline]
pub fn menu_markup_sync(names: impl IntoIterator<Item = String>) -> InlineKeyboardMarkup {
let names = names let names = names
.into_iter() .into_iter()
.map(|name| { .map(|name| {
let hash = <Sha256 as Digest>::digest(name.as_bytes()); let hash = <Sha256 as Digest>::digest(name.as_bytes());
let mut data = "get ".to_owned(); let mut data = command.to_owned();
data.reserve(43); data.reserve(44);
data.push(' ');
B64_ENGINE.encode_string(hash, &mut data); B64_ENGINE.encode_string(hash, &mut data);
InlineKeyboardButton::callback(name, data) InlineKeyboardButton::callback(name, data)
}) })
@ -42,12 +27,14 @@ pub fn menu_markup_sync(names: impl IntoIterator<Item = String>) -> InlineKeyboa
#[inline] #[inline]
pub async fn menu_markup( pub async fn menu_markup(
command: impl Into<String>,
user_id: u64, user_id: u64,
db: &DatabaseConnection, db: &DatabaseConnection,
) -> crate::Result<InlineKeyboardMarkup> { ) -> crate::Result<InlineKeyboardMarkup> {
let command: String = command.into();
let names: Vec<String> = Account::get_names(user_id, db).await?.try_collect().await?; let names: Vec<String> = Account::get_names(user_id, db).await?.try_collect().await?;
spawn_blocking(|| menu_markup_sync(names)) spawn_blocking(move || menu_markup_sync(&command, names))
.await .await
.map_err(Into::into) .map_err(Into::into)
} }
@ -69,26 +56,26 @@ pub fn account_markup(name: &str, is_encrypted: bool) -> InlineKeyboardMarkup {
.unwrap(); .unwrap();
let hash = std::str::from_utf8(&hash).unwrap(); let hash = std::str::from_utf8(&hash).unwrap();
let alter_buttons = [ let encryption_button = if is_encrypted {
("Decrypt", "decrypt")
} else {
("Hide", "get")
};
let main_buttons = [
("Alter name", "an"), ("Alter name", "an"),
("Alter login", "al"), ("Alter login", "al"),
("Alter password", "ap"), ("Alter password", "ap"),
encryption_button,
("Delete account", "delete"),
] ]
.map(|(text, command)| make_button(text, command, hash)); .into_iter()
.map(|(text, command)| make_button(text, command, hash))
let mut second_raw = Vec::new(); .chunks(3);
if is_encrypted {
second_raw.push(make_button("Decrypt", "decrypt", hash))
} else {
second_raw.push(make_button("Hide", "hide", hash));
}
second_raw.push(make_button("Delete account", "delete", hash));
let menu_button = InlineKeyboardButton::callback("Back to the menu", "get_menu"); let menu_button = InlineKeyboardButton::callback("Back to the menu", "get_menu");
InlineKeyboardMarkup::new([alter_buttons]) InlineKeyboardMarkup::new(&main_buttons).append_row([menu_button])
.append_row(second_raw)
.append_row([menu_button])
} }
/// Creates a markup with a "Delete message" button. /// Creates a markup with a "Delete message" button.

View File

@ -1,5 +1,4 @@
pub(crate) use crate::{ pub(crate) use crate::{
ask_name_handler,
commands::Command, commands::Command,
errors::*, errors::*,
first_handler, handler, first_handler, handler,

View File

@ -1,60 +0,0 @@
use crate::prelude::*;
/// Function to handle GetExistingName state
pub async fn get_existing_name(
bot: Throttle<Bot>,
msg: Message,
db: DatabaseConnection,
dialogue: MainDialogue,
next: PackagedHandler<String>,
) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let mut handler = next.lock().await;
if handler.func.is_none() {
let _ = dialogue.exit().await;
return Err(HandlerUsed.into());
}
let text = match msg.text() {
Some(text) => text.trim(),
None => {
let msg = bot
.send_message(
msg.chat.id,
"Couldn't get the text of the message. Send the name again",
)
.reply_markup(account_list_markup(user_id, &db).await?)
.await?;
handler.previous = MessageIds::from(&msg);
return Ok(());
}
};
if text == "/cancel" {
dialogue.exit().await?;
handler
.previous
.alter_message(&bot, "Successfully cancelled", deletion_markup(), None)
.await?;
return Ok(());
}
if !Account::exists(user_id, text, &db).await? {
let msg = bot
.send_message(msg.chat.id, "Account doesn't exists. Try again")
.reply_markup(account_list_markup(user_id, &db).await?)
.await?;
handler.previous = MessageIds::from(&msg);
return Ok(());
}
let func = handler.func.take().unwrap();
let text = text.to_owned();
if let Err(err) = func(bot, msg, db, dialogue.clone(), handler.previous, text).await {
let _ = dialogue.exit().await;
return Err(err);
}
Ok(())
}

View File

@ -39,7 +39,7 @@ impl MessageIds {
} }
match edit.send_ref().await { match edit.send_ref().await {
Ok(msg) => return Ok(()), Ok(_) => return Ok(()),
Err(RequestError::Api(_)) => (), Err(RequestError::Api(_)) => (),
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
}; };

View File

@ -1,7 +1,6 @@
//! This module consists of endpoints to handle the state //! This module consists of endpoints to handle the state
mod generic; mod generic;
mod get_existing_name;
mod get_login; mod get_login;
mod get_master_pass; mod get_master_pass;
mod get_new_master_pass; mod get_new_master_pass;
@ -10,7 +9,6 @@ mod get_password;
mod get_user; mod get_user;
mod handler; mod handler;
pub use get_existing_name::get_existing_name;
pub use get_login::get_login; pub use get_login::get_login;
pub use get_master_pass::get_master_pass; pub use get_master_pass::get_master_pass;
pub use get_new_master_pass::get_new_master_pass; pub use get_new_master_pass::get_new_master_pass;
@ -26,7 +24,6 @@ use teloxide::dispatching::dialogue::InMemStorage;
pub enum State { pub enum State {
#[default] #[default]
Start, Start,
GetExistingName(PackagedHandler<String>),
GetNewName(PackagedHandler<String>), GetNewName(PackagedHandler<String>),
GetMasterPass(PackagedHandler<String>), GetMasterPass(PackagedHandler<String>),
GetNewMasterPass(PackagedHandler<String>), GetNewMasterPass(PackagedHandler<String>),

View File

@ -16,3 +16,14 @@ pub fn validate_field(field: &str) -> bool {
.chars() .chars()
.all(|char| !['`', '\\', '\n', '\t'].contains(&char)) .all(|char| !['`', '\\', '\n', '\t'].contains(&char))
} }
pub async fn name_from_hash(
db: &DatabaseConnection,
user_id: u64,
hash: &[u8],
) -> crate::Result<Option<String>> {
let hash = hex::encode(hash);
Account::get_name_by_hash(user_id, hash, db)
.await
.map_err(Into::into)
}