Added support for multiple languages

This commit is contained in:
2024-04-16 16:02:48 +03:00
parent aded63f9d5
commit c5855fced7
52 changed files with 973 additions and 409 deletions

View File

@ -1,22 +1,31 @@
//! This module consists of endpoints to handle callbacks
crate::export_handlers!(decrypt, delete, delete_message, get, get_menu, alter);
crate::export_handlers!(
decrypt,
delete,
delete_message,
get,
get_menu,
alter,
change_locale
);
use crate::errors::InvalidCommand;
use crate::{errors::InvalidCommand, locales::LocaleTypeExt};
use base64::{engine::general_purpose::STANDARD_NO_PAD as B64_ENGINE, Engine as _};
use entity::locale::LocaleType;
use std::str::FromStr;
use teloxide::types::CallbackQuery;
type NameHash = Vec<u8>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AlterableField {
Name,
Login,
Pass,
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub enum CallbackCommand {
DeleteMessage,
Get(NameHash),
@ -25,12 +34,13 @@ pub enum CallbackCommand {
Hide(NameHash),
Alter(NameHash, AlterableField),
DeleteAccount { name: NameHash, is_command: bool },
ChangeLocale(LocaleType),
}
impl CallbackCommand {
pub fn from_query(q: CallbackQuery) -> Option<Self> {
q.message.as_ref()?;
q.data.and_then(|data| data.parse().ok())
q.message?;
q.data?.parse().inspect_err(|err| log::error!("{err}")).ok()
}
}
@ -45,13 +55,19 @@ impl FromStr for CallbackCommand {
};
let mut substrings = s.split(' ');
let (Some(command), Some(name), None) =
let (Some(command), Some(param), None) =
(substrings.next(), substrings.next(), substrings.next())
else {
return Err(InvalidCommand::InvalidParams);
};
let name_hash = B64_ENGINE.decode(name)?;
if command == "change_locale" {
let locale =
LocaleType::from_language_code(param).ok_or(InvalidCommand::UnknownLocale)?;
return Ok(Self::ChangeLocale(locale));
}
let name_hash = B64_ENGINE.decode(param)?;
if name_hash.len() != 32 {
return Err(InvalidCommand::InvalidOutputLength);
}

View File

@ -45,6 +45,7 @@ async fn get_master_pass(
db: Pool,
dialogue: MainDialogue,
mut ids: MessageIds,
locale: LocaleRef,
name: String,
field: AlterableField,
field_value: String,
@ -57,7 +58,7 @@ async fn get_master_pass(
ids.alter_message(
&bot,
"Success. Choose the account to view",
locale.success_choose_account_to_view.as_ref(),
menu_markup("get", user_id, &db).await?,
None,
)
@ -66,7 +67,7 @@ async fn get_master_pass(
Ok(())
}
handler!(get_field(name:String, field:AlterableField, field_value:String), "Send the master password", State::GetMasterPass, get_master_pass);
handler!(get_field(name:String, field:AlterableField, field_value:String),send_master_password, State::GetMasterPass, get_master_pass);
#[inline]
pub async fn alter(
@ -74,6 +75,7 @@ pub async fn alter(
q: CallbackQuery,
db: Pool,
dialogue: MainDialogue,
locale: LocaleRef,
(hash, field): (super::NameHash, AlterableField),
) -> crate::Result<()> {
let user_id = q.from.id.0;
@ -81,7 +83,7 @@ pub async fn alter(
let Some(name) = Account::get_name_by_hash(user_id, &hash, &db).await? else {
bot.send_message(ids.0, "Account wasn't found")
.reply_markup(deletion_markup())
.reply_markup(deletion_markup(locale))
.await?;
bot.answer_callback_query(q.id).await?;
return Ok(());
@ -90,15 +92,15 @@ pub async fn alter(
let text = match field {
Name => {
change_state!(dialogue, ids, (name, field), State::GetNewName, get_field);
"Send new account name"
locale.send_new_name.as_ref()
}
Login => {
change_state!(dialogue, ids, (name, field), State::GetLogin, get_field);
"Send new account login"
locale.send_new_login.as_ref()
}
Pass => {
change_state!(dialogue, ids, (name, field), State::GetPassword, get_field);
"Send new account password"
locale.send_new_password.as_ref()
}
};

View File

@ -0,0 +1,42 @@
use crate::{locales::LocaleTypeExt, prelude::*};
use entity::locale::LocaleType;
#[inline]
pub async fn change_locale(
bot: Throttle<Bot>,
q: CallbackQuery,
db: Pool,
mut locale: LocaleRef,
new_locale: LocaleType,
) -> crate::Result<()> {
let mut ids: MessageIds = q.message.as_ref().unwrap().into();
let user_id = q.from.id.0;
let is_successful = new_locale
.update(user_id, &db)
.await
.inspect_err(|err| log::error!("{err}"))
.unwrap_or(false);
if !is_successful {
ids.alter_message(
&bot,
locale.something_went_wrong.as_ref(),
deletion_markup(locale),
None,
)
.await?;
return Ok(());
}
locale = new_locale.get_locale();
ids.alter_message(
&bot,
locale.choose_language.as_ref(),
language_markup(),
None,
)
.await?;
Ok(())
}

View File

@ -3,12 +3,14 @@ use teloxide::types::ParseMode;
use tokio::task::spawn_blocking;
#[inline]
#[allow(clippy::too_many_arguments)]
async fn get_master_pass(
bot: Throttle<Bot>,
msg: Message,
db: Pool,
dialogue: MainDialogue,
mut ids: MessageIds,
locale: LocaleRef,
name: String,
master_pass: String,
) -> crate::Result<()> {
@ -17,8 +19,8 @@ async fn get_master_pass(
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let Some(account) = Account::get(user_id, &name, &db).await? else {
bot.send_message(msg.chat.id, "Account not found")
.reply_markup(deletion_markup())
bot.send_message(msg.chat.id, locale.no_accounts_found.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
return Ok(());
};
@ -26,15 +28,12 @@ async fn get_master_pass(
let account =
spawn_blocking(move || DecryptedAccount::from_account(account, &master_pass)).await??;
let text = format!(
"Name:\n`{name}`\nLogin:\n`{}`\nPassword:\n`{}`",
account.login, account.password
);
let text = locale.show_account(&account.name, &account.login, &account.password);
ids.alter_message(
&bot,
text,
account_markup(&name, false),
account_markup(&name, false, locale),
ParseMode::MarkdownV2,
)
.await?;
@ -47,20 +46,21 @@ pub async fn decrypt(
q: CallbackQuery,
db: Pool,
dialogue: MainDialogue,
locale: LocaleRef,
hash: super::NameHash,
) -> crate::Result<()> {
let mut ids: MessageIds = q.message.as_ref().unwrap().into();
let user_id = q.from.id.0;
let Some(name) = Account::get_name_by_hash(user_id, &hash, &db).await? else {
bot.send_message(ids.0, "Account wasn't found")
.reply_markup(deletion_markup())
bot.send_message(ids.0, locale.no_accounts_found.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
bot.answer_callback_query(q.id).await?;
return Ok(());
};
ids.alter_message(&bot, "Send master password", None, None)
ids.alter_message(&bot, locale.send_master_password.as_ref(), None, None)
.await?;
bot.answer_callback_query(q.id).await?;

View File

@ -1,12 +1,14 @@
use crate::{change_state, prelude::*};
#[inline]
#[allow(clippy::too_many_arguments)]
async fn get_master_pass(
bot: Throttle<Bot>,
msg: Message,
db: Pool,
dialogue: MainDialogue,
mut ids: MessageIds,
locale: LocaleRef,
name: String,
_: String,
) -> crate::Result<()> {
@ -15,13 +17,8 @@ async fn get_master_pass(
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
Account::delete(user_id, &name, &db).await?;
ids.alter_message(
&bot,
"The account is successfully deleted",
deletion_markup(),
None,
)
.await?;
ids.alter_message(&bot, locale.success.as_ref(), deletion_markup(locale), None)
.await?;
Ok(())
}
@ -32,26 +29,26 @@ pub async fn delete(
q: CallbackQuery,
db: Pool,
dialogue: MainDialogue,
locale: LocaleRef,
(hash, is_command): (super::NameHash, bool),
) -> crate::Result<()> {
const TEXT: &str = "Send master password. \
Once you send the master password the account is unrecoverable";
let text = locale.send_master_pass_to_delete_account.as_ref();
let mut ids: MessageIds = q.message.as_ref().unwrap().into();
let user_id = q.from.id.0;
let Some(name) = Account::get_name_by_hash(user_id, &hash, &db).await? else {
bot.send_message(ids.0, "Account wasn't found")
.reply_markup(deletion_markup())
bot.send_message(ids.0, locale.no_accounts_found.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
bot.answer_callback_query(q.id).await?;
return Ok(());
};
if is_command {
ids.alter_message(&bot, TEXT, None, None).await?;
ids.alter_message(&bot, text, None, None).await?;
} else {
let msg = bot.send_message(ids.0, TEXT).await?;
let msg = bot.send_message(ids.0, text).await?;
ids = MessageIds::from(&msg);
};

View File

@ -2,11 +2,15 @@ use crate::prelude::*;
/// Deletes the message from the callback
#[inline]
pub async fn delete_message(bot: Throttle<Bot>, q: CallbackQuery) -> crate::Result<()> {
pub async fn delete_message(
bot: Throttle<Bot>,
q: CallbackQuery,
locale: LocaleRef,
) -> crate::Result<()> {
if let Some(msg) = q.message {
if bot.delete_message(msg.chat.id, msg.id).await.is_err() {
bot.send_message(msg.chat.id, "Error deleting the message")
.reply_markup(deletion_markup())
bot.send_message(msg.chat.id, locale.error_deleting_message.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
}
}

View File

@ -6,25 +6,26 @@ pub async fn get(
bot: Throttle<Bot>,
q: CallbackQuery,
db: Pool,
locale: LocaleRef,
hash: super::NameHash,
) -> crate::Result<()> {
let user_id = q.from.id.0;
let mut ids: MessageIds = q.message.as_ref().unwrap().into();
let Some(name) = Account::get_name_by_hash(user_id, &hash, &db).await? else {
bot.send_message(ids.0, "Account wasn't found")
.reply_markup(deletion_markup())
bot.send_message(ids.0, locale.account_not_found.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
bot.answer_callback_query(q.id).await?;
return Ok(());
};
let text = format!("Name:\n`{name}`\nLogin:\n\\*\\*\\*\nPassword:\n\\*\\*\\*");
let text = locale.show_hidden_account(&name);
ids.alter_message(
&bot,
text,
account_markup(&name, true),
account_markup(&name, true, locale),
ParseMode::MarkdownV2,
)
.await?;

View File

@ -1,18 +1,28 @@
use crate::prelude::*;
#[inline]
pub async fn get_menu(bot: Throttle<Bot>, q: CallbackQuery, db: Pool) -> crate::Result<()> {
pub async fn get_menu(
bot: Throttle<Bot>,
q: CallbackQuery,
db: Pool,
locale: LocaleRef,
) -> crate::Result<()> {
let user_id = q.from.id.0;
let mut ids: MessageIds = q.message.as_ref().unwrap().into();
let markup = menu_markup("get", user_id, &db).await?;
if markup.inline_keyboard.is_empty() {
ids.alter_message(&bot, "You don't have any accounts", deletion_markup(), None)
.await?;
ids.alter_message(
&bot,
locale.no_accounts_found.as_ref(),
deletion_markup(locale),
None,
)
.await?;
return Ok(());
}
ids.alter_message(&bot, "Choose your account", markup, None)
ids.alter_message(&bot, locale.choose_account.as_ref(), markup, None)
.await?;
bot.answer_callback_query(q.id).await?;
Ok(())

View File

@ -13,41 +13,27 @@ crate::export_handlers!(
import,
menu,
set_master_pass,
start
start,
change_language
);
use teloxide::macros::BotCommands;
#[derive(BotCommands, Clone, Copy)]
#[command(
rename_rule = "snake_case",
description = "These commands are supported:"
)]
#[command(rename_rule = "snake_case")]
pub enum Command {
#[command(description = "displays the welcome message")]
Start,
#[command(description = "displays this text")]
Help,
#[command(description = "sets the master password")]
SetMasterPass,
#[command(description = "gives you a menu to manage your accounts")]
Menu,
#[command(description = "adds the account")]
AddAccount,
#[command(description = "gets the account")]
GetAccount,
#[command(description = "gets a list of accounts")]
GetAccounts,
#[command(description = "deletes the account")]
Delete,
#[command(description = "deletes all the accounts and the master password")]
DeleteAll,
#[command(description = "exports all the accounts in a json file")]
Export,
#[command(description = "loads the accounts from a json file")]
Import,
#[command(description = "generates 10 secure passwords")]
GenPassword,
#[command(description = "cancels the current action")]
Cancel,
ChangeLanguage,
}

View File

@ -10,6 +10,7 @@ async fn get_master_pass(
db: Pool,
dialogue: MainDialogue,
mut ids: MessageIds,
locale: LocaleRef,
name: String,
login: String,
password: String,
@ -30,26 +31,26 @@ async fn get_master_pass(
.await?;
account.insert(&db).await?;
ids.alter_message(&bot, "Success", deletion_markup(), None)
ids.alter_message(&bot, locale.success.as_ref(), deletion_markup(locale), None)
.await?;
Ok(())
}
handler!(
get_password(name:String, login: String, password: String),
"Send master password",
send_master_password,
State::GetMasterPass,
get_master_pass
);
handler!(get_login(name: String, login: String),
"Send password",
send_password,
State::GetPassword,
get_password
);
handler!(get_account_name(name: String), "Send login", State::GetLogin, get_login);
handler!(get_account_name(name: String), send_login, State::GetLogin, get_login);
first_handler!(
add_account,
"Send account name",
send_account_name,
State::GetNewName,
get_account_name
);

View File

@ -2,9 +2,9 @@ use crate::prelude::*;
/// Handles /cancel command when there's no active state
#[inline]
pub async fn cancel(bot: Throttle<Bot>, msg: Message) -> crate::Result<()> {
bot.send_message(msg.chat.id, "Nothing to cancel")
.reply_markup(deletion_markup())
pub async fn cancel(bot: Throttle<Bot>, msg: Message, locale: LocaleRef) -> crate::Result<()> {
bot.send_message(msg.chat.id, locale.nothing_to_cancel.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
Ok(())
}

View File

@ -0,0 +1,13 @@
use crate::prelude::*;
#[inline]
pub async fn change_language(
bot: Throttle<Bot>,
msg: Message,
locale: LocaleRef,
) -> crate::Result<()> {
bot.send_message(msg.chat.id, locale.choose_language.as_ref())
.reply_markup(language_markup())
.await?;
Ok(())
}

View File

@ -1,19 +1,24 @@
use crate::prelude::*;
#[inline]
pub async fn delete(bot: Throttle<Bot>, msg: Message, db: Pool) -> crate::Result<()> {
pub async fn delete(
bot: Throttle<Bot>,
msg: Message,
db: Pool,
locale: LocaleRef,
) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let markup = menu_markup("delete1", user_id, &db).await?;
if markup.inline_keyboard.is_empty() {
bot.send_message(msg.chat.id, "You don't have any accounts")
.reply_markup(deletion_markup())
bot.send_message(msg.chat.id, locale.no_accounts_found.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
return Ok(());
}
bot.send_message(msg.chat.id, "Choose the account to delete")
bot.send_message(msg.chat.id, locale.choose_account.as_ref())
.reply_markup(markup)
.await?;
Ok(())

View File

@ -10,6 +10,7 @@ async fn get_master_pass(
db: Pool,
dialogue: MainDialogue,
mut ids: MessageIds,
locale: LocaleRef,
_: String,
) -> crate::Result<()> {
dialogue.exit().await?;
@ -23,22 +24,22 @@ async fn get_master_pass(
let text = match result {
(Ok(()), Ok(())) => {
txn.commit().await?;
"Everything was deleted"
locale.everything_was_deleted.as_ref()
}
(Err(err), _) | (_, Err(err)) => {
error!("{}", crate::Error::from(err));
txn.rollback().await?;
"Something went wrong. Try again later"
locale.something_went_wrong.as_ref()
}
};
ids.alter_message(&bot, text, deletion_markup(), None)
ids.alter_message(&bot, text, deletion_markup(locale), None)
.await?;
Ok(())
}
first_handler!(
delete_all,
"Send master password to delete EVERYTHING.\nTHIS ACTION IS IRREVERSIBLE",
send_master_pass_to_delete_everything,
State::GetMasterPass,
get_master_pass
);

View File

@ -25,6 +25,7 @@ async fn get_master_pass(
db: Pool,
dialogue: MainDialogue,
ids: MessageIds,
locale: LocaleRef,
master_pass: String,
) -> crate::Result<()> {
dialogue.exit().await?;
@ -51,14 +52,14 @@ async fn get_master_pass(
let file = InputFile::memory(json).file_name("accounts.json");
bot.send_document(msg.chat.id, file)
.reply_markup(deletion_markup())
.reply_markup(deletion_markup(locale))
.await?;
Ok(())
}
first_handler!(
export,
"Send the master password to export your accounts",
send_master_password,
State::GetMasterPass,
get_master_pass
);

View File

@ -15,7 +15,11 @@ const BUFFER_LENGTH: usize =
/// Handles /`gen_password` command by generating 10 copyable passwords and sending them to the user
#[inline]
pub async fn gen_password(bot: Throttle<Bot>, msg: Message) -> crate::Result<()> {
pub async fn gen_password(
bot: Throttle<Bot>,
msg: Message,
locale: LocaleRef,
) -> crate::Result<()> {
let mut message: ArrayString<BUFFER_LENGTH> = MESSAGE_HEADER.try_into().unwrap();
let passwords: PasswordArray = spawn_blocking(generate_passwords).await?;
for password in passwords {
@ -23,7 +27,7 @@ pub async fn gen_password(bot: Throttle<Bot>, msg: Message) -> crate::Result<()>
}
bot.send_message(msg.chat.id, message.as_str())
.parse_mode(ParseMode::MarkdownV2)
.reply_markup(deletion_markup())
.reply_markup(deletion_markup(locale))
.await?;
Ok(())
}

View File

@ -1,19 +1,24 @@
use crate::prelude::*;
#[inline]
pub async fn get_account(bot: Throttle<Bot>, msg: Message, db: Pool) -> crate::Result<()> {
pub async fn get_account(
bot: Throttle<Bot>,
msg: Message,
db: Pool,
locale: LocaleRef,
) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let markup = menu_markup("decrypt", user_id, &db).await?;
if markup.inline_keyboard.is_empty() {
bot.send_message(msg.chat.id, "You don't have any accounts")
.reply_markup(deletion_markup())
bot.send_message(msg.chat.id, locale.no_accounts_found.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
return Ok(());
}
bot.send_message(msg.chat.id, "Choose the account to get")
bot.send_message(msg.chat.id, locale.choose_account.as_ref())
.reply_markup(markup)
.await?;
Ok(())

View File

@ -4,26 +4,32 @@ use teloxide::types::ParseMode;
/// Handles /`get_accounts` command by sending the list of copyable account names to the user
#[inline]
pub async fn get_accounts(bot: Throttle<Bot>, msg: Message, db: Pool) -> crate::Result<()> {
pub async fn get_accounts(
bot: Throttle<Bot>,
msg: Message,
db: Pool,
locale: LocaleRef,
) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let mut account_names = Account::get_names(user_id, &db);
let mut text = if let Some(name) = account_names.try_next().await? {
format!("Accounts:\n`{name}`")
} else {
bot.send_message(msg.chat.id, "No accounts found")
.reply_markup(deletion_markup())
let Some(mut text) = account_names.try_next().await? else {
bot.send_message(msg.chat.id, locale.no_accounts_found.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
return Ok(());
};
text.insert(0, '`');
text.push('`');
while let Some(name) = account_names.try_next().await? {
write!(text, "\n`{name}`")?;
}
bot.send_message(msg.chat.id, text)
.parse_mode(ParseMode::MarkdownV2)
.reply_markup(deletion_markup())
.reply_markup(deletion_markup(locale))
.await?;
Ok(())
}

View File

@ -1,11 +1,10 @@
use crate::prelude::*;
use teloxide::utils::command::BotCommands;
/// Handles the help command by sending the passwords descryptions
#[inline]
pub async fn help(bot: Throttle<Bot>, msg: Message) -> crate::Result<()> {
bot.send_message(msg.chat.id, Command::descriptions().to_string())
.reply_markup(deletion_markup())
pub async fn help(bot: Throttle<Bot>, msg: Message, locale: LocaleRef) -> crate::Result<()> {
bot.send_message(msg.chat.id, locale.help_command.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
Ok(())
}

View File

@ -27,12 +27,14 @@ async fn encrypt_account(
/// Gets the master password, encryptes and adds the accounts to the DB
#[inline]
#[allow(clippy::too_many_arguments)]
async fn get_master_pass(
bot: Throttle<Bot>,
msg: Message,
db: Pool,
dialogue: MainDialogue,
mut ids: MessageIds,
locale: LocaleRef,
user: User,
master_pass: String,
) -> crate::Result<()> {
@ -53,22 +55,18 @@ async fn get_master_pass(
}
let text = if failed.is_empty() {
"Success".to_owned()
locale.success.as_ref().to_owned()
} else {
format!(
"Failed to create the following accounts:\n{}",
"{}:\n{}",
locale.couldnt_create_following_accounts,
failed.into_iter().format("\n")
)
};
ids.alter_message(&bot, text, deletion_markup(), None)
ids.alter_message(&bot, text, deletion_markup(locale), None)
.await?;
Ok(())
}
handler!(get_user(user: User), "Send master password", State::GetMasterPass, get_master_pass);
first_handler!(
import,
"Send a json document with the same format as created by /export",
State::GetUser,
get_user
);
handler!(get_user(user: User),send_master_password, State::GetMasterPass, get_master_pass);
first_handler!(import, send_json_file, State::GetUser, get_user);

View File

@ -1,19 +1,24 @@
use crate::prelude::*;
#[inline]
pub async fn menu(bot: Throttle<Bot>, msg: Message, db: Pool) -> crate::Result<()> {
pub async fn menu(
bot: Throttle<Bot>,
msg: Message,
db: Pool,
locale: LocaleRef,
) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let markup = menu_markup("get", user_id, &db).await?;
if markup.inline_keyboard.is_empty() {
bot.send_message(msg.chat.id, "You don't have any accounts")
.reply_markup(deletion_markup())
bot.send_message(msg.chat.id, locale.no_accounts_found.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
return Ok(());
}
bot.send_message(msg.chat.id, "Choose your account")
bot.send_message(msg.chat.id, locale.choose_account.as_ref())
.reply_markup(markup)
.await?;
Ok(())

View File

@ -1,25 +1,29 @@
use crate::{change_state, prelude::*};
use crate::{change_state, locales::LocaleTypeExt, prelude::*};
use cryptography::hashing::HashedBytes;
use entity::locale::LocaleType;
use tokio::task::spawn_blocking;
#[inline]
#[allow(clippy::too_many_arguments)]
async fn get_master_pass2(
bot: Throttle<Bot>,
msg: Message,
db: Pool,
dialogue: MainDialogue,
mut ids: MessageIds,
locale: LocaleRef,
hash: HashedBytes<[u8; 64], [u8; 64]>,
master_pass: String,
) -> crate::Result<()> {
dialogue.exit().await?;
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let from = msg.from().ok_or(NoUserInfo)?;
let user_id = from.id.0;
if !hash.verify(master_pass.as_bytes()) {
ids.alter_message(
&bot,
"The passwords didn't match. Use the command again",
deletion_markup(),
locale.master_password_dont_match.as_ref(),
deletion_markup(locale),
None,
)
.await?;
@ -32,9 +36,14 @@ async fn get_master_pass2(
password_hash: hash.hash.to_vec(),
salt: hash.salt.to_vec(),
};
model.insert(&db).await?;
let locale_type = from
.language_code
.as_deref()
.and_then(LocaleType::from_language_code)
.unwrap_or_default();
model.insert(&db, locale_type).await?;
ids.alter_message(&bot, "Success", deletion_markup(), None)
ids.alter_message(&bot, locale.success.as_ref(), deletion_markup(locale), None)
.await?;
Ok(())
@ -47,11 +56,13 @@ async fn get_master_pass(
_: Pool,
dialogue: MainDialogue,
mut ids: MessageIds,
locale: LocaleRef,
master_pass: String,
) -> crate::Result<()> {
let hash = spawn_blocking(move || HashedBytes::new(master_pass.as_bytes())).await?;
ids.alter_message(&bot, "Send it again", None, None).await?;
ids.alter_message(&bot, locale.send_master_password_again.as_ref(), None, None)
.await?;
change_state!(
dialogue,
@ -71,16 +82,17 @@ pub async fn set_master_pass(
msg: Message,
dialogue: MainDialogue,
db: Pool,
locale: LocaleRef,
) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
if MasterPass::exists(user_id, &db).await? {
bot.send_message(msg.chat.id, "Master password already exists")
.reply_markup(deletion_markup())
bot.send_message(msg.chat.id, locale.master_password_is_set.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
return Ok(());
}
let previous = bot
.send_message(msg.chat.id, "Send new master password")
.send_message(msg.chat.id, locale.send_new_master_password.as_ref())
.await?;
change_state!(

View File

@ -2,12 +2,8 @@ use crate::prelude::*;
/// Handles /start command by sending the greeting message
#[inline]
pub async fn start(bot: Throttle<Bot>, msg: Message) -> crate::Result<()> {
bot.send_message(
msg.chat.id,
"Hi! This bot can be used to store the passwords securely. \
Use /help command to get the list of commands",
)
.await?;
pub async fn start(bot: Throttle<Bot>, msg: Message, locale: LocaleRef) -> crate::Result<()> {
bot.send_message(msg.chat.id, locale.start_command.as_ref())
.await?;
Ok(())
}

View File

@ -2,12 +2,9 @@ use crate::prelude::*;
/// Handles the messages which weren't matched by any commands or states
#[inline]
pub async fn default(bot: Throttle<Bot>, msg: Message) -> crate::Result<()> {
bot.send_message(
msg.chat.id,
"Unknown command. Use /help command to get the list of commands",
)
.reply_markup(deletion_markup())
.await?;
pub async fn default(bot: Throttle<Bot>, msg: Message, locale: LocaleRef) -> crate::Result<()> {
bot.send_message(msg.chat.id, locale.unknown_command_use_help.as_ref())
.reply_markup(deletion_markup(locale))
.await?;
Ok(())
}

View File

@ -14,4 +14,6 @@ pub enum InvalidCommand {
InvalidOutputLength,
#[error("Error decoding the values: {0}")]
NameDecodingError(#[from] base64::DecodeError),
#[error("Unknown locale passed into callback")]
UnknownLocale,
}

View File

@ -13,13 +13,14 @@ async fn notify_about_no_user_info(
bot: Throttle<Bot>,
msg: Message,
state: State,
locale: LocaleRef,
) -> crate::Result<()> {
const TEXT: &str = "Invalid message. Couldn't get the user information. Send the message again";
let text = locale.couldnt_get_user_info_send_again.as_ref();
match state {
State::Start => {
bot.send_message(msg.chat.id, TEXT)
.reply_markup(deletion_markup())
bot.send_message(msg.chat.id, text)
.reply_markup(deletion_markup(locale))
.await?;
}
State::GetNewName(handler)
@ -30,14 +31,14 @@ async fn notify_about_no_user_info(
let mut handler = handler.lock().await;
handler
.previous
.alter_message(&bot, TEXT, None, None)
.alter_message(&bot, text, None, None)
.await?;
}
State::GetUser(handler) => {
let mut handler = handler.lock().await;
handler
.previous
.alter_message(&bot, TEXT, None, None)
.alter_message(&bot, text, None, None)
.await?;
}
};

166
src/locales.rs Normal file
View File

@ -0,0 +1,166 @@
use crate::prelude::*;
use anyhow::Result;
use entity::locale::LocaleType;
use log::error;
use std::future::Future;
use std::sync::OnceLock;
static LOCALES: OnceLock<LocaleStore> = OnceLock::new();
pub struct LocaleStore {
eng: Locale,
ru: Locale,
}
impl LocaleStore {
pub fn init() -> Result<()> {
let ru = serde_yaml::from_slice(include_bytes!("../locales/ru.yaml"))?;
let eng = serde_yaml::from_slice(include_bytes!("../locales/eng.yaml"))?;
assert!(
LOCALES.set(Self { eng, ru }).is_ok(),
"Locales are already intialized"
);
Ok(())
}
}
#[derive(serde::Deserialize)]
pub struct Locale {
pub master_password_is_not_set: Box<str>,
pub hide_button: Box<str>,
pub change_name_button: Box<str>,
pub change_login_button: Box<str>,
pub change_password_button: Box<str>,
pub delete_account_button: Box<str>,
pub couldnt_get_user_info_send_again: Box<str>,
pub unknown_command_use_help: Box<str>,
pub help_command: Box<str>,
pub no_file_send: Box<str>,
pub file_too_large: Box<str>,
pub couldnt_get_file_name: Box<str>,
pub following_accounts_have_problems: Box<str>,
pub duplicate_names: Box<str>,
pub accounts_already_in_db: Box<str>,
pub invalid_fields: Box<str>,
pub fix_that_and_send_again: Box<str>,
pub error_downloading_file: Box<str>,
pub error_getting_account_names: Box<str>,
pub error_parsing_json_file: Box<str>,
pub successfully_canceled: Box<str>,
pub invalid_password: Box<str>,
pub couldnt_get_message_text: Box<str>,
pub invalid_file_name: Box<str>,
pub account_already_exists: Box<str>,
pub master_password_too_weak: Box<str>,
pub no_lowercase: Box<str>,
pub no_uppercase: Box<str>,
pub no_numbers: Box<str>,
pub master_pass_too_short: Box<str>,
pub change_master_password_and_send_again: Box<str>,
pub wrong_master_password: Box<str>,
pub invalid_login: Box<str>,
pub start_command: Box<str>,
pub master_password_dont_match: Box<str>,
pub success: Box<str>,
pub send_master_password_again: Box<str>,
pub master_password_is_set: Box<str>,
pub send_new_master_password: Box<str>,
pub no_accounts_found: Box<str>,
pub choose_account: Box<str>,
pub couldnt_create_following_accounts: Box<str>,
pub send_master_password: Box<str>,
pub something_went_wrong: Box<str>,
pub nothing_to_cancel: Box<str>,
pub send_account_name: Box<str>,
pub send_login: Box<str>,
pub error_deleting_message: Box<str>,
pub account_not_found: Box<str>,
pub send_new_name: Box<str>,
pub send_new_login: Box<str>,
pub send_new_password: Box<str>,
pub success_choose_account_to_view: Box<str>,
pub send_json_file: Box<str>,
pub send_master_pass_to_delete_everything: Box<str>,
pub everything_was_deleted: Box<str>,
pub send_password: Box<str>,
pub send_master_pass_to_delete_account: Box<str>,
pub no_special_characters: Box<str>,
pub invalid_name: Box<str>,
pub decrypt_button: Box<str>,
pub delete_message_button: Box<str>,
pub menu_button: Box<str>,
pub choose_language: Box<str>,
word_name: Box<str>,
word_login: Box<str>,
word_password: Box<str>,
}
impl Locale {
pub async fn from_update(update: Update, db: Pool) -> &'static Self {
let locale_type = LocaleType::locale_for_update(&update, &db).await;
locale_type.get_locale()
}
pub fn show_account(&self, name: &str, login: &str, password: &str) -> String {
format!(
"{}:\n`{name}`\n{}:\n`{login}`\n{}:\n`{password}`",
self.word_name, self.word_login, self.word_password
)
}
pub fn show_hidden_account(&self, name: &str) -> String {
format!(
"{}:\n`{name}`\n{}:\n\\*\\*\\*\n{}:\n\\*\\*\\*",
self.word_name, self.word_login, self.word_password
)
}
}
pub type LocaleRef = &'static Locale;
pub trait LocaleTypeExt: Sized {
fn locale_for_update(update: &Update, db: &Pool) -> impl Future<Output = Self> + Send;
fn from_language_code(code: &str) -> Option<Self>;
fn get_locale(self) -> &'static Locale;
}
impl LocaleTypeExt for LocaleType {
async fn locale_for_update(update: &Update, db: &Pool) -> Self {
let Some(from) = update.user() else {
return Self::default();
};
match Self::get_from_db(from.id.0, db).await {
Ok(Some(locale)) => return locale,
Ok(None) => (),
Err(err) => error!("{err}"),
}
from.language_code
.as_deref()
.and_then(Self::from_language_code)
.unwrap_or_default()
}
fn from_language_code(code: &str) -> Option<Self> {
match code {
"ru" => Some(Self::Ru),
"en" => Some(Self::Eng),
_ => None,
}
}
fn get_locale(self) -> &'static Locale {
let Some(store) = LOCALES.get() else {
panic!("Locales are not initialized")
};
match self {
Self::Eng => &store.eng,
Self::Ru => &store.ru,
}
}
}

View File

@ -3,7 +3,7 @@ macro_rules! change_state {
($dialogue: expr, $previous: expr, ($($param: ident),*), $next_state: expr, $next_func: ident) => {{
$dialogue
.update($next_state(Handler::new(
move |bot, msg, db, dialogue, ids, param| Box::pin($next_func(bot, msg, db, dialogue, ids, $($param,)* param)),
move |bot, msg, db, dialogue, locale, ids, param| Box::pin($next_func(bot, msg, db, dialogue, locale, ids, $($param,)* param)),
$previous,
)))
.await?;
@ -12,14 +12,17 @@ macro_rules! change_state {
#[macro_export]
macro_rules! first_handler {
($function_name: ident, $message: expr, $next_state: expr, $next_func: ident) => {
($function_name: ident, $message: ident, $next_state: expr, $next_func: ident) => {
#[inline]
pub async fn $function_name(
bot: Throttle<Bot>,
msg: Message,
dialogue: MainDialogue,
locale: LocaleRef,
) -> $crate::Result<()> {
let previous = bot.send_message(msg.chat.id, $message).await?;
let previous = bot
.send_message(msg.chat.id, locale.$message.as_ref())
.await?;
$crate::change_state!(dialogue, &previous, (), $next_state, $next_func);
@ -30,7 +33,7 @@ macro_rules! first_handler {
#[macro_export]
macro_rules! handler {
($function_name: ident ($($param: ident: $type: ty),*), $message: literal, $next_state: expr, $next_func: ident) => {
($function_name: ident ($($param: ident: $type: ty),*), $message: ident, $next_state: expr, $next_func: ident) => {
#[allow(clippy::too_many_arguments)]
#[inline]
async fn $function_name(
@ -39,9 +42,10 @@ macro_rules! handler {
_: Pool,
dialogue: MainDialogue,
mut ids: MessageIds,
locale: LocaleRef,
$($param: $type),*
) -> $crate::Result<()> {
ids.alter_message(&bot, $message, None, None).await?;
ids.alter_message(&bot, locale.$message.as_ref(), None, None).await?;
$crate::change_state!(dialogue, ids, ($($param),*), $next_state, $next_func);
@ -52,13 +56,14 @@ macro_rules! handler {
#[macro_export]
macro_rules! simple_state_handler {
($function_name: ident, $check: ident, $no_text_message: literal) => {
($function_name: ident, $check: ident) => {
#[inline]
pub async fn $function_name(
bot: Throttle<Bot>,
msg: Message,
db: Pool,
dialogue: MainDialogue,
locale: LocaleRef,
next: PackagedHandler<String>,
) -> $crate::Result<()> {
super::generic::generic(
@ -66,8 +71,8 @@ macro_rules! simple_state_handler {
msg,
db,
dialogue,
|msg, db, param| Box::pin($check(msg, db, param)),
$no_text_message,
|msg, db, locale, param| Box::pin($check(msg, db, locale, param)),
locale,
next,
)
.await

View File

@ -4,6 +4,7 @@ mod default;
mod delete_mesage_handler;
mod errors;
mod filter_user_info;
mod locales;
mod macros;
mod markups;
mod master_password_check;
@ -42,7 +43,8 @@ fn get_dispatcher(
.branch(case![Command::Delete].endpoint(commands::delete))
.branch(case![Command::DeleteAll].endpoint(commands::delete_all))
.branch(case![Command::Export].endpoint(commands::export))
.branch(case![Command::Import].endpoint(commands::import));
.branch(case![Command::Import].endpoint(commands::import))
.branch(case![Command::ChangeLanguage].endpoint(commands::change_language));
let message_handler = Update::filter_message()
.map_async(delete_mesage_handler::delete_message)
@ -67,9 +69,11 @@ fn get_dispatcher(
.branch(
case![CallbackCommand::DeleteAccount { name, is_command }].endpoint(callbacks::delete),
)
.branch(case![CallbackCommand::Alter(hash, field)].endpoint(callbacks::alter));
.branch(case![CallbackCommand::Alter(hash, field)].endpoint(callbacks::alter))
.branch(case![CallbackCommand::ChangeLocale(locale)].endpoint(callbacks::change_locale));
let handler = dptree::entry()
.map_async(Locale::from_update)
.enter_dialogue::<Update, InMemStorage<State>, State>()
.branch(message_handler)
.branch(callback_handler);
@ -85,6 +89,8 @@ async fn main() -> Result<()> {
let _ = dotenv();
pretty_env_logger::init();
locales::LocaleStore::init()?;
let token = env::var("TOKEN").expect("expected TOKEN in the enviroment");
let database_url = env::var("DATABASE_URL").expect("expected DATABASE_URL in the enviroment");
let pool = Pool::connect(&database_url).await?;

View File

@ -40,16 +40,16 @@ pub async fn menu_markup(
}
#[inline]
fn make_button(text: &str, command: &str, hash: &str) -> InlineKeyboardButton {
fn make_button(text: &str, command: &str, param: &str) -> InlineKeyboardButton {
let mut data = command.to_owned();
data.reserve(44);
data.reserve(param.len() + 1);
data.push(' ');
data.push_str(hash);
data.push_str(param);
InlineKeyboardButton::callback(text, data)
}
#[inline]
pub fn account_markup(name: &str, is_encrypted: bool) -> InlineKeyboardMarkup {
pub fn account_markup(name: &str, is_encrypted: bool, locale: LocaleRef) -> InlineKeyboardMarkup {
let mut hash = [0; 43];
B64_ENGINE
.encode_slice(<Sha256 as Digest>::digest(name), &mut hash)
@ -57,31 +57,41 @@ pub fn account_markup(name: &str, is_encrypted: bool) -> InlineKeyboardMarkup {
let hash = std::str::from_utf8(&hash).unwrap();
let encryption_button = if is_encrypted {
("Decrypt", "decrypt")
(locale.decrypt_button.as_ref(), "decrypt")
} else {
("Hide", "get")
(locale.hide_button.as_ref(), "get")
};
let main_buttons = [
("Alter name", "an"),
("Alter login", "al"),
("Alter password", "ap"),
(locale.change_name_button.as_ref(), "an"),
(locale.change_login_button.as_ref(), "al"),
(locale.change_password_button.as_ref(), "ap"),
encryption_button,
("Delete account", "delete0"),
(locale.delete_account_button.as_ref(), "delete0"),
]
.into_iter()
.map(|(text, command)| make_button(text, command, hash))
.chunks(3);
let menu_button = InlineKeyboardButton::callback("Back to the menu", "get_menu");
let menu_button = InlineKeyboardButton::callback(locale.menu_button.as_ref(), "get_menu");
InlineKeyboardMarkup::new(&main_buttons).append_row([menu_button])
}
#[inline]
pub fn language_markup() -> InlineKeyboardMarkup {
let languages = [("🇷🇺 Русский", "ru"), ("🇬🇧 English", "en")]
.into_iter()
.map(|(text, param)| make_button(text, "change_locale", param))
.chunks(3);
InlineKeyboardMarkup::new(&languages)
}
/// Creates a markup with a "Delete message" button.
/// This markup should be added for all messages that won't be deleted afterwards
#[inline]
pub fn deletion_markup() -> InlineKeyboardMarkup {
let button = InlineKeyboardButton::callback("Delete message", "delete_message");
pub fn deletion_markup(locale: LocaleRef) -> InlineKeyboardMarkup {
let button =
InlineKeyboardButton::callback(locale.delete_message_button.as_ref(), "delete_message");
InlineKeyboardMarkup::new([[button]])
}

View File

@ -29,6 +29,7 @@ async fn master_pass_exists(update: Update, db: Pool) -> Option<Option<DynError>
#[inline]
async fn notify_about_no_master_pass(
bot: Throttle<Bot>,
locale: LocaleRef,
result: Option<DynError>,
update: Update,
) -> crate::Result<()> {
@ -37,9 +38,9 @@ async fn notify_about_no_master_pass(
}
bot.send_message(
update.chat_id().unwrap(),
"No master password set. Use /set_master_pass to set it",
locale.master_password_is_not_set.as_ref(),
)
.reply_markup(deletion_markup())
.reply_markup(deletion_markup(locale))
.await?;
Ok(())
}

View File

@ -2,6 +2,7 @@ pub use crate::{
commands::Command,
errors::*,
first_handler, handler,
locales::{Locale, LocaleRef},
markups::*,
models::*,
state::{Handler, MainDialogue, MessageIds, PackagedHandler, State},

View File

@ -9,11 +9,16 @@ pub async fn generic<F>(
db: Pool,
dialogue: MainDialogue,
check: F,
no_text_message: impl Into<String> + Send,
locale: LocaleRef,
handler: PackagedHandler<String>,
) -> crate::Result<()>
where
for<'a> F: FnOnce(&'a Message, &'a Pool, &'a str) -> BoxFuture<'a, crate::Result<Option<String>>>
for<'a> F: FnOnce(
&'a Message,
&'a Pool,
LocaleRef,
&'a str,
) -> BoxFuture<'a, crate::Result<Option<String>>>
+ Send,
{
let mut handler = handler.lock().await;
@ -25,7 +30,7 @@ where
let Some(text) = msg.text().map(str::trim) else {
handler
.previous
.alter_message(&bot, no_text_message, None, None)
.alter_message(&bot, locale.couldnt_get_message_text.as_ref(), None, None)
.await?;
return Ok(());
};
@ -34,12 +39,17 @@ where
dialogue.exit().await?;
handler
.previous
.alter_message(&bot, "Successfully cancelled", deletion_markup(), None)
.alter_message(
&bot,
locale.successfully_canceled.as_ref(),
deletion_markup(locale),
None,
)
.await?;
return Ok(());
}
if let Some(text) = check(&msg, &db, text).await? {
if let Some(text) = check(&msg, &db, locale, text).await? {
handler
.previous
.alter_message(&bot, text, None, None)
@ -52,7 +62,7 @@ where
drop(handler);
let text = text.to_owned();
if let Err(err) = func(bot, msg, db, dialogue.clone(), previous, text).await {
if let Err(err) = func(bot, msg, db, dialogue.clone(), previous, locale, text).await {
let _ = dialogue.exit().await;
return Err(err);
}

View File

@ -1,16 +1,17 @@
use crate::prelude::*;
#[inline]
async fn check_login(_: &Message, _: &Pool, login: &str) -> crate::Result<Option<String>> {
async fn check_login(
_: &Message,
_: &Pool,
locale: LocaleRef,
login: &str,
) -> crate::Result<Option<String>> {
let is_valid = validate_field(login);
if !is_valid {
return Ok(Some("Invalid login. Try again".to_owned()));
return Ok(Some(locale.invalid_login.as_ref().into()));
}
Ok(None)
}
crate::simple_state_handler!(
get_login,
check_login,
"Couldn't get the text of the message. Send the login again"
);
crate::simple_state_handler!(get_login, check_login);

View File

@ -8,14 +8,13 @@ use tokio::task::spawn_blocking;
async fn check_master_pass(
msg: &Message,
db: &Pool,
locale: LocaleRef,
master_pass: &str,
) -> crate::Result<Option<String>> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let Some(model) = MasterPass::get(user_id, db).await? else {
error!("User was put into the GetMasterPass state with no master password set");
return Ok(Some(
"No master password set. Use /cancel and set it by using /set_master_pass".to_owned(),
));
return Ok(Some(locale.master_password_is_not_set.as_ref().into()));
};
let is_valid = {
@ -25,13 +24,9 @@ async fn check_master_pass(
};
if !is_valid {
return Ok(Some("Wrong master password. Try again".to_owned()));
return Ok(Some(locale.wrong_master_password.as_ref().into()));
}
Ok(None)
}
crate::simple_state_handler!(
get_master_pass,
check_master_pass,
"Couldn't get the text of the message. Send the master password again"
);
crate::simple_state_handler!(get_master_pass, check_master_pass);

View File

@ -1,31 +1,32 @@
use crate::prelude::*;
use cryptography::passwords::{check_master_pass, PasswordValidity};
use std::fmt::Write as _;
#[inline]
fn process_validity(validity: PasswordValidity) -> Result<(), String> {
fn process_validity(validity: PasswordValidity, locale: LocaleRef) -> Result<(), String> {
if validity.is_empty() {
return Ok(());
}
let mut error_text = "Your master password is invalid:\n".to_owned();
let mut error_text = locale.master_password_too_weak.as_ref().to_owned();
if validity.contains(PasswordValidity::NO_LOWERCASE) {
error_text.push_str("\n* It doesn't have any lowercase characters");
write!(error_text, "\n* {}", locale.no_lowercase).unwrap();
}
if validity.contains(PasswordValidity::NO_UPPERCASE) {
error_text.push_str("\n* It doesn't have any uppercase characters");
write!(error_text, "\n* {}", locale.no_uppercase).unwrap();
}
if validity.contains(PasswordValidity::NO_NUMBER) {
error_text.push_str("\n* It doesn't have any numbers");
write!(error_text, "\n* {}", locale.no_numbers).unwrap();
}
if validity.contains(PasswordValidity::NO_SPECIAL_CHARACTER) {
error_text.push_str("\n* It doesn't have any special characters");
write!(error_text, "\n* {}", locale.no_special_characters).unwrap();
}
if validity.contains(PasswordValidity::TOO_SHORT) {
error_text.push_str("\n* It is shorter than 8 characters");
write!(error_text, "\n* {}", locale.master_pass_too_short).unwrap();
}
error_text.push_str("\n\nModify your password and send it again");
error_text.push_str(&locale.change_master_password_and_send_again);
Err(error_text)
}
@ -35,15 +36,12 @@ fn process_validity(validity: PasswordValidity) -> Result<(), String> {
async fn check_new_master_pass(
_: &Message,
_: &Pool,
locale: LocaleRef,
password: &str,
) -> crate::Result<Option<String>> {
let validity = check_master_pass(password);
Ok(process_validity(validity).err())
Ok(process_validity(validity, locale).err())
}
crate::simple_state_handler!(
get_new_master_pass,
check_new_master_pass,
"Couldn't get the text of the message. Send the master password again"
);
crate::simple_state_handler!(get_new_master_pass, check_new_master_pass);

View File

@ -5,21 +5,18 @@ use crate::prelude::*;
async fn check_new_account_name(
msg: &Message,
db: &Pool,
locale: LocaleRef,
name: &str,
) -> crate::Result<Option<String>> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
if Account::exists(user_id, name, db).await? {
Ok(Some("Account already exists".to_owned()))
Ok(Some(locale.account_already_exists.as_ref().into()))
} else if !validate_field(name) {
Ok(Some("Invalid account name. Try again".to_owned()))
Ok(Some(locale.invalid_name.as_ref().into()))
} else {
Ok(None)
}
}
crate::simple_state_handler!(
get_new_name,
check_new_account_name,
"Couldn't get the text of the message. Send the name of the new account again"
);
crate::simple_state_handler!(get_new_name, check_new_account_name);

View File

@ -1,16 +1,17 @@
use crate::prelude::*;
#[inline]
async fn check_password(_: &Message, _: &Pool, password: &str) -> crate::Result<Option<String>> {
async fn check_password(
_: &Message,
_: &Pool,
locale: LocaleRef,
password: &str,
) -> crate::Result<Option<String>> {
let is_valid = validate_field(password);
if !is_valid {
return Ok(Some("Invalid password. Try again".to_owned()));
return Ok(Some(locale.invalid_password.as_ref().into()));
}
Ok(None)
}
crate::simple_state_handler!(
get_password,
check_password,
"Couldn't get the text of the message. Send the password again"
);
crate::simple_state_handler!(get_password, check_password);

View File

@ -9,24 +9,43 @@ use teloxide::{
use tokio::{task::spawn_blocking, try_join};
use trim_in_place::TrimInPlace;
#[derive(Clone, Copy)]
enum InvalidDocument {
NoFileSend,
FileTooLarge,
CouldntGetFileName,
InvalidFileName,
}
impl InvalidDocument {
const fn into_str(self, locale: LocaleRef) -> &'static str {
match self {
Self::NoFileSend => &locale.no_file_send,
Self::FileTooLarge => &locale.file_too_large,
Self::CouldntGetFileName => &locale.couldnt_get_file_name,
Self::InvalidFileName => &locale.invalid_file_name,
}
}
}
#[inline]
fn validate_document(document: Option<&Document>) -> Result<&Document, &'static str> {
fn validate_document(document: Option<&Document>) -> Result<&Document, InvalidDocument> {
let Some(document) = document else {
return Err("You didn't send a file. Try again");
return Err(InvalidDocument::NoFileSend);
};
if document.file.size > 1024 * 1024 * 200 {
return Err("The file is larger than 200 MiB. Try splitting it into multiple files");
return Err(InvalidDocument::FileTooLarge);
}
let name = match document.file_name.as_deref() {
Some(name) => Path::new(name.trim_end()),
None => return Err("Couldn't get the name of the file. Try sending it again"),
None => return Err(InvalidDocument::CouldntGetFileName),
};
match name.extension() {
Some(ext) if ext.eq_ignore_ascii_case("json") => Ok(document),
_ => Err("Invalid file name. You need to send a json file. Try again"),
_ => Err(InvalidDocument::InvalidFileName),
}
}
@ -45,6 +64,7 @@ async fn download_file(bot: &Throttle<Bot>, file: &FileMeta) -> crate::Result<Bo
fn process_accounts(
accounts: &mut [DecryptedAccount],
existing_names: ahash::HashSet<String>,
locale: LocaleRef,
) -> crate::Result<Result<(), String>> {
for account in accounts.iter_mut() {
account.name.trim_in_place();
@ -82,12 +102,13 @@ fn process_accounts(
return Ok(Ok(()));
}
let mut error_text = "Your accounts have the following problems:".to_owned();
let mut error_text = locale.following_accounts_have_problems.as_ref().to_owned();
if !duplicates.is_empty() {
write!(
error_text,
"\n\nDuplicate names:\n{:?}",
"\n\n{}:\n{:?}",
locale.duplicate_names,
duplicates.into_iter().format("\n")
)?;
}
@ -95,7 +116,8 @@ fn process_accounts(
if !existing.is_empty() {
write!(
error_text,
"\n\nAccounts with these names already exist in the database:\n{:?}",
"\n\n{}:\n{:?}",
locale.accounts_already_in_db,
existing.into_iter().format("\n")
)?;
}
@ -103,12 +125,13 @@ fn process_accounts(
if !invalid.is_empty() {
write!(
error_text,
"\n\nInvalid account fields:\n{:?}",
"\n\n{}:\n{:?}",
locale.invalid_fields,
invalid.into_iter().format("\n")
)?;
}
error_text.push_str("\n\nFix these problems and send the file again");
error_text.push_str(&locale.fix_that_and_send_again);
Ok(Err(error_text))
}
@ -117,10 +140,11 @@ fn process_accounts(
fn user_from_bytes(
bytes: impl AsRef<[u8]>,
existing_names: ahash::HashSet<String>,
locale: LocaleRef,
) -> crate::Result<Result<User, String>> {
let mut user: User = serde_json::from_slice(bytes.as_ref())?;
drop(bytes);
match process_accounts(&mut user.accounts, existing_names)? {
match process_accounts(&mut user.accounts, existing_names, locale)? {
Ok(()) => Ok(Ok(user)),
Err(error_text) => Ok(Err(error_text)),
}
@ -132,21 +156,24 @@ async fn user_from_document(
db: &Pool,
document: Option<&Document>,
user_id: u64,
locale: LocaleRef,
) -> Result<User, Cow<'static, str>> {
let (data, existing_names) = {
let file = &validate_document(document)?.file;
let data = download_file(bot, file).map_err(|_| "Error downloading the file. Try again");
let file = &validate_document(document)
.map_err(|err| err.into_str(locale))?
.file;
let data = download_file(bot, file).map_err(|_| locale.error_downloading_file.as_ref());
let existing_names = Account::get_names(user_id, db)
.try_collect()
.map_err(|_| "Error getting existing account names. Try again");
.map_err(|_| locale.error_getting_account_names.as_ref());
try_join!(data, existing_names)?
};
match spawn_blocking(|| user_from_bytes(data, existing_names)).await {
match spawn_blocking(|| user_from_bytes(data, existing_names, locale)).await {
Ok(Ok(user)) => user.map_err(Cow::Owned),
_ => Err(Cow::Borrowed("Error parsing the json file. Try again")),
_ => Err(Cow::Borrowed(&locale.error_parsing_json_file)),
}
}
@ -157,6 +184,7 @@ pub async fn get_user(
msg: Message,
db: Pool,
dialogue: MainDialogue,
locale: LocaleRef,
handler: PackagedHandler<User>,
) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
@ -171,12 +199,17 @@ pub async fn get_user(
dialogue.exit().await?;
handler
.previous
.alter_message(&bot, "Successfully cancelled", deletion_markup(), None)
.alter_message(
&bot,
locale.successfully_canceled.as_ref(),
deletion_markup(locale),
None,
)
.await?;
return Ok(());
}
let user = match user_from_document(&bot, &db, msg.document(), user_id).await {
let user = match user_from_document(&bot, &db, msg.document(), user_id, locale).await {
Ok(user) => user,
Err(error_text) => {
handler
@ -191,7 +224,7 @@ pub async fn get_user(
let func = handler.func.take().unwrap();
drop(handler);
if let Err(err) = func(bot, msg, db, dialogue.clone(), previous, user).await {
if let Err(err) = func(bot, msg, db, dialogue.clone(), previous, locale, user).await {
let _ = dialogue.exit().await;
return Err(err);
}

View File

@ -68,6 +68,7 @@ type DynHanlder<T> = Box<
Pool,
MainDialogue,
MessageIds,
LocaleRef,
T,
) -> BoxFuture<'static, crate::Result<()>>
+ Send,
@ -92,6 +93,7 @@ impl<T> Handler<T> {
Pool,
MainDialogue,
MessageIds,
LocaleRef,
T,
) -> BoxFuture<'static, crate::Result<()>>
+ Send