diff --git a/.sqlx/query-1fc824ca1ca447a990c3e68ee4f2a15b8e5bc260641057d914bb7ff871b51aa3.json b/.sqlx/query-1fc824ca1ca447a990c3e68ee4f2a15b8e5bc260641057d914bb7ff871b51aa3.json deleted file mode 100644 index a741c4b..0000000 --- a/.sqlx/query-1fc824ca1ca447a990c3e68ee4f2a15b8e5bc260641057d914bb7ff871b51aa3.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "MySQL", - "query": "INSERT INTO master_pass VALUES (?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "1fc824ca1ca447a990c3e68ee4f2a15b8e5bc260641057d914bb7ff871b51aa3" -} diff --git a/.sqlx/query-71661faf7bde5ba94a5090dde7fb3ee72e9b2860692e30333f2213188188fdb6.json b/.sqlx/query-71661faf7bde5ba94a5090dde7fb3ee72e9b2860692e30333f2213188188fdb6.json new file mode 100644 index 0000000..dedd4a2 --- /dev/null +++ b/.sqlx/query-71661faf7bde5ba94a5090dde7fb3ee72e9b2860692e30333f2213188188fdb6.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "UPDATE master_pass SET locale = ? WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "71661faf7bde5ba94a5090dde7fb3ee72e9b2860692e30333f2213188188fdb6" +} diff --git a/.sqlx/query-ff36e44aa2d064634d3ad183ce503cd30555d1d745cf5ab486c0d17e8e9a026f.json b/.sqlx/query-ff36e44aa2d064634d3ad183ce503cd30555d1d745cf5ab486c0d17e8e9a026f.json new file mode 100644 index 0000000..4bb686e --- /dev/null +++ b/.sqlx/query-ff36e44aa2d064634d3ad183ce503cd30555d1d745cf5ab486c0d17e8e9a026f.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "INSERT INTO master_pass VALUES (?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "ff36e44aa2d064634d3ad183ce503cd30555d1d745cf5ab486c0d17e8e9a026f" +} diff --git a/Cargo.lock b/Cargo.lock index 9601e98..54f1eb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,24 +42,24 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "aquamarine" @@ -89,27 +89,17 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic-write-file" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8204db279bf648d64fe845bd8840f78b39c8132ed4d6a4194c3b10d4b4cfb0b" -dependencies = [ - "nix", - "rand", -] - [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -146,9 +136,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] @@ -164,9 +154,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -176,15 +166,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.90" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" [[package]] name = "cfg-if" @@ -192,12 +182,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "chacha20" version = "0.9.1" @@ -224,9 +208,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "num-traits", ] @@ -281,9 +265,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.0.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" dependencies = [ "crc-catalog", ] @@ -325,7 +309,7 @@ name = "cryptography" version = "0.1.0" dependencies = [ "arrayvec", - "bitflags 2.4.2", + "bitflags 2.5.0", "chacha20poly1305", "entity", "once_cell", @@ -375,9 +359,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", @@ -426,18 +410,18 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" dependencies = [ "serde", ] [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -509,9 +493,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "finl_unicode" @@ -612,7 +596,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.59", ] [[package]] @@ -657,9 +641,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", @@ -674,9 +658,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -854,9 +838,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", @@ -908,9 +892,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" @@ -987,9 +971,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mime" @@ -1039,18 +1023,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.4.2", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -1180,6 +1152,7 @@ dependencies = [ "pretty_env_logger", "serde", "serde_json", + "serde_yaml", "sha2", "teloxide", "thiserror", @@ -1246,14 +1219,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.59", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1341,18 +1314,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1407,9 +1380,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -1430,15 +1403,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.24" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", "bytes", @@ -1530,11 +1503,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -1638,14 +1611,14 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.59", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -1676,6 +1649,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1728,9 +1714,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -1780,9 +1766,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" dependencies = [ "sqlx-core", "sqlx-macros", @@ -1793,9 +1779,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ "ahash", "atoi", @@ -1803,7 +1789,6 @@ dependencies = [ "bytes", "crc", "crossbeam-queue", - "dotenvy", "either", "event-listener", "futures-channel", @@ -1836,9 +1821,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ "proc-macro2", "quote", @@ -1849,11 +1834,10 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ - "atomic-write-file", "dotenvy", "either", "heck", @@ -1874,13 +1858,13 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.4.2", + "bitflags 2.5.0", "byteorder", "bytes", "crc", @@ -1916,13 +1900,13 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.4.2", + "bitflags 2.5.0", "byteorder", "crc", "dotenvy", @@ -1943,7 +1927,6 @@ dependencies = [ "rand", "serde", "serde_json", - "sha1", "sha2", "smallvec", "sqlx-core", @@ -1955,9 +1938,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", "flume", @@ -2011,9 +1994,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" dependencies = [ "proc-macro2", "quote", @@ -2152,22 +2135,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.59", ] [[package]] @@ -2187,9 +2170,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -2211,7 +2194,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.59", ] [[package]] @@ -2226,9 +2209,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -2275,7 +2258,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.59", ] [[package]] @@ -2357,6 +2340,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -2383,9 +2372,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "uuid" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", ] @@ -2450,7 +2439,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.59", "wasm-bindgen-shared", ] @@ -2484,7 +2473,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.59", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2526,9 +2515,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "whoami" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fec781d48b41f8163426ed18e8fc2864c12937df9ce54c88ede7bd47270893e" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ "redox_syscall", "wasite", @@ -2580,7 +2569,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -2600,17 +2589,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -2621,9 +2611,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -2633,9 +2623,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -2645,9 +2635,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -2657,9 +2653,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -2669,9 +2665,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -2681,9 +2677,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -2693,9 +2689,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winreg" @@ -2724,7 +2720,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.59", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bfab646..d2df14b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ parking_lot = "0.12" pretty_env_logger = "0.5" serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_yaml = "0.9" sha2 = "0.10" teloxide = { version = "0.12", features = [ "macros", diff --git a/entity/migrations/0002_locales.down.sql b/entity/migrations/0002_locales.down.sql new file mode 100644 index 0000000..dda6bce --- /dev/null +++ b/entity/migrations/0002_locales.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +ALTER TABLE master_pass +DROP COLUMN locale; \ No newline at end of file diff --git a/entity/migrations/0002_locales.up.sql b/entity/migrations/0002_locales.up.sql new file mode 100644 index 0000000..b7cf8f5 --- /dev/null +++ b/entity/migrations/0002_locales.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here +ALTER TABLE master_pass +ADD COLUMN locale SMALLINT UNSIGNED NOT NULL DEFAULT 1; \ No newline at end of file diff --git a/entity/src/lib.rs b/entity/src/lib.rs index 3e71fab..69cd245 100644 --- a/entity/src/lib.rs +++ b/entity/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::missing_errors_doc)] pub mod account; +pub mod locale; pub mod master_pass; pub mod prelude; diff --git a/entity/src/locale.rs b/entity/src/locale.rs new file mode 100644 index 0000000..32b81e0 --- /dev/null +++ b/entity/src/locale.rs @@ -0,0 +1,50 @@ +use crate::prelude::*; +use sqlx::{mysql::MySqlQueryResult as QueryResult, query, query_as}; + +#[derive(Clone, Copy, Default)] +#[allow(clippy::module_name_repetitions)] +pub enum LocaleType { + #[default] + Eng = 1, + Ru = 2, +} + +impl TryFrom for LocaleType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(Self::Eng), + 2 => Ok(Self::Ru), + _ => Err(()), + } + } +} + +impl From for u8 { + fn from(value: LocaleType) -> Self { + value as Self + } +} + +impl LocaleType { + pub async fn get_from_db(user_id: u64, db: &Pool) -> crate::Result> { + let result: Option<(u8,)> = query_as("SELECT locale FROM master_pass WHERE user_id = ?") + .bind(user_id) + .fetch_optional(db) + .await?; + Ok(result.and_then(|val| val.0.try_into().ok())) + } + + pub async fn update(self, user_id: u64, db: &Pool) -> crate::Result { + let result: QueryResult = query!( + "UPDATE master_pass SET locale = ? WHERE user_id = ?", + u8::from(self), + user_id + ) + .execute(db) + .await?; + + Ok(result.rows_affected() == 1) + } +} diff --git a/entity/src/master_pass.rs b/entity/src/master_pass.rs index 04dc1d6..5173ecf 100644 --- a/entity/src/master_pass.rs +++ b/entity/src/master_pass.rs @@ -1,4 +1,4 @@ -use super::Pool; +use crate::{locale::LocaleType, Pool}; use sqlx::{prelude::FromRow, query, query_as, Executor, MySql}; #[derive(Clone, Debug, PartialEq, FromRow, Eq)] @@ -11,12 +11,14 @@ pub struct MasterPass { impl MasterPass { // Inserts the master password into DB #[inline] - pub async fn insert(&self, pool: &Pool) -> crate::Result<()> { + pub async fn insert(&self, pool: &Pool, locale: LocaleType) -> crate::Result<()> { + let locale: u8 = locale.into(); query!( - "INSERT INTO master_pass VALUES (?, ?, ?)", + "INSERT INTO master_pass VALUES (?, ?, ?, ?)", self.user_id, self.salt, - self.password_hash + self.password_hash, + locale ) .execute(pool) .await @@ -26,7 +28,7 @@ impl MasterPass { /// Gets the master password from the database #[inline] pub async fn get(user_id: u64, pool: &Pool) -> crate::Result> { - query_as("SELECT * FROM master_pass WHERE user_id = ?") + query_as("SELECT user_id, salt, password_hash FROM master_pass WHERE user_id = ?") .bind(user_id) .fetch_optional(pool) .await diff --git a/locales/eng.yaml b/locales/eng.yaml new file mode 100644 index 0000000..93a2a30 --- /dev/null +++ b/locales/eng.yaml @@ -0,0 +1,83 @@ +master_password_is_not_set: "Master password is not set. Use /cancel to cancel the current action and /set_master_pass to set it" +hide_button: "Hide" +change_name_button: "Change name" +change_login_button: "Change login" +change_password_button: "Change password" +delete_account_button: "Delete account" +couldnt_get_user_info_send_again: "Couldn't get user information. Send the message again" +unknown_command_use_help: "Unknown command. Use /help to get info on existing commands" +help_command: | + These commands are supported: + + /start — displays the welcome message + /help — displays this text + /set_master_pass — sets the master password + /menu — gives you a menu to manage your accounts + /add_account — adds the account + /get_account — gets the account + /get_accounts — gets a list of accounts + /delete — deletes the account + /delete_all — deletes all the accounts and the master password + /export — exports all the accounts in a json file + /import — loads the accounts from a json file + /gen_password — generates 10 secure passwords + /cancel — cancels the current action + /change_language - allows you to change the language +no_file_send: "Expected you to send a file" +file_too_large: "File is larger that 200 MiB. Please split it into multiple files" +couldnt_get_file_name: "Couldn't get file name. Please try again" +following_accounts_have_problems: "Following accounts have problems:" +duplicate_names: "Duplicate names" +accounts_already_in_db: "Accounts with these names already exist in the database" +invalid_fields: "Accounts have invalid names" +fix_that_and_send_again: "Fix those errors and send the file again" +error_downloading_file: "Error downloading the file" +error_getting_account_names: "Error getting existing account names" +error_parsing_json_file: "Error parsing the json file" +successfully_canceled: "Successfully canceled" +invalid_password: "The password is invalid. Change it and send it again" +couldnt_get_message_text: "Couldn't get the text of the message. Send the message again" +invalid_file_name: "Invalid file name. The program expected you to send a file with .json extension" +account_already_exists: "Account with that names already exists" +master_password_too_weak: "Master password is too weak" +no_lowercase: "It doesn't have any lowercase characters" +no_uppercase: "It doesn't have any uppercase characters" +no_numbers: "It doesn't have any numbers" +master_pass_too_short: "It is shorter than 8 characters" +change_master_password_and_send_again: "Change the master password and send it again" +wrong_master_password: "Wrong master password. Try again" +invalid_login: "Invalid login. Change it and send it again" +start_command: "Hi! This bot can be used to store the passwords securely. Use /help command to get the list of commands" +master_password_dont_match: "Master password didn't match. Use the command again" +success: "Success" +send_master_password_again: "Send the master password again" +master_password_is_set: "Master password has already been set" +send_new_master_password: "Send new master password" +no_accounts_found: "No accounts found" +choose_account: "Choose the account" +couldnt_create_following_accounts: "Couldn't create the following accounts" +send_master_password: "Send the master password" +something_went_wrong: "Something went wrong. Try again later" +nothing_to_cancel: "Nothing to cancel" +send_account_name: "Send account name" +send_login: "Send login" +error_deleting_message: "Error deleting the message" +account_not_found: "Account wasn't found" +send_new_name: "Send new name" +send_new_login: "Send new login" +send_new_password: "Send new password" +success_choose_account_to_view: "Success. Chose an account to view" +send_json_file: "Send the json file to import" +send_master_pass_to_delete_everything: "Send master password to delete EVERY ACCOUNT. THIS ACTION CANNOT BE UNDONE" +everything_was_deleted: "Every account was deleted" +send_password: "Send password" +send_master_pass_to_delete_account: "Send the master password to delete the account" +no_special_characters: "No special characters in the master password" +invalid_name: "Invalid name" +word_name: "Name" +word_login: "Login" +word_password: "Password" +decrypt_button: "Decrypt" +delete_message_button: "Delete message" +menu_button: "Back to the menu" +choose_language: "Choose the language" diff --git a/locales/ru.yaml b/locales/ru.yaml new file mode 100644 index 0000000..666b238 --- /dev/null +++ b/locales/ru.yaml @@ -0,0 +1,83 @@ +master_password_is_not_set: "Мастер-пароль не установлен. Используйте /cancel, чтобы отменить текущее действие, и /set_master_pass, чтобы установить его" +hide_button: "Скрыть" +change_name_button: "Изменить имя" +change_login_button: "Изменить логин" +change_password_button: "Изменить пароль" +delete_account_button: "Удалить аккаунт" +couldnt_get_user_info_send_again: "Не удалось получить информацию о пользователе. Отправьте сообщение еще раз" +unknown_command_use_help: "Неизвестная команда. Используйте /help, чтобы получить информацию о существующих командах" +help_command: | + Поддерживаются следующие команды: + + /start — отображает приветственное сообщение + /help — отображает этот текст + /set_master_pass — устанавливает мастер-пароль + /menu — предоставляет меню для управления вашими аккаунтами + /add_account — добавляет аккаунт + /get_account — получает аккаунт + /get_accounts — получает список аккаунтов + /delete — удаляет аккаунт + /delete_all — удаляет все аккаунты и мастер-пароль + /export — экспортирует все аккаунты в файл json + /import — загружает аккаунты из файла json + /gen_password — генерирует 10 сложных паролей + /cancel — отменяет текущее действие + /change_language - позволяет изменить язык +no_file_send: "Ожидалось, что вы отправите файл" +file_too_large: "Файл больше 200 MiB. Пожалуйста, разделите его на несколько файлов" +couldnt_get_file_name: "Не удалось получить имя файла. Пожалуйста, попробуйте еще раз" +following_accounts_have_problems: "У следующих аккаунтов есть проблемы:" +duplicate_names: "Дублирующиеся имена" +accounts_already_in_db: "Аккаунты с такими именами уже существуют в базе данных" +invalid_fields: "Аккаунты имеют недопустимые имена" +fix_that_and_send_again: "Исправьте эти ошибки и отправьте файл снова" +error_downloading_file: "Ошибка загрузки файла" +error_getting_account_names: "Ошибка получения существующих имен аккаунтов" +error_parsing_json_file: "Ошибка разбора файла json" +successfully_canceled: "Успешная отменена" +invalid_password: "Неверный пароль. Измените его и отправьте снова" +couldnt_get_message_text: "Не удалось получить текст сообщения. Отправьте сообщение еще раз" +invalid_file_name: "Недопустимое имя файла. Программа ожидала, что вы отправите файл с расширением .json" +account_already_exists: "Аккаунт с таким именем уже существует" +master_password_too_weak: "Мастер-пароль слишком слабый" +no_lowercase: "Отсутствуют строчные буквы" +no_uppercase: "Отсутствуют заглавные буквы" +no_numbers: "Отсутствуют цифры" +master_pass_too_short: "Меньше 8 символов" +change_master_password_and_send_again: "Измените мастер-пароль и отправьте его снова" +wrong_master_password: "Неверный мастер-пароль. Попробуйте снова" +invalid_login: "Недопустимый логин. Измените его и отправьте снова" +start_command: "Привет! Этот бот может использоваться для безопасного хранения паролей. Используйте команду /help, чтобы получить список команд" +master_password_dont_match: "Мастер-пароли не совпадают. Используйте команду снова" +success: "Успешно" +send_master_password_again: "Отправьте мастер-пароль снова" +master_password_is_set: "Мастер-пароль уже установлен" +send_new_master_password: "Отправьте новый мастер-пароль" +no_accounts_found: "Аккаунты не найдены" +choose_account: "Выберите аккаунт" +couldnt_create_following_accounts: "Не удалось создать следующие аккаунты" +send_master_password: "Отправьте мастер-пароль" +something_went_wrong: "Что-то пошло не так. Попробуйте еще раз позже" +nothing_to_cancel: "Нечего отменять" +send_account_name: "Отправьте имя аккаунта" +send_login: "Отправьте логин" +error_deleting_message: "Ошибка удаления сообщения" +account_not_found: "Аккаунт не найден" +send_new_name: "Отправьте новое имя" +send_new_login: "Отправьте новый логин" +send_new_password: "Отправьте новый пароль" +success_choose_account_to_view: "Успешно. Выберите аккаунт для просмотра" +send_json_file: "Отправьте файл json для импорта" +send_master_pass_to_delete_everything: "Отправьте мастер-пароль, чтобы удалить ВСЕ АККАУНТЫ. ЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ" +everything_was_deleted: "Все аккаунты были удалены" +send_password: "Отправьте пароль" +send_master_pass_to_delete_account: "Отправьте мастер-пароль, чтобы удалить аккаунт" +no_special_characters: "Нет специальных символов в мастер-пароле" +invalid_name: "Недопустимое имя" +word_name: "Имя" +word_login: "Логин" +word_password: "Пароль" +decrypt_button: "Дешифровать" +delete_message_button: "Удалить сообщение" +menu_button: "Назад в меню" +choose_language: "Выберете язык" diff --git a/src/callbacks.rs b/src/callbacks.rs index 1fc8888..2b6d9bc 100644 --- a/src/callbacks.rs +++ b/src/callbacks.rs @@ -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; -#[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 { - 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); } diff --git a/src/callbacks/alter.rs b/src/callbacks/alter.rs index 75ef38a..b19205b 100644 --- a/src/callbacks/alter.rs +++ b/src/callbacks/alter.rs @@ -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() } }; diff --git a/src/callbacks/change_locale.rs b/src/callbacks/change_locale.rs new file mode 100644 index 0000000..9b60194 --- /dev/null +++ b/src/callbacks/change_locale.rs @@ -0,0 +1,42 @@ +use crate::{locales::LocaleTypeExt, prelude::*}; +use entity::locale::LocaleType; + +#[inline] +pub async fn change_locale( + bot: Throttle, + 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(()) +} diff --git a/src/callbacks/decrypt.rs b/src/callbacks/decrypt.rs index 5565732..33f9851 100644 --- a/src/callbacks/decrypt.rs +++ b/src/callbacks/decrypt.rs @@ -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, 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?; diff --git a/src/callbacks/delete.rs b/src/callbacks/delete.rs index ab4a492..4a31cd4 100644 --- a/src/callbacks/delete.rs +++ b/src/callbacks/delete.rs @@ -1,12 +1,14 @@ use crate::{change_state, prelude::*}; #[inline] +#[allow(clippy::too_many_arguments)] async fn get_master_pass( bot: Throttle, 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); }; diff --git a/src/callbacks/delete_message.rs b/src/callbacks/delete_message.rs index 771b343..3cef74d 100644 --- a/src/callbacks/delete_message.rs +++ b/src/callbacks/delete_message.rs @@ -2,11 +2,15 @@ use crate::prelude::*; /// Deletes the message from the callback #[inline] -pub async fn delete_message(bot: Throttle, q: CallbackQuery) -> crate::Result<()> { +pub async fn delete_message( + bot: Throttle, + 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?; } } diff --git a/src/callbacks/get.rs b/src/callbacks/get.rs index 098dc1a..096d212 100644 --- a/src/callbacks/get.rs +++ b/src/callbacks/get.rs @@ -6,25 +6,26 @@ pub async fn get( bot: Throttle, 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?; diff --git a/src/callbacks/get_menu.rs b/src/callbacks/get_menu.rs index c9bccad..bea3fc1 100644 --- a/src/callbacks/get_menu.rs +++ b/src/callbacks/get_menu.rs @@ -1,18 +1,28 @@ use crate::prelude::*; #[inline] -pub async fn get_menu(bot: Throttle, q: CallbackQuery, db: Pool) -> crate::Result<()> { +pub async fn get_menu( + bot: Throttle, + 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(()) diff --git a/src/commands.rs b/src/commands.rs index 7c10c7b..38c11bd 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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, } diff --git a/src/commands/add_account.rs b/src/commands/add_account.rs index 9a55cc5..f1e1dd4 100644 --- a/src/commands/add_account.rs +++ b/src/commands/add_account.rs @@ -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 ); diff --git a/src/commands/cancel.rs b/src/commands/cancel.rs index c42a988..03cc4d3 100644 --- a/src/commands/cancel.rs +++ b/src/commands/cancel.rs @@ -2,9 +2,9 @@ use crate::prelude::*; /// Handles /cancel command when there's no active state #[inline] -pub async fn cancel(bot: Throttle, msg: Message) -> crate::Result<()> { - bot.send_message(msg.chat.id, "Nothing to cancel") - .reply_markup(deletion_markup()) +pub async fn cancel(bot: Throttle, 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(()) } diff --git a/src/commands/change_language.rs b/src/commands/change_language.rs new file mode 100644 index 0000000..902f86e --- /dev/null +++ b/src/commands/change_language.rs @@ -0,0 +1,13 @@ +use crate::prelude::*; + +#[inline] +pub async fn change_language( + bot: Throttle, + msg: Message, + locale: LocaleRef, +) -> crate::Result<()> { + bot.send_message(msg.chat.id, locale.choose_language.as_ref()) + .reply_markup(language_markup()) + .await?; + Ok(()) +} diff --git a/src/commands/delete.rs b/src/commands/delete.rs index 7507788..0b748c3 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -1,19 +1,24 @@ use crate::prelude::*; #[inline] -pub async fn delete(bot: Throttle, msg: Message, db: Pool) -> crate::Result<()> { +pub async fn delete( + bot: Throttle, + 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(()) diff --git a/src/commands/delete_all.rs b/src/commands/delete_all.rs index 9ce7b7e..67b46ce 100644 --- a/src/commands/delete_all.rs +++ b/src/commands/delete_all.rs @@ -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 ); diff --git a/src/commands/export.rs b/src/commands/export.rs index 2d740af..754a8e1 100644 --- a/src/commands/export.rs +++ b/src/commands/export.rs @@ -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 ); diff --git a/src/commands/gen_password.rs b/src/commands/gen_password.rs index 104c885..9c14cf3 100644 --- a/src/commands/gen_password.rs +++ b/src/commands/gen_password.rs @@ -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, msg: Message) -> crate::Result<()> { +pub async fn gen_password( + bot: Throttle, + msg: Message, + locale: LocaleRef, +) -> crate::Result<()> { let mut message: ArrayString = 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, 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(()) } diff --git a/src/commands/get_account.rs b/src/commands/get_account.rs index 76f9581..a926b6d 100644 --- a/src/commands/get_account.rs +++ b/src/commands/get_account.rs @@ -1,19 +1,24 @@ use crate::prelude::*; #[inline] -pub async fn get_account(bot: Throttle, msg: Message, db: Pool) -> crate::Result<()> { +pub async fn get_account( + bot: Throttle, + 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(()) diff --git a/src/commands/get_accounts.rs b/src/commands/get_accounts.rs index 6a95de8..dafeeeb 100644 --- a/src/commands/get_accounts.rs +++ b/src/commands/get_accounts.rs @@ -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, msg: Message, db: Pool) -> crate::Result<()> { +pub async fn get_accounts( + bot: Throttle, + 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(()) } diff --git a/src/commands/help.rs b/src/commands/help.rs index 539e1bd..463ab5c 100644 --- a/src/commands/help.rs +++ b/src/commands/help.rs @@ -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, msg: Message) -> crate::Result<()> { - bot.send_message(msg.chat.id, Command::descriptions().to_string()) - .reply_markup(deletion_markup()) +pub async fn help(bot: Throttle, msg: Message, locale: LocaleRef) -> crate::Result<()> { + bot.send_message(msg.chat.id, locale.help_command.as_ref()) + .reply_markup(deletion_markup(locale)) .await?; Ok(()) } diff --git a/src/commands/import.rs b/src/commands/import.rs index ecd26ae..cc29d49 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -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, 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); diff --git a/src/commands/menu.rs b/src/commands/menu.rs index 3ee704b..c3f836d 100644 --- a/src/commands/menu.rs +++ b/src/commands/menu.rs @@ -1,19 +1,24 @@ use crate::prelude::*; #[inline] -pub async fn menu(bot: Throttle, msg: Message, db: Pool) -> crate::Result<()> { +pub async fn menu( + bot: Throttle, + 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(()) diff --git a/src/commands/set_master_pass.rs b/src/commands/set_master_pass.rs index b3a04d7..4f308ef 100644 --- a/src/commands/set_master_pass.rs +++ b/src/commands/set_master_pass.rs @@ -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, 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!( diff --git a/src/commands/start.rs b/src/commands/start.rs index c99ae71..a8b21b2 100644 --- a/src/commands/start.rs +++ b/src/commands/start.rs @@ -2,12 +2,8 @@ use crate::prelude::*; /// Handles /start command by sending the greeting message #[inline] -pub async fn start(bot: Throttle, 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, msg: Message, locale: LocaleRef) -> crate::Result<()> { + bot.send_message(msg.chat.id, locale.start_command.as_ref()) + .await?; Ok(()) } diff --git a/src/default.rs b/src/default.rs index 07cfa0e..7061c55 100644 --- a/src/default.rs +++ b/src/default.rs @@ -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, 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, 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(()) } diff --git a/src/errors.rs b/src/errors.rs index bae9762..689ac26 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -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, } diff --git a/src/filter_user_info.rs b/src/filter_user_info.rs index c30ea80..02c8e09 100644 --- a/src/filter_user_info.rs +++ b/src/filter_user_info.rs @@ -13,13 +13,14 @@ async fn notify_about_no_user_info( bot: Throttle, 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?; } }; diff --git a/src/locales.rs b/src/locales.rs new file mode 100644 index 0000000..8c6203e --- /dev/null +++ b/src/locales.rs @@ -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 = 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, + pub hide_button: Box, + pub change_name_button: Box, + pub change_login_button: Box, + pub change_password_button: Box, + pub delete_account_button: Box, + pub couldnt_get_user_info_send_again: Box, + pub unknown_command_use_help: Box, + pub help_command: Box, + pub no_file_send: Box, + pub file_too_large: Box, + pub couldnt_get_file_name: Box, + pub following_accounts_have_problems: Box, + pub duplicate_names: Box, + pub accounts_already_in_db: Box, + pub invalid_fields: Box, + pub fix_that_and_send_again: Box, + pub error_downloading_file: Box, + pub error_getting_account_names: Box, + pub error_parsing_json_file: Box, + pub successfully_canceled: Box, + pub invalid_password: Box, + pub couldnt_get_message_text: Box, + pub invalid_file_name: Box, + pub account_already_exists: Box, + pub master_password_too_weak: Box, + pub no_lowercase: Box, + pub no_uppercase: Box, + pub no_numbers: Box, + pub master_pass_too_short: Box, + pub change_master_password_and_send_again: Box, + pub wrong_master_password: Box, + pub invalid_login: Box, + pub start_command: Box, + pub master_password_dont_match: Box, + pub success: Box, + pub send_master_password_again: Box, + pub master_password_is_set: Box, + pub send_new_master_password: Box, + pub no_accounts_found: Box, + pub choose_account: Box, + pub couldnt_create_following_accounts: Box, + pub send_master_password: Box, + pub something_went_wrong: Box, + pub nothing_to_cancel: Box, + pub send_account_name: Box, + pub send_login: Box, + pub error_deleting_message: Box, + pub account_not_found: Box, + pub send_new_name: Box, + pub send_new_login: Box, + pub send_new_password: Box, + pub success_choose_account_to_view: Box, + pub send_json_file: Box, + pub send_master_pass_to_delete_everything: Box, + pub everything_was_deleted: Box, + pub send_password: Box, + pub send_master_pass_to_delete_account: Box, + pub no_special_characters: Box, + pub invalid_name: Box, + pub decrypt_button: Box, + pub delete_message_button: Box, + pub menu_button: Box, + pub choose_language: Box, + word_name: Box, + word_login: Box, + word_password: Box, +} + +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 + Send; + + fn from_language_code(code: &str) -> Option; + + 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 { + 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, + } + } +} diff --git a/src/macros.rs b/src/macros.rs index 359cf41..102e7d2 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -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, 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, msg: Message, db: Pool, dialogue: MainDialogue, + locale: LocaleRef, next: PackagedHandler, ) -> $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 diff --git a/src/main.rs b/src/main.rs index 01fbf1a..82ecf83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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::, 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?; diff --git a/src/markups.rs b/src/markups.rs index 8b865c4..11edb36 100644 --- a/src/markups.rs +++ b/src/markups.rs @@ -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(::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]]) } diff --git a/src/master_password_check.rs b/src/master_password_check.rs index 6fedab2..1ea07f2 100644 --- a/src/master_password_check.rs +++ b/src/master_password_check.rs @@ -29,6 +29,7 @@ async fn master_pass_exists(update: Update, db: Pool) -> Option #[inline] async fn notify_about_no_master_pass( bot: Throttle, + locale: LocaleRef, result: Option, 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(()) } diff --git a/src/prelude.rs b/src/prelude.rs index 15bad65..3fc1108 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -2,6 +2,7 @@ pub use crate::{ commands::Command, errors::*, first_handler, handler, + locales::{Locale, LocaleRef}, markups::*, models::*, state::{Handler, MainDialogue, MessageIds, PackagedHandler, State}, diff --git a/src/state/generic.rs b/src/state/generic.rs index a124f09..9a3cb0b 100644 --- a/src/state/generic.rs +++ b/src/state/generic.rs @@ -9,11 +9,16 @@ pub async fn generic( db: Pool, dialogue: MainDialogue, check: F, - no_text_message: impl Into + Send, + locale: LocaleRef, handler: PackagedHandler, ) -> crate::Result<()> where - for<'a> F: FnOnce(&'a Message, &'a Pool, &'a str) -> BoxFuture<'a, crate::Result>> + for<'a> F: FnOnce( + &'a Message, + &'a Pool, + LocaleRef, + &'a str, + ) -> BoxFuture<'a, crate::Result>> + 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); } diff --git a/src/state/get_login.rs b/src/state/get_login.rs index 6c9c15a..7d70ebc 100644 --- a/src/state/get_login.rs +++ b/src/state/get_login.rs @@ -1,16 +1,17 @@ use crate::prelude::*; #[inline] -async fn check_login(_: &Message, _: &Pool, login: &str) -> crate::Result> { +async fn check_login( + _: &Message, + _: &Pool, + locale: LocaleRef, + login: &str, +) -> crate::Result> { 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); diff --git a/src/state/get_master_pass.rs b/src/state/get_master_pass.rs index 4a9de43..ddf5a1b 100644 --- a/src/state/get_master_pass.rs +++ b/src/state/get_master_pass.rs @@ -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> { 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); diff --git a/src/state/get_new_master_pass.rs b/src/state/get_new_master_pass.rs index b33df6d..5cf3044 100644 --- a/src/state/get_new_master_pass.rs +++ b/src/state/get_new_master_pass.rs @@ -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> { 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); diff --git a/src/state/get_new_name.rs b/src/state/get_new_name.rs index ec76531..38e6b1b 100644 --- a/src/state/get_new_name.rs +++ b/src/state/get_new_name.rs @@ -5,21 +5,18 @@ use crate::prelude::*; async fn check_new_account_name( msg: &Message, db: &Pool, + locale: LocaleRef, name: &str, ) -> crate::Result> { 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); diff --git a/src/state/get_password.rs b/src/state/get_password.rs index e214981..4937311 100644 --- a/src/state/get_password.rs +++ b/src/state/get_password.rs @@ -1,16 +1,17 @@ use crate::prelude::*; #[inline] -async fn check_password(_: &Message, _: &Pool, password: &str) -> crate::Result> { +async fn check_password( + _: &Message, + _: &Pool, + locale: LocaleRef, + password: &str, +) -> crate::Result> { 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); diff --git a/src/state/get_user.rs b/src/state/get_user.rs index d8e73ff..f564d68 100644 --- a/src/state/get_user.rs +++ b/src/state/get_user.rs @@ -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, file: &FileMeta) -> crate::Result, + locale: LocaleRef, ) -> crate::Result> { 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, + locale: LocaleRef, ) -> crate::Result> { 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> { 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, ) -> 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); } diff --git a/src/state/handler.rs b/src/state/handler.rs index 2638428..2cc777a 100644 --- a/src/state/handler.rs +++ b/src/state/handler.rs @@ -68,6 +68,7 @@ type DynHanlder = Box< Pool, MainDialogue, MessageIds, + LocaleRef, T, ) -> BoxFuture<'static, crate::Result<()>> + Send, @@ -92,6 +93,7 @@ impl Handler { Pool, MainDialogue, MessageIds, + LocaleRef, T, ) -> BoxFuture<'static, crate::Result<()>> + Send