From 42fd8f571e6e80a79ca6aa8cd1f7febbf27bd0ca Mon Sep 17 00:00:00 2001 From: shiroyashik Date: Wed, 8 Jan 2025 18:22:57 +0300 Subject: [PATCH] The first completed version! --- Cargo.lock | 504 +++++++------ Cargo.toml | 13 +- LICENSE | 674 ++++++++++++++++++ README.md | 31 +- {entity => database}/Cargo.toml | 5 +- {migration => database/migration}/Cargo.toml | 0 {migration => database/migration}/README.md | 0 {migration => database/migration}/src/lib.rs | 6 +- .../src/m20241211_182453_create_tables.rs | 190 +++++ {migration => database/migration}/src/main.rs | 0 database/src/actions.rs | 33 + .../actions.rs => database/src/archived.rs | 14 +- {entity => database}/src/lib.rs | 4 +- {entity => database}/src/moderators.rs | 4 +- {entity => database}/src/prelude.rs | 3 + .../src/videos.rs => database/src/requests.rs | 25 +- database/src/users.rs | 17 + database/src/videos.rs | 34 + entity/src/sea_orm_active_enums.rs | 14 - .../src/m20220101_000001_create_table.rs | 123 ---- src/handle/about.rs | 20 + src/handle/add.rs | 172 +++++ src/handle/archive.rs | 132 ++++ src/handle/ban.rs | 22 + src/handle/info.rs | 177 +++++ src/handle/list.rs | 84 +++ src/handle/mod.rs | 101 +++ src/handle/moderator/add.rs | 43 ++ src/handle/moderator/list.rs | 26 + src/handle/moderator/mod.rs | 3 + src/handle/moderator/remove.rs | 51 ++ src/handle/notify.rs | 39 + src/handle/start.rs | 24 + src/inline.rs | 45 ++ src/main.rs | 620 +++------------- src/markup.rs | 14 + youtube/src/lib.rs | 5 +- 37 files changed, 2320 insertions(+), 952 deletions(-) create mode 100644 LICENSE rename {entity => database}/Cargo.toml (57%) rename {migration => database/migration}/Cargo.toml (100%) rename {migration => database/migration}/README.md (100%) rename {migration => database/migration}/src/lib.rs (60%) create mode 100644 database/migration/src/m20241211_182453_create_tables.rs rename {migration => database/migration}/src/main.rs (100%) create mode 100644 database/src/actions.rs rename entity/src/actions.rs => database/src/archived.rs (70%) rename {entity => database}/src/lib.rs (71%) rename {entity => database}/src/moderators.rs (86%) rename {entity => database}/src/prelude.rs (59%) rename entity/src/videos.rs => database/src/requests.rs (54%) create mode 100644 database/src/users.rs create mode 100644 database/src/videos.rs delete mode 100644 entity/src/sea_orm_active_enums.rs delete mode 100644 migration/src/m20220101_000001_create_table.rs create mode 100644 src/handle/about.rs create mode 100644 src/handle/add.rs create mode 100644 src/handle/archive.rs create mode 100644 src/handle/ban.rs create mode 100644 src/handle/info.rs create mode 100644 src/handle/list.rs create mode 100644 src/handle/mod.rs create mode 100644 src/handle/moderator/add.rs create mode 100644 src/handle/moderator/list.rs create mode 100644 src/handle/moderator/mod.rs create mode 100644 src/handle/moderator/remove.rs create mode 100644 src/handle/notify.rs create mode 100644 src/handle/start.rs create mode 100644 src/inline.rs create mode 100644 src/markup.rs diff --git a/Cargo.lock b/Cargo.lock index 7a7ea08..9ef75a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,18 +28,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -127,9 +115,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "aquamarine" @@ -142,7 +130,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -238,7 +226,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 5.3.1", + "event-listener 5.4.0", "event-listener-strategy", "pin-project-lite", ] @@ -289,7 +277,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -300,13 +288,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -365,9 +353,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bigdecimal" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" dependencies = [ "autocfg", "libm", @@ -446,7 +434,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -491,9 +479,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.2.2" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" dependencies = [ "shlex", ] @@ -512,9 +500,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -527,9 +515,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.22" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b" +checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd" dependencies = [ "clap_builder", "clap_derive", @@ -537,9 +525,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.22" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1" +checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd" dependencies = [ "anstream", "anstyle", @@ -549,21 +537,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" @@ -634,18 +622,18 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" @@ -701,7 +689,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -723,7 +711,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -740,6 +728,13 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "database" +version = "0.1.0" +dependencies = [ + "sea-orm", +] + [[package]] name = "der" version = "0.7.9" @@ -771,7 +766,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -794,18 +789,18 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] name = "doggy-watch" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "chrono", "dashmap", + "database", "dotenvy", - "entity", "indexmap", "lazy_static", "sea-orm", @@ -851,13 +846,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "entity" -version = "0.1.0" -dependencies = [ - "sea-orm", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -903,9 +891,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -918,15 +906,15 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ - "event-listener 5.3.1", + "event-listener 5.4.0", "pin-project-lite", ] [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "flume" @@ -945,6 +933,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1055,7 +1049,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -1117,9 +1111,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "gloo-timers" @@ -1177,7 +1171,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.8", + "ahash", ] [[package]] @@ -1185,24 +1179,25 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.11", - "allocator-api2", -] [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] @@ -1249,11 +1244,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1326,9 +1321,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.31" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1350,9 +1345,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", @@ -1370,13 +1365,13 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.2.0", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-util", "rustls", "rustls-pki-types", @@ -1392,7 +1387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.31", + "hyper 0.14.32", "native-tls", "tokio", "tokio-native-tls", @@ -1406,7 +1401,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-util", "native-tls", "tokio", @@ -1425,7 +1420,7 @@ dependencies = [ "futures-util", "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.1", + "hyper 1.5.2", "pin-project-lite", "socket2", "tokio", @@ -1571,7 +1566,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -1638,7 +1633,7 @@ checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -1679,9 +1674,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.74" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ "once_cell", "wasm-bindgen", @@ -1707,9 +1702,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.167" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libm" @@ -1730,9 +1725,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" @@ -1808,17 +1803,11 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] @@ -1851,16 +1840,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1936,9 +1915,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -1972,7 +1951,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -2024,7 +2003,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -2062,12 +2041,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2085,29 +2058,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2235,7 +2208,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -2255,7 +2228,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", "version_check", "yansi", ] @@ -2282,9 +2255,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2336,9 +2309,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] @@ -2410,7 +2383,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.31", + "hyper 0.14.32", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2441,9 +2414,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.9" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64 0.22.1", "bytes", @@ -2454,7 +2427,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-rustls", "hyper-tls 0.6.0", "hyper-util", @@ -2474,6 +2447,7 @@ dependencies = [ "system-configuration 0.6.1", "tokio", "tokio-native-tls", + "tower", "tower-service", "url", "wasm-bindgen", @@ -2579,22 +2553,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.41" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.19" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "ring", @@ -2624,9 +2598,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-webpki" @@ -2670,14 +2644,14 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] name = "sea-orm" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b24d72a69e89762982c29af249542b06c59fa131f87cc9d5b94be1f692b427a" +checksum = "0dbcf83248860dc632c46c7e81a221e041b50d0006191756cb001d9e8afc60a9" dependencies = [ "async-stream", "async-trait", @@ -2694,7 +2668,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "url", @@ -2703,9 +2677,9 @@ dependencies = [ [[package]] name = "sea-orm-cli" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653da4aba23cb596bf03d6f5faad274c74852c8ef014171a4fb9518032377105" +checksum = "7a8dbef29c7e534a8e9afb49abfa946c7a9df3d85f610dfed9140215ff8bff17" dependencies = [ "chrono", "clap", @@ -2720,23 +2694,23 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0497f4fd82ecb2a222bea5319b9048f8ab58d4e734d095b062987acbcdeecdda" +checksum = "49ce6f08134f3681b1ca92185b96fac898f26d9b4f5538d13f7032ef243d14b2" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", "sea-bae", - "syn 2.0.90", + "syn 2.0.95", "unicode-ident", ] [[package]] name = "sea-orm-migration" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b271c0dd7729623d8debb5f017806f066902e3c287f08dc5ff312c3277dc6f" +checksum = "2e53f46fe9874161ba57b15ff45d2589d754d7cbbbaab9470cb79cbada149ca7" dependencies = [ "async-trait", "clap", @@ -2792,15 +2766,15 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.90", - "thiserror", + "syn 2.0.95", + "thiserror 1.0.69", ] [[package]] name = "sea-schema" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aab1592d17860a9a8584d9b549aebcd06f7bdc3ff615f71752486ba0b05b1e6e" +checksum = "0ef5dd7848c993f3789d09a2616484c72c9330cae2b048df59d8c9b8c0343e95" dependencies = [ "futures", "sea-query", @@ -2816,7 +2790,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -2840,9 +2814,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -2850,35 +2824,35 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "itoa", "memchr", @@ -3029,21 +3003,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" dependencies = [ "sqlx-core", "sqlx-macros", @@ -3054,32 +3018,27 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ - "atoi", "bigdecimal", - "byteorder", "bytes", "chrono", "crc", "crossbeam-queue", "either", - "event-listener 5.3.1", - "futures-channel", + "event-listener 5.4.0", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "hashlink", - "hex", "indexmap", "log", "memchr", "once_cell", - "paste", "percent-encoding", "rust_decimal", "rustls", @@ -3088,8 +3047,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "sqlformat", - "thiserror", + "thiserror 2.0.9", "time", "tokio", "tokio-stream", @@ -3101,22 +3059,22 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] name = "sqlx-macros-core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" dependencies = [ "dotenvy", "either", @@ -3132,7 +3090,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.90", + "syn 2.0.95", "tempfile", "tokio", "url", @@ -3140,9 +3098,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" dependencies = [ "atoi", "base64 0.22.1", @@ -3178,7 +3136,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.9", "time", "tracing", "uuid", @@ -3187,9 +3145,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" dependencies = [ "atoi", "base64 0.22.1", @@ -3202,7 +3160,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -3222,7 +3179,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.9", "time", "tracing", "uuid", @@ -3231,9 +3188,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" dependencies = [ "atoi", "chrono", @@ -3315,9 +3272,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", @@ -3347,7 +3304,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -3429,7 +3386,7 @@ dependencies = [ "serde_json", "teloxide-core", "teloxide-macros", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tokio-util", @@ -3459,7 +3416,7 @@ dependencies = [ "serde_with", "take_mut", "takecell", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-util", "url", @@ -3480,12 +3437,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -3497,7 +3455,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl 2.0.9", ] [[package]] @@ -3508,7 +3475,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.95", ] [[package]] @@ -3564,9 +3542,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -3603,7 +3581,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -3628,9 +3606,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -3667,6 +3645,27 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -3693,7 +3692,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -3759,15 +3758,15 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" @@ -3790,12 +3789,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "untrusted" version = "0.9.0" @@ -3890,9 +3883,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -3901,24 +3894,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.47" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfaf8f50e5f293737ee323940c7d8b08a66a95a419223d9f41610ca08b0833d" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", @@ -3929,9 +3921,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3939,22 +3931,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wasm-streams" @@ -3971,9 +3963,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.74" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98bc3c33f0fe7e59ad7cd041b89034fa82a7c2d4365ca538dda6cdaf513863c" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -4209,9 +4201,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" dependencies = [ "memchr", ] @@ -4273,7 +4265,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", "synstructure", ] @@ -4281,7 +4273,7 @@ dependencies = [ name = "youtube" version = "0.1.0" dependencies = [ - "reqwest 0.12.9", + "reqwest 0.12.12", "serde", "serde_json", "tokio", @@ -4306,7 +4298,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] [[package]] @@ -4326,7 +4318,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", "synstructure", ] @@ -4355,5 +4347,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.95", ] diff --git a/Cargo.toml b/Cargo.toml index b4a5a6d..37eaaf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "doggy-watch" -version = "0.1.0" +authors = ["Shiroyashik "] +version = "0.2.0" edition = "2021" publish = false [workspace] -members = [ "entity", "migration", "youtube"] +members = [ "database", "youtube", "database/migration" ] [dependencies] -entity = { path = "entity" } +database = { path = "database" } youtube = { path = "youtube" } anyhow = "1.0" @@ -21,9 +22,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.11", features = ["fast-rng", "v4"] } chrono = "0.4" tracing-panic = "0.1" -lazy_static = "1.5.0" -indexmap = "2.7.0" -dashmap = "6.1.0" +lazy_static = "1.5" +indexmap = "2.7" +dashmap = "6.1" # https://github.com/teloxide/teloxide/issues/1154 # [profile.dev] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index eb86a6f..23001f4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,31 @@ -Смотрите README v0.2.0 релиза, то что вы здесь видете не для публикации. +# Doggy Watch + +Telegram бот для предложения YouTube видео на стрим. +Сделан специально для [Doggy Dox](https://www.twitch.tv/doggy_dox). + +## Contributing +![Спроси меня о чём угодно!](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg) +в +[![Telegram](https://badgen.net/static/icon/Telegram?icon=telegram&color=cyan&label)](https://t.me/shiroyashik) +или +![Discord](https://badgen.net/badge/icon/Discord?icon=discord&label) + +Если у вас есть идем, нашли баг или хотите предложить улучшения +создавайте [issue](https://github.com/shiroyashik/doggy-watch/issues) +или свяжитесь со мной напрямую через Discord/Telegram (**@shiroyashik**). + +Если вы Rust разработчик, буду рад вашим Pull Request'ам: + +1. Форкните репу +2. Создайте новую ветку +3. Создайте PR! + +Буду рад любой вашей помощи! ❤ --- -**АХТУНГ!** В исходниках матюки! **:3** \ No newline at end of file + +**АХТУНГ!** В исходниках матюки! **:3** + +## License + +Doggy Watch is licensed under [GPL-3.0](LICENSE) \ No newline at end of file diff --git a/entity/Cargo.toml b/database/Cargo.toml similarity index 57% rename from entity/Cargo.toml rename to database/Cargo.toml index 2d56b21..ce89f76 100644 --- a/entity/Cargo.toml +++ b/database/Cargo.toml @@ -1,7 +1,8 @@ [package] -name = "entity" +name = "database" version = "0.1.0" edition = "2021" +publish = false [dependencies] -sea-orm = { version = "1.1", features = ["macros", "sqlx-sqlite", "runtime-tokio-rustls", "sqlx-postgres", "with-uuid", "with-chrono"] } \ No newline at end of file +sea-orm = { version = "1.1", features = ["macros", "sqlx-sqlite", "runtime-tokio-rustls", "sqlx-postgres", "with-chrono"] } \ No newline at end of file diff --git a/migration/Cargo.toml b/database/migration/Cargo.toml similarity index 100% rename from migration/Cargo.toml rename to database/migration/Cargo.toml diff --git a/migration/README.md b/database/migration/README.md similarity index 100% rename from migration/README.md rename to database/migration/README.md diff --git a/migration/src/lib.rs b/database/migration/src/lib.rs similarity index 60% rename from migration/src/lib.rs rename to database/migration/src/lib.rs index 2c605af..6005648 100644 --- a/migration/src/lib.rs +++ b/database/migration/src/lib.rs @@ -1,12 +1,14 @@ pub use sea_orm_migration::prelude::*; -mod m20220101_000001_create_table; +mod m20241211_182453_create_tables; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220101_000001_create_table::Migration)] + vec![ + Box::new(m20241211_182453_create_tables::Migration), + ] } } diff --git a/database/migration/src/m20241211_182453_create_tables.rs b/database/migration/src/m20241211_182453_create_tables.rs new file mode 100644 index 0000000..02d8c10 --- /dev/null +++ b/database/migration/src/m20241211_182453_create_tables.rs @@ -0,0 +1,190 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Videos + manager + .create_table( + Table::create() + .table(Videos::Table) + .if_not_exists() + .col(string_len_uniq(Videos::Ytid, 11).primary_key()) + .col(string(Videos::Title)) + .col(boolean(Videos::Banned).default(Expr::value(false))) + .to_owned(), + ) + .await?; + // Requests + manager + .create_table( + Table::create() + .table(Requests::Table) + .if_not_exists() + .col(pk_auto(Requests::Id)) + .col(string_len(Requests::Ytid, 11)) + .col(timestamp_null(Requests::ViewedAt).default(Expr::value(Keyword::Null))) + .foreign_key( + ForeignKey::create() + .name("fk_videos_ytid_requests") + .from(Requests::Table, Requests::Ytid) + .to(Videos::Table, Videos::Ytid) + .on_delete(ForeignKeyAction::Cascade) + ) + .to_owned(), + ) + .await?; + // Actions + manager + .create_table( + Table::create() + .table(Actions::Table) + .if_not_exists() + .col(pk_auto(Actions::Id)) + .col(integer(Actions::Rid)) + .col(big_integer(Actions::Uid)) + .col(timestamp(Actions::CreatedAt).default(Expr::current_timestamp())) + .foreign_key( + ForeignKey::create() + .name("fk_requests_rid_actions") + .from(Actions::Table, Actions::Rid) + .to(Requests::Table, Requests::Id) + .on_delete(ForeignKeyAction::Cascade) + ) + .to_owned(), + ) + .await?; + // Archived + manager + .create_table( + Table::create() + .table(Archived::Table) + .if_not_exists() + .col(pk_auto(Archived::Id)) + .col(string_len(Archived::Ytid, 11)) + .col(timestamp_null(Archived::ViewedAt)) + .col(big_integer(Archived::CreatedBy)) + .col(timestamp(Archived::CreatedAt).default(Expr::current_timestamp())) + .col(unsigned(Archived::Contributors)) + .foreign_key( + ForeignKey::create() + .name("fk_videos_ytid_archived") + .from(Archived::Table, Archived::Ytid) + .to(Videos::Table, Videos::Ytid) + .on_delete(ForeignKeyAction::NoAction) + ) + .to_owned(), + ) + .await?; + // Moderators + manager + .create_table( + Table::create() + .table(Moderators::Table) + .if_not_exists() + .col(big_integer_uniq(Moderators::Id).primary_key()) + .col(timestamp(Moderators::CreatedAt).default(Expr::current_timestamp())) + .col(boolean(Moderators::Notify).default(Expr::value(true))) + .col(boolean(Moderators::CanAddMods).default(Expr::value(false))) + .to_owned(), + ) + .await?; + // Users + manager + .create_table( + Table::create() + .table(Users::Table) + .if_not_exists() + .col(big_integer_uniq(Users::Id).primary_key()) + .col(timestamp(Users::CreatedAt).default(Expr::current_timestamp())) + .col(unsigned(Users::Contributions).default(Expr::value(0))) + .to_owned(), + ) + .await?; + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Videos + manager + .drop_table(Table::drop().table(Videos::Table).to_owned()) + .await?; + // Requests + manager + .drop_table(Table::drop().table(Requests::Table).to_owned()) + .await?; + // Actions + manager + .drop_table(Table::drop().table(Actions::Table).to_owned()) + .await?; + // Archived + manager + .drop_table(Table::drop().table(Archived::Table).to_owned()) + .await?; + // Moderators + manager + .drop_table(Table::drop().table(Moderators::Table).to_owned()) + .await?; + // Users + manager + .drop_table(Table::drop().table(Users::Table).to_owned()) + .await?; + Ok(()) + } +} + +#[derive(DeriveIden)] +enum Videos { + Table, + Ytid, + Title, + Banned +} + +#[derive(DeriveIden)] +enum Requests { + Table, + Id, + Ytid, + ViewedAt +} + +#[derive(DeriveIden)] +enum Actions { + Table, + Id, + Rid, + Uid, + CreatedAt +} + +#[derive(DeriveIden)] +enum Archived { + Table, + Id, + Ytid, + ViewedAt, + CreatedBy, + CreatedAt, + Contributors +} + +#[derive(DeriveIden)] +enum Moderators { + Table, + Id, + CreatedAt, + Notify, + CanAddMods +} + +#[derive(DeriveIden)] +enum Users { + Table, + Id, + CreatedAt, + Contributions +} \ No newline at end of file diff --git a/migration/src/main.rs b/database/migration/src/main.rs similarity index 100% rename from migration/src/main.rs rename to database/migration/src/main.rs diff --git a/database/src/actions.rs b/database/src/actions.rs new file mode 100644 index 0000000..16a4a2b --- /dev/null +++ b/database/src/actions.rs @@ -0,0 +1,33 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.2 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "actions")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub rid: i32, + pub uid: i64, + pub created_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::requests::Entity", + from = "Column::Rid", + to = "super::requests::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Requests, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Requests.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/actions.rs b/database/src/archived.rs similarity index 70% rename from entity/src/actions.rs rename to database/src/archived.rs index ecc09d8..fdd00b4 100644 --- a/entity/src/actions.rs +++ b/database/src/archived.rs @@ -3,23 +3,25 @@ use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] -#[sea_orm(table_name = "actions")] +#[sea_orm(table_name = "archived")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, - pub uid: String, - pub vid: i32, + pub ytid: String, + pub viewed_at: Option, + pub created_by: i64, pub created_at: DateTime, + pub contributors: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( belongs_to = "super::videos::Entity", - from = "Column::Vid", - to = "super::videos::Column::Id", + from = "Column::Ytid", + to = "super::videos::Column::Ytid", on_update = "NoAction", - on_delete = "Cascade" + on_delete = "NoAction" )] Videos, } diff --git a/entity/src/lib.rs b/database/src/lib.rs similarity index 71% rename from entity/src/lib.rs rename to database/src/lib.rs index 41ac7bd..d111a0b 100644 --- a/entity/src/lib.rs +++ b/database/src/lib.rs @@ -3,6 +3,8 @@ pub mod prelude; pub mod actions; +pub mod archived; pub mod moderators; -pub mod sea_orm_active_enums; +pub mod requests; +pub mod users; pub mod videos; diff --git a/entity/src/moderators.rs b/database/src/moderators.rs similarity index 86% rename from entity/src/moderators.rs rename to database/src/moderators.rs index bc4a71d..18cedf1 100644 --- a/entity/src/moderators.rs +++ b/database/src/moderators.rs @@ -6,8 +6,10 @@ use sea_orm::entity::prelude::*; #[sea_orm(table_name = "moderators")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] - pub uid: String, + pub id: i64, pub created_at: DateTime, + pub notify: bool, + pub can_add_mods: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/entity/src/prelude.rs b/database/src/prelude.rs similarity index 59% rename from entity/src/prelude.rs rename to database/src/prelude.rs index dce7f11..7f02ba4 100644 --- a/entity/src/prelude.rs +++ b/database/src/prelude.rs @@ -1,5 +1,8 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.2 pub use super::actions::Entity as Actions; +pub use super::archived::Entity as Archived; pub use super::moderators::Entity as Moderators; +pub use super::requests::Entity as Requests; +pub use super::users::Entity as Users; pub use super::videos::Entity as Videos; diff --git a/entity/src/videos.rs b/database/src/requests.rs similarity index 54% rename from entity/src/videos.rs rename to database/src/requests.rs index 40f8573..7f0ab92 100644 --- a/entity/src/videos.rs +++ b/database/src/requests.rs @@ -1,25 +1,28 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.2 -use super::sea_orm_active_enums::Status; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] -#[sea_orm(table_name = "videos")] +#[sea_orm(table_name = "requests")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, - pub title: String, - #[sea_orm(unique)] - pub yt_id: String, - pub created_at: DateTime, - pub status: Status, - pub updated_at: Option, + pub ytid: String, + pub viewed_at: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm(has_many = "super::actions::Entity")] Actions, + #[sea_orm( + belongs_to = "super::videos::Entity", + from = "Column::Ytid", + to = "super::videos::Column::Ytid", + on_update = "NoAction", + on_delete = "Cascade" + )] + Videos, } impl Related for Entity { @@ -28,4 +31,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Videos.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/database/src/users.rs b/database/src/users.rs new file mode 100644 index 0000000..79297e2 --- /dev/null +++ b/database/src/users.rs @@ -0,0 +1,17 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.2 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "users")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: i64, + pub created_at: DateTime, + pub contributions: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/database/src/videos.rs b/database/src/videos.rs new file mode 100644 index 0000000..cd82327 --- /dev/null +++ b/database/src/videos.rs @@ -0,0 +1,34 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.2 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "videos")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub ytid: String, + pub title: String, + pub banned: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::archived::Entity")] + Archived, + #[sea_orm(has_many = "super::requests::Entity")] + Requests, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Archived.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Requests.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/sea_orm_active_enums.rs b/entity/src/sea_orm_active_enums.rs deleted file mode 100644 index 5fd2fd8..0000000 --- a/entity/src/sea_orm_active_enums.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.2 - -use sea_orm::entity::prelude::*; - -#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] -#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "status")] -pub enum Status { - #[sea_orm(string_value = "Banned")] - Banned, - #[sea_orm(string_value = "Pending")] - Pending, - #[sea_orm(string_value = "Viewed")] - Viewed, -} diff --git a/migration/src/m20220101_000001_create_table.rs b/migration/src/m20220101_000001_create_table.rs deleted file mode 100644 index ebc266f..0000000 --- a/migration/src/m20220101_000001_create_table.rs +++ /dev/null @@ -1,123 +0,0 @@ -use sea_orm::{EnumIter, Iterable}; -use sea_orm_migration::{prelude::*, schema::*}; -use sea_query::extension::postgres::Type; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_type( - Type::create() - .as_enum(Alias::new("status")) - .values(Status::iter()) - .to_owned() - ) - .await?; - manager - .create_table( - Table::create() - .table(Videos::Table) - .if_not_exists() - .col(pk_auto(Videos::Id)) - .col(string(Videos::Title)) - .col(string_uniq(Videos::YtId)) - .col(timestamp(Videos::CreatedAt)) - .col(enumeration(Videos::Status, Alias::new("status"), Status::iter())) - .col(timestamp_null(Videos::UpdatedAt)) - .to_owned(), - ) - .await?; - manager - .create_table( - Table::create() - .table(Actions::Table) - .if_not_exists() - .col(pk_auto(Actions::Id)) - .col(string_len(Actions::Uid, 36)) - .col(integer(Actions::Vid)) - .col(timestamp(Actions::CreatedAt)) - .foreign_key( - ForeignKey::create() - .name("fk_videos_id") - .from(Actions::Table, Actions::Vid) - .to(Videos::Table, Videos::Id) - .on_delete(ForeignKeyAction::Cascade) - ) - .to_owned(), - ) - .await?; - manager - .create_table( - Table::create() - .table(Moderators::Table) - .if_not_exists() - .col(string_len_uniq(Moderators::Uid, 36).primary_key()) - .col(timestamp(Moderators::CreatedAt)) - .to_owned(), - ) - .await?; - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Actions::Table).to_owned()) - .await?; - manager - .drop_table(Table::drop().table(Videos::Table).to_owned()) - .await?; - manager - .drop_table(Table::drop().table(Moderators::Table).to_owned()) - .await?; - manager - .drop_type( - Type::drop().if_exists().name(Alias::new("status")).cascade().to_owned()) - .await?; - // manager - // .drop_foreign_key( - // ForeignKey::drop().name("fk_videos_id").to_owned()) - // .await?; - Ok(()) - } -} - -#[derive(DeriveIden)] -enum Videos { - Table, - Id, - Title, - YtId, - CreatedAt, - Status, - UpdatedAt - -} - -#[derive(DeriveIden)] -enum Actions { - Table, - Id, - Vid, - Uid, - CreatedAt -} - -#[derive(Iden, EnumIter)] -pub enum Status { - #[iden = "Pending"] - Pending, - #[iden = "Viewed"] - Viewed, - #[iden = "Banned"] - Banned -} - -#[derive(DeriveIden)] -enum Moderators { - Table, - Uid, - CreatedAt, -} \ No newline at end of file diff --git a/src/handle/about.rs b/src/handle/about.rs new file mode 100644 index 0000000..e226828 --- /dev/null +++ b/src/handle/about.rs @@ -0,0 +1,20 @@ +use chrono::Local; +use teloxide::{prelude::*, Bot}; + +use crate::{Rights, CHANNEL, COOLDOWN_DURATION, VERSION}; + +pub async fn command(bot: Bot, msg: Message, rights: Rights) -> anyhow::Result<()> { + bot.send_message(msg.chat.id, format!( + "Doggy-Watch v{VERSION}\n\ + ____________________\n\ + Debug information:\n\ + Rights level: {rights:?}\n\ + Linked channel: {}\n\ + Cooldown duration: {:?}\n\ + Server time:\n\ + {}", + *CHANNEL, COOLDOWN_DURATION, + Local::now().format("%Y-%m-%d %H:%M:%S") + )).await?; + Ok(()) +} \ No newline at end of file diff --git a/src/handle/add.rs b/src/handle/add.rs new file mode 100644 index 0000000..062727b --- /dev/null +++ b/src/handle/add.rs @@ -0,0 +1,172 @@ +use std::sync::Arc; + +use database::*; +use sea_orm::{prelude::*, EntityTrait, IntoActiveModel, Set}; +use teloxide::{prelude::*, types::ParseMode}; +use tokio::time::Instant; + +use crate::{check_subscription, markup, notify, AppState, DialogueState, MyDialogue, CHANNEL_INVITE_HASH, COOLDOWN_DURATION}; + +pub async fn message(bot: Bot, msg: Message, dialogue: MyDialogue) -> anyhow::Result<()> { + use youtube::*; + if let Some(text) = msg.clone().text() { + if let Some(user) = check_subscription(&bot, &msg.from.ok_or(anyhow::anyhow!("Message not from user!"))?.id).await { + // Get ready! + if let Some(ytid) = extract_youtube_video_id(text) { + let meta = get_video_metadata(&ytid).await?; + // Post + bot.send_message(msg.chat.id, format!( + "Вы уверены что хотите добавить {}", + meta.title + )).parse_mode(ParseMode::Html).reply_markup(markup::inline_yes_or_no()).await?; + dialogue.update(DialogueState::AcceptVideo { ytid, uid: user.id.0, title: meta.title }).await?; + } else { + bot.send_message(msg.chat.id, "Это не похоже на YouTube видео... Долбоёб").await?; + } + } else { + let link = if let Some(hash) = CHANNEL_INVITE_HASH.as_ref() { + &format!("Telegram канал", hash) + } else { + "Telegram канал" + }; + bot.send_message(msg.chat.id, format!("Вы не подписаны на {}!", link)).parse_mode(ParseMode::Html).await?; + } + } else { + bot.send_message(msg.chat.id, "Не-а!").await?; + } + Ok(()) +} + +pub async fn inline( + bot: Bot, + q: CallbackQuery, + msg: Message, + state: Arc, + (ytid, uid, title): (String, u64, String), + dialogue: MyDialogue +) -> anyhow::Result<()> { + let data = q.data.ok_or(anyhow::anyhow!("Inline: Нет данных!"))?; + let text = if &data == "yes" { + if let Some(last) = state.cooldown.get(&uid) { + if last.elapsed() < COOLDOWN_DURATION { + bot.edit_message_text(msg.chat.id, msg.id, "Слишком часто!").await?; + dialogue.exit().await?; + return Ok(()); + } + } + match add_video(&ytid, &title, &state).await { + Ok(col) => { + // Теперь видео создано. Можно приступать к созданию "запроса" и действия + match add_action(&col, uid, &state).await { + Ok(_) => { + // Обновляем кул-давн. + state.cooldown.insert(uid, Instant::now()); + // Обновляем данные о пользователе + if let Err(err) = add_user(uid, &state).await { + tracing::error!("Caused an exception in add_user due: {err:?}"); + } + // Отправляем уведомления + let bot_clone = bot.clone(); + tokio::spawn(async move { + let _ = notify(&bot_clone, format!("Добавленно новое видео: {title}!"), &state, vec![UserId(uid)]).await.inspect_err(|err| { + tracing::error!("Caused an exception in notify due: {err:?}"); + }); + }); + "Добавлено!" + }, + Err(err) => { + tracing::error!("Caused an exception in add_action due: {err:?}"); + &format!("{err:?}") + }, + } + }, + Err(err) => { + tracing::error!("Caused an exception in add_video due: {err:?}"); + &format!("{err:?}") + }, + } + } else { + "Отменено." + }; + bot.edit_message_text(msg.chat.id, msg.id, text).await?; + dialogue.exit().await?; + Ok(()) +} + + + +async fn add_video(ytid: &str, title: &str, state: &AppState) -> anyhow::Result { + // Проверяем есть ли необходимость в создании столбца video + if let Some(video) = videos::Entity::find_by_id(ytid).one(&state.db).await? { + // Необходимо проверить заблокировано ли видео и создавался ли запрос для этого видео + if video.banned { + anyhow::bail!("Ошибка: В чёрном списке!\nВероятнее всего был неоднократно просмотрен.") + } + Ok(video) + } else { + let new = videos::ActiveModel { + ytid: Set(ytid.to_string()), + title: Set(title.to_string()), + ..Default::default() + }; + Ok(new.insert(&state.db).await?) + } +} + +async fn add_action(col: &videos::Model, uid: u64, state: &AppState) -> anyhow::Result<()> { + // Проверяем существует ли запрос + let req = if let Some(req_col) = col.find_related(requests::Entity).one(&state.db).await? { + // Запрос существует + // Проверяем был ли уже просмотрен + if req_col.viewed_at.is_some() { + anyhow::bail!("Ошибка: Просмотрено!\nВидео было отмечано как просмотренное {}", req_col.viewed_at.unwrap().format("%Y-%m-%d %H:%M:%S")) + } + // Проверяем внёс ли этот пользователь свой "вклад" в этот запрос + if 0 != req_col.find_related(actions::Entity).filter(actions::Column::Uid.eq(uid)).count(&state.db).await? { + // Пользователь сделал свой "вклад", больше одного нельзя + anyhow::bail!("Ошибка: Такой запрос уже существует!\nВы уже запрашивали данное видео ранее.") + } + req_col + } else { + // Запрос не существует, создаём... + let new_req = requests::ActiveModel { + ytid: Set(col.ytid.clone()), + ..Default::default() + }; + new_req.insert(&state.db).await? + }; + + let new_act = actions::ActiveModel { + rid: Set(req.id), + uid: Set(uid as i64), + ..Default::default() + }; + + // Обрабатываем ошибку на случай неудачи, чтобы удалить запрос без действия + match new_act.insert(&state.db).await { + Ok(_) => Ok(()), + Err(err) => { + // Если для запроса не существует "действий", удаляем его. + if 0 == req.find_related(actions::Entity).count(&state.db).await? { + req.delete(&state.db).await?; + } + Err(err.into()) + }, + } +} + +async fn add_user(uid: u64, state: &AppState) -> anyhow::Result { + if let Some(user) = users::Entity::find_by_id(uid as i64).one(&state.db).await? { + let contributions = user.contributions; + let mut user = user.into_active_model(); + user.contributions = Set(contributions + 1); + Ok(user.update(&state.db).await?) + } else { + let user = users::ActiveModel { + id: Set(uid as i64), + contributions: Set(1), + ..Default::default() + }; + Ok(user.insert(&state.db).await?) + } +} \ No newline at end of file diff --git a/src/handle/archive.rs b/src/handle/archive.rs new file mode 100644 index 0000000..dd1d600 --- /dev/null +++ b/src/handle/archive.rs @@ -0,0 +1,132 @@ +use std::{sync::Arc, vec}; + +use sea_orm::{prelude::Expr, EntityTrait, QueryFilter, Set}; +use teloxide::{prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup, ParseMode}}; + +use crate::{AppState, InlineCommand}; +use database::{actions, requests, archived}; + +pub async fn command(bot: Bot, msg: Message) -> anyhow::Result<()> { + let keyboard: Vec> = vec![ + vec![InlineKeyboardButton::callback("Архивировать просмотренные", "archive_viewed")], + vec![InlineKeyboardButton::callback("Архивировать всё", "archive_all")] + ]; + let out = "Выберите действие с архивом:"; + bot.send_message(msg.chat.id, out).reply_markup(InlineKeyboardMarkup::new(keyboard)).await?; + Ok(()) +} + +pub async fn inline( + bot: Bot, + q: CallbackQuery, + chatid: ChatId, + state: Arc, + command: InlineCommand, +) -> anyhow::Result<()> { + // Есть только два действия, архивровать просмотренные и архивировать всё + // database::archived::ActiveModel { + // id: todo!(), / Auto + // ytid: todo!(), / From request + // viewed_at: todo!(), / From request + // created_by: todo!(), / From actions + // created_at: todo!(), / Auto + // contributors: todo!(), / From actions + // } + bot.answer_callback_query(&q.id).await?; + let text = match command { + InlineCommand::ArchiveViewed => { + match collect_viewed(&state).await { + Ok(total) => { + &format!("\"{}\" просмотренных запросов успешно архивировано!", total) + }, + Err(err) => { + tracing::error!("Caused an exception in archive viewed due: {err:?}"); + &format!("{err:?}") + }, + } + }, + InlineCommand::ArchiveAll => { + match collect_all(&state).await { + Ok(total) => { + &format!("\"{}\" запросов успешно архивировано!", total) + }, + Err(err) => { + tracing::error!("Caused an exception in archive all due: {err:?}"); + &format!("{err:?}") + }, + } + } + _ => { + tracing::error!("Unrecognized status! {command:?}"); + "Ошибка распознавания!" + } + }; + + bot.send_message(chatid, text).parse_mode(ParseMode::Html).await?; + Ok(()) +} + +// +// Auxiliary functions +// + +async fn archive(entities: Vec<(requests::Model, Vec)>, state: &AppState) -> anyhow::Result { + if entities.is_empty() { + anyhow::bail!("Нет объектов для архивации!"); + } + + let mut active_entities = Vec::new(); + for (request, actions) in entities.iter() { + let creator = actions.iter() + .min_by_key(|actions| actions.id) + .ok_or(anyhow::anyhow!("Actions vector cannot be empty!"))?; + let contributors = actions.len().try_into()?; + let ytid = request.ytid.clone(); + let viewed_at = request.viewed_at; + let created_by = creator.uid; + // let created_at = creator.created_at.clone(); Время архивации, а не создания запроса + active_entities.push(archived::ActiveModel { + ytid: Set(ytid), + viewed_at: Set(viewed_at), + created_by: Set(created_by), + // created_at: Set(created_at), + contributors: Set(contributors), + ..Default::default() + }); + } + + let total = active_entities.len().try_into()?; + + archived::Entity::insert_many(active_entities) + .exec(&state.db) + .await?; + + Ok(total) +} + +async fn collect_viewed(state: &AppState) -> anyhow::Result { + let entities: Vec<(requests::Model, Vec)> = requests::Entity::find() + .find_with_related(actions::Entity) + .filter(Expr::col(requests::Column::ViewedAt).is_not_null()) + .all(&state.db) + .await?; + let total = archive(entities, state).await?; + requests::Entity::delete_many() + .filter(Expr::col(requests::Column::ViewedAt).is_not_null()) + .exec(&state.db) + .await?; + Ok(total) + +} + +async fn collect_all(state: &AppState) -> anyhow::Result { + let entities: Vec<(requests::Model, Vec)> = requests::Entity::find() + .find_with_related(actions::Entity) + .all(&state.db) + .await?; + let total = archive(entities, state).await?; + requests::Entity::delete_many() + .exec(&state.db) + .await?; + Ok(total) +} \ No newline at end of file diff --git a/src/handle/ban.rs b/src/handle/ban.rs new file mode 100644 index 0000000..da3b924 --- /dev/null +++ b/src/handle/ban.rs @@ -0,0 +1,22 @@ +use teloxide::prelude::*; + +// FIXME: После редизайна код был перемещён без изменений! + +async fn command(bot: Bot, msg: Message) -> anyhow::Result<()> { + use database::{videos::{Entity, ActiveModel}, sea_orm_active_enums::Status}; + let video = Entity::find_by_id(vid).one(&state.db).await?; + if let Some(model) = video { + let title = model.title.clone(); + let mut video: ActiveModel = model.into(); + video.status = Set(Status::Banned); + video.updated_at = Set(Some(Local::now().naive_local())); + if video.update(&state.db).await.is_ok() { + bot.send_message(msg.chat.id, format!("Видео \"{title}\" успешно добавленно в чёрный список!")).parse_mode(ParseMode::Html).await?; + } else { + bot.send_message(msg.chat.id, "Произошла ошибка обновления записи в базе данных!").await?; + } + } else { + bot.send_message(msg.chat.id, "Не найдено.").await?; + } + Ok(()) +} \ No newline at end of file diff --git a/src/handle/info.rs b/src/handle/info.rs new file mode 100644 index 0000000..1a59d52 --- /dev/null +++ b/src/handle/info.rs @@ -0,0 +1,177 @@ +use std::sync::Arc; + +use chrono::Local; +use teloxide::{prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup, ParseMode}, utils::html::user_mention}; +use sea_orm::{prelude::*, IntoActiveModel, Order, QueryOrder as _, Set}; + +use crate::{AppState, InlineCommand}; +use database::*; +use youtube::DEFAULT_YT; + +// Вытаскивает VID из сообщений: /123 или 123 +pub fn recognise_vid(text: &str) -> Option { + if let Ok(vid) = text.parse::() { + Some(vid) + } else if let Some(unslash) = text.strip_prefix("/") { + if let Ok(vid) = unslash.parse::() { + Some(vid) + } else { + None + } + } else { + None + } +} + +pub async fn message(bot: Bot, msg: Message, state: Arc, rid: i32) -> anyhow::Result<()> { + match collect_info(&rid, &state).await { + Ok((video, request, creator, contributors)) => { + let name = bot.get_chat_member(ChatId(creator.uid), UserId(creator.uid as u64)).await?.user.full_name(); + let creator_mention = user_mention(UserId(creator.uid as u64), &name); + + let out: String = format!( + "{}\n\ + Добавлено {creator_mention} (👀{contributors})" + , video.ytid, video.title); + + // TODO: УБЕДИТСЯ ЧТО НЕ ТРЕБУЕТСЯ https://docs.rs/teloxide/latest/teloxide/types/struct.LinkPreviewOptions.html + let ban_title = if video.banned { + ("Пардоньте", "pardon") + } else { + ("В бан", "ban") + }; + let viewed_title = if request.viewed_at.is_some() { + ("Убрать из просмотренных", "unview") + } else { + ("В просмотренные", "view") + }; + let keyboard: Vec> = vec![ + vec![ + InlineKeyboardButton::callback(viewed_title.0, format!("{} {}", viewed_title.1, request.id)), + InlineKeyboardButton::callback(ban_title.0, format!("{} {}", ban_title.1, request.id)) + ] + ]; + bot.send_message(msg.chat.id, out).parse_mode(ParseMode::Html).reply_markup(InlineKeyboardMarkup::new(keyboard)).await?; + }, + Err(err) => { + tracing::error!("Caused an exception in collect_info due: {err:?}"); + bot.send_message(msg.chat.id, format!("{err:?}")).await?; + }, + } + Ok(()) +} + +async fn collect_info(rid: &i32, state: &AppState) -> anyhow::Result<(videos::Model, requests::Model, actions::Model, u64)> { + let request = requests::Entity::find_by_id(*rid).one(&state.db).await? + .ok_or(anyhow::anyhow!("Can't find request ID {rid}"))?; + + let video = request.find_related(videos::Entity) + .one(&state.db).await? + .ok_or(anyhow::anyhow!("Can't find video entry for {request:?}"))?; + let creator = request.find_related(actions::Entity) + .order_by(actions::Column::Id, Order::Asc) + .one(&state.db).await? + .ok_or(anyhow::anyhow!("Can't find creator entry for {request:?}"))?; + + let contributors = request.find_related(actions::Entity).count(&state.db).await?; + + Ok((video, request, creator, contributors)) +} + +// Изменение статуса видео. +pub async fn inline(bot: Bot, q: CallbackQuery, chatid: ChatId, state: Arc, command: InlineCommand) -> anyhow::Result<()> { + bot.answer_callback_query(&q.id).await?; + + let text = { + match command { + InlineCommand::Ban(rid) => { + match ban(&rid, &state).await { + Ok(vid) => { + &format!("Статус видео \"{}\" успешно обновлён!", vid.title) + }, + Err(err) => { + tracing::error!("Caused an exception in ban due: {err:?}"); + &format!("{err:?}") + }, + } + }, + InlineCommand::Pardon(rid) => { + match pardon(&rid, &state).await { + Ok(vid) => { + &format!("Статус видео \"{}\" успешно обновлён!", vid.title) + }, + Err(err) => { + tracing::error!("Caused an exception in pardon due: {err:?}"); + &format!("{err:?}") + }, + } + }, + InlineCommand::View(rid) => { + match view(&rid, &state).await { + Ok(vid) => { + &format!("Статус видео \"{}\" успешно обновлён!", vid.title) + }, + Err(err) => { + tracing::error!("Caused an exception in view due: {err:?}"); + &format!("{err:?}") + }, + } + }, + InlineCommand::Unview(rid) => { + match unview(&rid, &state).await { + Ok(vid) => { + &format!("Статус видео \"{}\" успешно обновлён!", vid.title) + }, + Err(err) => { + tracing::error!("Caused an exception in unview due: {err:?}"); + &format!("{err:?}") + }, + } + }, + _ => { + tracing::error!("Unrecognized status! {command:?}"); + "Ошибка распознавания!" + } + } + }; + bot.send_message(chatid, text).parse_mode(ParseMode::Html).await?; + Ok(()) +} + +// Auxiliary functions + +async fn ban(rid: &i32, state: &AppState) -> anyhow::Result { + let request = requests::Entity::find_by_id(*rid).one(&state.db).await? + .ok_or(anyhow::anyhow!("Can't find request ID {rid}"))?; + let mut video = request.find_related(videos::Entity).one(&state.db).await? + .ok_or(anyhow::anyhow!("Can't find video for {request:?}"))?.into_active_model(); + video.banned = Set(true); + Ok(video.update(&state.db).await?) +} + +async fn view(rid: &i32, state: &AppState) -> anyhow::Result { + let mut request = requests::Entity::find_by_id(*rid).one(&state.db).await? + .ok_or(anyhow::anyhow!("Can't find request ID {rid}"))?.into_active_model(); +request.viewed_at = Set(Some(Local::now().naive_local())); +request.update(&state.db).await?.find_related(videos::Entity).one(&state.db).await? +.ok_or(anyhow::anyhow!("Can't find video by RID {rid}")) +} + +// Alternate + +async fn pardon(rid: &i32, state: &AppState) -> anyhow::Result { + let request = requests::Entity::find_by_id(*rid).one(&state.db).await? + .ok_or(anyhow::anyhow!("Can't find request ID {rid}"))?; + let mut video = request.find_related(videos::Entity).one(&state.db).await? + .ok_or(anyhow::anyhow!("Can't find video for {request:?}"))?.into_active_model(); + video.banned = Set(false); + Ok(video.update(&state.db).await?) +} + +async fn unview(rid: &i32, state: &AppState) -> anyhow::Result { + let mut request = requests::Entity::find_by_id(*rid).one(&state.db).await? + .ok_or(anyhow::anyhow!("Can't find request ID {rid}"))?.into_active_model(); + request.viewed_at = Set(None); + request.update(&state.db).await?.find_related(videos::Entity).one(&state.db).await? + .ok_or(anyhow::anyhow!("Can't find video by RID {rid}")) +} \ No newline at end of file diff --git a/src/handle/list.rs b/src/handle/list.rs new file mode 100644 index 0000000..3e14f59 --- /dev/null +++ b/src/handle/list.rs @@ -0,0 +1,84 @@ +use std::sync::Arc; + +use indexmap::IndexMap; +use teloxide::{prelude::*, types::{LinkPreviewOptions, ParseMode}}; +use sea_orm::{prelude::*, Order, QueryOrder}; + +use database::*; + +use crate::AppState; + +pub async fn command(bot: Bot, msg: Message, state: Arc) -> anyhow::Result<()> { + struct Video { + id: i32, + title: String, + url: String, + contributors: u64, + status: String, + } + let videos: Vec<(requests::Model, Option)> = requests::Entity::find() + .find_also_related(videos::Entity).filter(videos::Column::Banned.eq(false)).all(&state.db).await?; + // let videos_len = videos.len(); + if !videos.is_empty() { + let mut by_date: IndexMap> = IndexMap::new(); + for (request, video) in videos { + let video = video.unwrap(); + let creator = if let Some(c) = request.find_related(actions::Entity).order_by(actions::Column::Id, Order::Asc).one(&state.db).await? { + c + } else { + let data = format!("Can't find creator for {request:?}"); + bot.send_message(msg.chat.id, data.clone()).await?; + anyhow::bail!(data); + }; + + let contributors = request.find_related(actions::Entity).count(&state.db).await?; + let date = creator.created_at.date(); + let url = format!("{}{}", youtube::DEFAULT_YT, video.ytid); + + let viewed_times = archived::Entity::find().filter(archived::Column::Ytid.eq(video.ytid.clone())).filter(archived::Column::ViewedAt.is_not_null()).count(&state.db).await?; + let archived_times = archived::Entity::find().filter(archived::Column::Ytid.eq(video.ytid)).count(&state.db).await?; + + let mut status = String::new(); + status.push(if request.viewed_at.is_some() { + '👀' + } else if viewed_times != 0 { + '⭐' + } else if archived_times != 0 { + '📁' + } else { + '🆕' + }); + + if let Some(entry) = by_date.get_mut(&date) { + entry.push(Video { id: request.id, title: video.title, url, contributors, status }); + } else { + by_date.insert(date, vec![Video { id: request.id, title: video.title, url, contributors, status }]); + }; + } + by_date.sort_unstable_by(|a, _, c, _| c.cmp(a)); + let mut result = String::new(); + for (date, mut videos) in by_date { + if result.is_empty() { + result.push_str(&format!("[{}]", date.format("%d.%m"))); + } else { + result.push_str(&format!("\n[{}]", date.format("%d.%m"))); + } + // result.push_str(&format!(" {}", videos.len())); + videos.sort_unstable_by(|a, b| a.contributors.cmp(&b.contributors)); + for video in videos { + let contributors = if video.contributors != 1 { + format!("(🙍‍♂️{}) ", video.contributors) + } else { + String::new() + }; + result.push_str(&format!("\n{}/{} 📺YT {}{}", video.status, video.id, video.url, contributors, video.title)); + // result.push_str(&format!("\n{}. {} YT ({})", me.username.clone().unwrap(), video.id, video.id, video.title, video.url, video.contributors)); + } + } + // result.push_str(&format!("\nВсего: {}", videos_len)); + bot.send_message(msg.chat.id, result).parse_mode(ParseMode::Html).link_preview_options(LinkPreviewOptions { is_disabled: true, url: None, prefer_small_media: false, prefer_large_media: false, show_above_text: false }).await?; + } else { + bot.send_message(msg.chat.id, "Нет видео для просмотра :(").await?; + } + Ok(()) +} \ No newline at end of file diff --git a/src/handle/mod.rs b/src/handle/mod.rs new file mode 100644 index 0000000..ae43d23 --- /dev/null +++ b/src/handle/mod.rs @@ -0,0 +1,101 @@ +use std::sync::Arc; + +use dptree::{filter, filter_map}; +use teloxide::{dispatching::{dialogue::{self, GetChatId, InMemStorage}, HandlerExt, UpdateHandler}, prelude::*, types::User}; + +use crate::{cancel, AppState, Command, DialogueState, InlineCommand, Rights}; + +mod moderator; +mod about; +mod add; +mod list; +// mod ban; +mod info; +mod start; +mod archive; +mod notify; + +pub fn schema() -> UpdateHandler { + use dptree::case; + let moderator_commands = dptree::entry() + .branch(case![Command::Start].endpoint(start::command_mod)) + .branch(case![Command::List].endpoint(list::command)) + .branch(case![Command::Archive].endpoint(archive::command)) + .branch(case![Command::Mods].endpoint(moderator::list::command)) + .branch(case![Command::AddMod].endpoint(moderator::add::command)) + .branch(case![Command::RemMod(uid)].endpoint(moderator::remove::command)) + .branch(case![Command::Notify].endpoint(notify::command)) + .branch(case![Command::About].endpoint(about::command)); + + let user_commands = dptree::entry() + .branch(case![Command::Start].endpoint(start::command_user)) + .branch(case![Command::About].endpoint(about::command)); + + let command_handler = dptree::entry() + .filter_command::() + .branch(case![DialogueState::Nothing] + .branch(case![Rights::None].branch(user_commands)) + .branch(case![Rights::Moderator { can_add_mods }].branch(moderator_commands.clone())) + ); + + let message_handler = Update::filter_message() + .filter_map(|msg: Message| { + msg.from // Get User + }) + .map(|user: User| { + user.id // Get UserId + }) + .filter_map_async(|state: Arc, uid: UserId| async move { + state.check_rights(&uid).await.ok() + }) + // State handlers + .branch(case![DialogueState::NewModeratorInput].endpoint(moderator::add::recieved_message)) + .branch(command_handler) + .branch( + dptree::filter_map(|msg: Message| { + if let Some(text) = msg.text() { + info::recognise_vid(text) + // проверяем что из сообщения можно достать vid (example: /123 where 123 is vid) + } else { + None + } + }).endpoint(info::message) + ) + .branch( + dptree::filter(|msg: Message| { + msg.text().is_some() && msg.from.is_some() + }) + .endpoint(add::message) + ); + + let parsable_callback = dptree::entry() + .chain(filter_map(|q: CallbackQuery| { + InlineCommand::parse(&q.data?) + })) + .branch(case![InlineCommand::Cancel].endpoint(cancel)) + .branch(filter(|com: InlineCommand| { + matches!(com, InlineCommand::ArchiveAll | InlineCommand::ArchiveViewed) + }).endpoint(archive::inline)) + .branch(filter(|com: InlineCommand| { + matches!(com, InlineCommand::Ban(_) | InlineCommand::Pardon(_) | InlineCommand::View(_) | InlineCommand::Unview(_)) + }).endpoint(info::inline)); + + let callback_query_handler = Update::filter_callback_query() + .filter_map(|q: CallbackQuery| { + q.regular_message().cloned() + }) + .filter_map(|q: CallbackQuery| { + q.chat_id() + }) + .filter_map(|cid: ChatId| { + cid.as_user() + }) + .branch(parsable_callback) + // FIXME: .branch(case![DialogueState::Nothing].endpoint(info::inline)) + .branch(case![DialogueState::RemoveModeratorConfirm { uid }].endpoint(moderator::remove::inline)) + .branch(case![DialogueState::AcceptVideo { ytid, uid, title }].endpoint(add::inline)); + + dialogue::enter::, DialogueState, _>() + .branch(message_handler) + .branch(callback_query_handler) +} \ No newline at end of file diff --git a/src/handle/moderator/add.rs b/src/handle/moderator/add.rs new file mode 100644 index 0000000..ccce543 --- /dev/null +++ b/src/handle/moderator/add.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use sea_orm::{ActiveModelTrait, EntityTrait, Set}; +use teloxide::prelude::*; + +use crate::{check_subscription, markup, AppState, DialogueState, MyDialogue}; + +pub async fn command(bot: Bot, msg: Message, dialogue: MyDialogue) -> anyhow::Result<()> { + bot.send_message(msg.chat.id, "Перешлите любое сообщение от человека которого вы хотите добавить как модератора:").reply_markup(markup::inline_cancel()).await?; + dialogue.update(DialogueState::NewModeratorInput).await?; + Ok(()) +} + +/// Второй этап добавления модератора. +/// Захватывается пересланное сообщение от другого пользователя и из него достаётся UserId. +pub async fn recieved_message(bot: Bot, msg: Message, id: UserId, state: Arc, dialogue: MyDialogue) -> anyhow::Result<()> { + use database::moderators; + // add can_add_mods check + let moderator = moderators::Entity::find_by_id(id.0 as i64).one(&state.db).await?.ok_or(anyhow::anyhow!("Ошибка! Не модератор."))?; + if !moderator.can_add_mods { + bot.send_message(msg.chat.id, "Недостаточно прав!").await?; + dialogue.exit().await?; + return Ok(()); + } + // if let Some(current) = + if let Some(user) = msg.forward_from_user() { + let member = check_subscription(&bot, &user.id).await; + if let Some(user) = member { + let model = moderators::ActiveModel { + id: Set(user.id.0 as i64), + ..Default::default() + }; + if model.insert(&state.db).await.is_ok() { + bot.send_message(msg.chat.id, "Модератор добавлен!").await?; + } else { + bot.send_message(msg.chat.id, "Произошла ошибка!\nМожет данный модератор уже добавлен?").await?; + } + dialogue.exit().await?; + } else { bot.send_message(msg.chat.id, "Ошибка! Не подписан на канал!").await?; } + } else { bot.send_message(msg.chat.id, "Ошибка! Перешлите сообщение!").await?; } + dialogue.exit().await?; + Ok(()) +} \ No newline at end of file diff --git a/src/handle/moderator/list.rs b/src/handle/moderator/list.rs new file mode 100644 index 0000000..29f6b6b --- /dev/null +++ b/src/handle/moderator/list.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; + +use sea_orm::EntityTrait as _; +use teloxide::{prelude::*, types::ParseMode, utils::html::user_mention}; + +use crate::AppState; + +pub async fn command(bot: Bot, msg: Message, state: Arc) -> anyhow::Result<()> { + use database::moderators::{Entity, Model}; + let columns: Vec = Entity::find().all(&state.db).await.unwrap(); + if !columns.is_empty() { + let mut str = String::from("Модераторы:"); + for col in columns { + tracing::info!("{col:?}"); + let uid: u64 = col.id as u64; + let name = bot.get_chat_member(ChatId(col.id), UserId(uid)).await?.user.full_name(); + let mention = user_mention(UserId(uid), &name); + str.push_str(&format!("\n - {mention}\nНа посту с {}, UID: {uid}", col.created_at.format("%Y-%m-%d %H:%M:%S"))); + }; + tracing::info!("Sending message! {str}"); + bot.send_message(msg.chat.id, str).parse_mode(ParseMode::Html).await? + } else { + bot.send_message(msg.chat.id, "Модераторов нет").await? + }; + Ok(()) +} \ No newline at end of file diff --git a/src/handle/moderator/mod.rs b/src/handle/moderator/mod.rs new file mode 100644 index 0000000..ea65ac0 --- /dev/null +++ b/src/handle/moderator/mod.rs @@ -0,0 +1,3 @@ +pub mod add; +pub mod remove; +pub mod list; \ No newline at end of file diff --git a/src/handle/moderator/remove.rs b/src/handle/moderator/remove.rs new file mode 100644 index 0000000..49dcfb3 --- /dev/null +++ b/src/handle/moderator/remove.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use sea_orm::EntityTrait; +use teloxide::prelude::*; + +use crate::{markup, AppState, DialogueState, MyDialogue}; + +pub async fn command(bot: Bot, msg: Message, id: UserId, dialogue: MyDialogue, state: Arc, uid: String) -> anyhow::Result<()> { + // add can_add_mods check + let moderator = database::moderators::Entity::find_by_id(id.0 as i64).one(&state.db).await?.ok_or(anyhow::anyhow!("Ошибка! Не модератор."))?; + if !moderator.can_add_mods { + bot.send_message(msg.chat.id, "Недостаточно прав!").await?; + return Ok(()); + } + if uid.is_empty() { + bot.send_message(msg.chat.id, "После команды необходимо указать UID модератора. (/remmod 1234567)").await?; + } else { + bot.send_message(msg.chat.id, "Вы уверены что хотите удалить модератора?").reply_markup(markup::inline_yes_or_no()).await?; + dialogue.update(DialogueState::RemoveModeratorConfirm { uid }).await?; + } + Ok(()) +} + +/// Второй этап удаления модератора. +pub async fn inline(bot: Bot, q: CallbackQuery, state: Arc, uid: String, dialogue: MyDialogue) -> anyhow::Result<()> { + use database::moderators::Entity; + bot.answer_callback_query(&q.id).await?; + if let Some(msg) = q.regular_message() { + if let Some(data) = q.clone().data { + let text= if &data == "yes" { + if let Ok(uid) = uid.parse::() { + if Entity::delete_by_id(uid as i32).exec(&state.db).await?.rows_affected != 0 { + "Модератор удалён!" + } else { + "Произошла ошибка!\nПо всей видимости такого модератора не существует." + } + } else { + "Ошибка! Это точно число?" + } + } else { + "Раскулачивание модера отменено." + }; + bot.edit_message_text(msg.chat.id, msg.id, text).await?; + // else if let Some(id) = q.inline_message_id { + // bot.edit_message_text_inline(id, text).await?; + // } + } + } + dialogue.exit().await?; + Ok(()) +} \ No newline at end of file diff --git a/src/handle/notify.rs b/src/handle/notify.rs new file mode 100644 index 0000000..00cedc0 --- /dev/null +++ b/src/handle/notify.rs @@ -0,0 +1,39 @@ +use std::sync::Arc; + +use sea_orm::{EntityTrait, IntoActiveModel, Set, prelude::*}; +use teloxide::{prelude::*, Bot}; + +use database::*; +use crate::AppState; + +/// Invert notify status for moderator +pub async fn command(bot: Bot, msg: Message, uid: UserId, state: Arc) -> anyhow::Result<()> { + let text = if let Some(moder) = moderators::Entity::find_by_id(uid.0 as i32).one(&state.db).await? { + let moder = match moder.notify { + true => { + let mut moder = moder.into_active_model(); + moder.notify = Set(false); + moder + }, + false => { + let mut moder = moder.into_active_model(); + moder.notify = Set(true); + moder + }, + }; + let moder = moder.update(&state.db).await?; + + if moder.notify { + "Теперь уведомления включены!".to_string() + } else { + "Теперь уведомления отключены!".to_string() + } + } else { + let text = format!("No moderator found for {uid}!"); + tracing::error!(text); + text + }; + + bot.send_message(msg.chat.id, text).parse_mode(teloxide::types::ParseMode::Html).await?; + Ok(()) +} \ No newline at end of file diff --git a/src/handle/start.rs b/src/handle/start.rs new file mode 100644 index 0000000..b0d8526 --- /dev/null +++ b/src/handle/start.rs @@ -0,0 +1,24 @@ +use teloxide::{prelude::*, types::{InputFile, User}, utils::command::BotCommands as _}; + +use crate::Command; + +pub async fn command_user(bot: Bot, msg: Message, user: User) -> anyhow::Result<()> { + bot.send_sticker( + msg.chat.id, + InputFile::file_id("CAACAgIAAxkBAAECxFlnVeGjr8kRcDNWU30uDII5R1DwNAACKl4AAkxE8UmPev9DDR6RgTYE") + ).emoji("🥳") + .await?; + bot.send_message(msg.chat.id, format!( + "Приветствую {}!\n\ + Отправьте в этот чат ссылку на YouTube видео, чтобы предложить его для просмотра!", + user.full_name() + )).await?; + Ok(()) +} + +pub async fn command_mod(bot: Bot, msg: Message) -> anyhow::Result<()> { + let mut result = String::from(&Command::descriptions().to_string()); + result.push_str("\n\nЧтобы получить информацию о видео или изменить его статус просто отправь его номер в чат."); + bot.send_message(msg.chat.id, result).await?; + Ok(()) +} \ No newline at end of file diff --git a/src/inline.rs b/src/inline.rs new file mode 100644 index 0000000..6642e34 --- /dev/null +++ b/src/inline.rs @@ -0,0 +1,45 @@ +#[derive(Debug, PartialEq, Clone)] +pub enum InlineCommand { + Ban(i32), + Pardon(i32), + View(i32), + Unview(i32), + ArchiveViewed, + ArchiveAll, + Cancel, +} + +impl InlineCommand { + pub fn parse(input: &str) -> Option { + let mut parts = input.split_whitespace(); + Some(match parts.next()? { + "ban" => Self::Ban(parts.next()?.parse().ok()?), + "pardon" => Self::Pardon(parts.next()?.parse().ok()?), + "view" => Self::View(parts.next()?.parse().ok()?), + "unview" => Self::Unview(parts.next()?.parse().ok()?), + "archive_viewed" => Self::ArchiveViewed, + "archive_all" => Self::ArchiveAll, + "cancel" => Self::Cancel, + _ => return None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_cancel() { + let text = "cancel"; + let result = InlineCommand::parse(text); + assert_eq!(result, Some(InlineCommand::Cancel)); + } + + #[test] + fn test_parse_ban() { + let text = "ban 123"; + let result = InlineCommand::parse(text); + assert_eq!(result, Some(InlineCommand::Ban(123))); + } +} diff --git a/src/main.rs b/src/main.rs index a32e880..fa38886 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,24 @@ use std::{env::var, sync::Arc, time::Duration}; -use chrono::Local; use dashmap::DashMap; -use indexmap::IndexMap; -use sea_orm::{prelude::*, ActiveValue::*, Database, Order, QueryOrder}; +use database::moderators; +use sea_orm::{prelude::*, sea_query::OnConflict, ConnectOptions, Database, Set}; use teloxide::{ - dispatching::dialogue::{GetChatId, InMemStorage}, prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup, InputFile, LinkPreviewOptions, ParseMode, User}, utils::{command::BotCommands, html::user_mention} + dispatching::dialogue::InMemStorage, + macros::BotCommands, prelude::*, types::User }; use tokio::time::Instant; use tracing_panic::panic_hook; use lazy_static::lazy_static; -const COOLDOWN_DURATION: Duration = Duration::from_secs(30); -const VERSION: &'static str = env!("CARGO_PKG_VERSION"); +mod handle; +mod markup; + +mod inline; +pub use inline::InlineCommand; + +pub const COOLDOWN_DURATION: Duration = Duration::from_secs(10); +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); lazy_static! { pub static ref LOGGER_ENV: String = { @@ -31,27 +37,9 @@ lazy_static! { pub static ref CHANNEL: i64 = { var("CHANNEL").expect("TOKEN env not set.").parse().expect("Cant't parse env CHANNEL to i64.") }; -} - -struct AppState { - db: DatabaseConnection, - administrators: Vec, - cooldown: DashMap - -} - -impl AppState { - async fn check_rights(&self, uid: &UserId) -> anyhow::Result { - use entity::moderators::Entity as Moderators; - - Ok(if self.administrators.contains(&uid.0) { - Rights::Administrator - } else if Moderators::find_by_id(uid.to_string()).one(&self.db).await?.is_some() { - Rights::Moderator - } else { - Rights::None - }) - } + pub static ref CHANNEL_INVITE_HASH: Option = { + var("CHANNEL_INVITE_HASH").ok() + }; } @@ -74,55 +62,30 @@ async fn main() -> anyhow::Result<()> { tracing::info!("{:?}", *ADMINISTRATORS); let bot = Bot::new(&*TOKEN); - let db: DatabaseConnection = Database::connect(&*DATABASE_URL).await?; + let mut opt = ConnectOptions::new(&*DATABASE_URL); + opt.sqlx_logging_level(tracing::log::LevelFilter::Trace); + let db: DatabaseConnection = Database::connect(opt).await?; + + // add administrators to db + { + let admins: Vec = ADMINISTRATORS.iter().map(|&x| { + moderators::ActiveModel { + id: Set(x as i64), + can_add_mods: Set(true), + ..Default::default() + } + }).collect(); + moderators::Entity::insert_many(admins) + .on_conflict(OnConflict::column(moderators::Column::Id) + .update_column(moderators::Column::CanAddMods).to_owned() + ).exec(&db).await?; + } + // teloxide::repl(bot, answer).await; - let state = Arc::new(AppState {db, administrators: (&ADMINISTRATORS).to_vec(), cooldown: DashMap::new()}); - - // let handler = dptree::entry() - // .branch(Update::filter_message().endpoint(answer)) - // .branch(Update::filter_callback_query().endpoint(callback_handler)); - let handler = dptree::entry() - .branch(Update::filter_message() - .enter_dialogue::, DialogueState>() - .branch(dptree::case![DialogueState::Nothing] - .branch( - dptree::filter_async(is_moderator) - .branch(dptree::filter(|msg: Message| { - if let Some(text) = msg.text() { - recognise_vid(text).is_some() - // проверяем что из сообщения можно достать vid (example: /123 where 123 is vid) - } else { - false - } - }).endpoint(info)) - .filter_command::() - .endpoint(answer) - ) - .branch( - dptree::entry() - .filter_command::() - .endpoint(insufficient_rights) - ) - .branch( - dptree::filter(|msg: Message| { - msg.text().is_some() && msg.from.is_some() - }) - .endpoint(normal_answer) - )) - .branch(dptree::case![DialogueState::NewModeratorInput].endpoint(add_moderator_from_recived_message)) - // .branch(dptree::case![DialogueState::RemoveModeratorConfirm { uid }].endpoint(remove_moderator)) - ) - .branch(Update::filter_callback_query() - .enter_dialogue::, DialogueState>() - .branch(dptree::case![DialogueState::Nothing].endpoint(change_status)) - .branch(dptree::case![DialogueState::RemoveModeratorConfirm { uid }].endpoint(remove_moderator)) - .branch(dptree::case![DialogueState::AcceptVideo { accept }].endpoint(accept_video)) - .branch(dptree::case![DialogueState::NewModeratorInput].endpoint(cancel)) - // .endpoint(callback_handler) - ); + let state = Arc::new(AppState {db, cooldown: DashMap::new()}); - Dispatcher::builder(bot, handler) + Dispatcher::builder(bot, handle::schema()) // Pass the shared state to the handler as a dependency. .dependencies(dptree::deps![state, InMemStorage::::new()]) .default_handler(|upd| async move { @@ -135,328 +98,58 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -async fn is_moderator(state: Arc, msg: Message) -> bool { - if let Some(user) = msg.from { - let rights = state.check_rights(&user.id).await; - if let Ok(rights) = rights { - rights.into() - } else { - false - } - } else { - false - } -} - -type MyDialogue = Dialogue>; +pub type MyDialogue = Dialogue>; #[derive(Clone, Default)] pub enum DialogueState { #[default] Nothing, // User - AcceptVideo{ accept: ForAccept }, + AcceptVideo{ ytid: String, uid: u64, title: String }, // Moderator NewModeratorInput, - RemoveModeratorConfirm{uid: String}, -} - -#[derive(Clone)] -pub struct ForAccept { - pub ytid: String, - pub uid: u64, - pub title: String, + RemoveModeratorConfirm{ uid: String }, } #[derive(BotCommands, Clone)] #[command(rename_rule = "lowercase", description = "Список поддерживаемых команд:")] enum Command { - #[command(description = "отобразить этот текст.")] - Help, - #[command(description = "запустить бота.")] + #[command(description = "запустить бота и/или вывести этот текст.")] Start, #[command(description = "вывести список.")] List, - // #[command(description = "информация о видео.")] - // I(i32), - // #[command(description = "вывести чёрный список.")] - // Blacklisted, - #[command(description = "добавить в чёрный список.")] - Ban(i32), - // #[command(description = "удалить из чёрного списка.")] - // RemBlacklisted(u128), + #[command(description = "действия с архивом.")] + Archive, #[command(description = "вывести список модераторов.")] Mods, #[command(description = "добавить модератора.")] AddMod, #[command(description = "удалить модератора.")] RemMod(String), + #[command(description = "включить/выключить уведомления.")] + Notify, About } -async fn answer(bot: Bot, msg: Message, cmd: Command, state: Arc, dialogue: MyDialogue) -> anyhow::Result<()> { - let user = msg.from.unwrap(); // Потому что уже уверены что пользователь администратор или модератор - let rights = state.check_rights(&user.id).await.unwrap(); - tracing::info!("{rights:?}"); - match cmd { - Command::Help => { - let mut result = String::from(&Command::descriptions().to_string()); - result.push_str("\n\nЧтобы получить информацию о видео или изменить его статус просто отправь его номер в чат."); - bot.send_message(msg.chat.id, result).await?; - }, - Command::Start => { - let mut result = String::from(&Command::descriptions().to_string()); - result.push_str("\n\nЧтобы получить информацию о видео или изменить его статус просто отправь его номер в чат."); - bot.send_message(msg.chat.id, result).await?; - }, - Command::List => { - use entity::{actions, videos, sea_orm_active_enums::Status}; - struct Video { - id: i32, - title: String, - url: String, - contributors: u64, - } - let videos = videos::Entity::find().filter(videos::Column::Status.eq(Status::Pending)).all(&state.db).await?; - if videos.len() != 0 { - let mut by_date: IndexMap> = IndexMap::new(); - for video in videos { - let contributors = actions::Entity::find().filter(actions::Column::Vid.eq(video.id)).count(&state.db).await?; - let date = video.created_at.date(); - let url = format!("{}{}", youtube::DEFAULT_YT, video.yt_id); - let title = video.title.replace("/", "/ "); - - if let Some(entry) = by_date.get_mut(&date) { - entry.push(Video { id: video.id, title, url, contributors }); - } else { - by_date.insert(date, vec![Video { id: video.id, title, url, contributors }]); - }; - } - by_date.sort_unstable_by(|a, _, c, _| c.cmp(a)); - let mut result = String::new(); - for (date, mut videos) in by_date { - if result.is_empty() { - result.push_str(&format!("[{}]", date.format("%m.%d"))); - } else { - result.push_str(&format!("\n[{}]", date.format("%m.%d"))); - } - videos.sort_unstable_by(|a, b| a.contributors.cmp(&b.contributors)); - for video in videos { - result.push_str(&format!("\n/{} 📺YT (👀{}) {}\n", video.id, video.url, video.contributors, video.title)); - // result.push_str(&format!("\n{}. {} YT ({})", me.username.clone().unwrap(), video.id, video.id, video.title, video.url, video.contributors)); - } - } - bot.send_message(msg.chat.id, result).parse_mode(ParseMode::Html).link_preview_options(LinkPreviewOptions { is_disabled: true, url: None, prefer_small_media: false, prefer_large_media: false, show_above_text: false }).await?; - } else { - bot.send_message(msg.chat.id, "Нет видео для просмотра :(").await?; - } - // for (date, value) in by_date.sort_unstable_by(|a, b| b.cmp(a)) { - // result.push_str(&format!("{}: {}\n", date, value)); - // } +// ------------------------ +// NOTIFICATIONS +// ------------------------ - // let keyboard: Vec> = vec![ - // vec![InlineKeyboardButton::callback("Hello1", "1")], - // vec![InlineKeyboardButton::callback("Hello2", "2"), InlineKeyboardButton::callback("Hello1", "1")], - // vec![InlineKeyboardButton::callback("Hello3", "3")], - // ]; - - // bot.send_message(msg.chat.id, format!("{messages_total:?}")).reply_markup(InlineKeyboardMarkup::new(keyboard)).await?; - }, - // Command::Blacklisted => todo!(), - Command::Ban(vid) => { - use entity::{videos::{Entity, ActiveModel}, sea_orm_active_enums::Status}; - let video = Entity::find_by_id(vid).one(&state.db).await?; - if let Some(model) = video { - let title = model.title.clone(); - let mut video: ActiveModel = model.into(); - video.status = Set(Status::Banned); - video.updated_at = Set(Some(Local::now().naive_local())); - if video.update(&state.db).await.is_ok() { - bot.send_message(msg.chat.id, format!("Видео \"{title}\" успешно добавленно в чёрный список!")).parse_mode(ParseMode::Html).await?; - } else { - bot.send_message(msg.chat.id, "Произошла ошибка обновления записи в базе данных!").await?; - } - } else { - bot.send_message(msg.chat.id, "Не найдено.").await?; - } - }, - // Command::RemBlacklisted() => todo!(), - Command::Mods => { - use entity::moderators::{Entity, Model}; - let columns: Vec = Entity::find().all(&state.db).await.unwrap(); - if columns.len() != 0 { - let mut str = String::from("Модераторы:"); - for col in columns { - tracing::info!("{col:?}"); - let uid: u64 = col.uid.parse()?; - let name = bot.get_chat_member(ChatId(uid as i64), UserId(uid)).await?.user.full_name(); - let mention = user_mention(UserId(uid), &name); - str.push_str(&format!("\n - {mention}\nНа посту с {}, UID: {uid}", col.created_at.format("%Y-%m-%d %H:%M:%S"))); - }; - tracing::info!("Sending message! {str}"); - bot.send_message(msg.chat.id, str).parse_mode(ParseMode::Html).await? - } else { - bot.send_message(msg.chat.id, "Модераторов нет").await? - }; - }, - Command::AddMod => { - bot.send_message(msg.chat.id, "Перешлите любое сообщение от человека которого вы хотите добавить как модератора:").reply_markup(inline_cancel()).await?; - dialogue.update(DialogueState::NewModeratorInput).await?; - }, - Command::RemMod(uid) => { - if uid.is_empty() { - bot.send_message(msg.chat.id, "После команды необходимо указать UID модератора. (/remmod 1234567)").await?; - } else { - bot.send_message(msg.chat.id, "Вы уверены что хотите удалить модератора?").reply_markup(inline_yes_or_no()).await?; - dialogue.update(DialogueState::RemoveModeratorConfirm { uid }).await?; - } - }, - Command::About => { - bot.send_message(msg.chat.id, about_msg(&rights)).await?; - }, - }; - Ok(()) -} - -fn recognise_vid(text: &str) -> Option { - if let Ok(vid) = text.parse::() { - Some(vid) - } else { - if let Some(unslash) = text.strip_prefix("/") { - if let Ok(vid) = unslash.parse::() { - Some(vid) - } else { - None - } - } else { - None +async fn notify(bot: &Bot, title: String, state: &AppState, exclude: Vec) -> anyhow::Result<()> { + let notifiable = moderators::Entity::find().filter(moderators::Column::Notify.eq(true)).all(&state.db).await?; + if notifiable.is_empty() { + // No one to notify + return Ok(()); + } + let mesg = format!("📢 {title}"); + for moder in notifiable { + let uid = UserId(moder.id as u64); + if exclude.contains(&uid) { + continue; } + let chat_id: ChatId = uid.into(); + bot.send_message(chat_id, mesg.clone()).parse_mode(teloxide::types::ParseMode::Html).await?; } -} - -async fn info(bot: Bot, msg: Message, state: Arc) -> anyhow::Result<()> { - use entity::{videos, actions}; - use youtube::DEFAULT_YT; - let vid = recognise_vid(msg.text().unwrap()).unwrap(); // Проверено в dptree - let col = videos::Entity::find_by_id(vid).one(&state.db).await?; - if let Some(video) = col { - // Getting creator from actions - let creator = actions::Entity::find() - .filter(actions::Column::Vid.eq(video.id)) - .order_by(actions::Column::Id, Order::Asc) - .one(&state.db).await? - .ok_or(anyhow::anyhow!("Can't find creator entry for {video:?}"))?; - let contributors = actions::Entity::find().filter(actions::Column::Vid.eq(video.id)).count(&state.db).await?; - - let creator_uid = creator.uid.parse()?; - let name = bot.get_chat_member(ChatId(creator_uid as i64), UserId(creator_uid)).await?.user.full_name(); - let creator_mention = user_mention(UserId(creator_uid), &name); - - let out: String = format!( - "{}\n\ - Добавлено {creator_mention} (👀{contributors})" - , video.yt_id, video.title); - - // TODO: УБЕДИТСЯ ЧТО НЕ ТРЕБУЕТСЯ https://docs.rs/teloxide/latest/teloxide/types/struct.LinkPreviewOptions.html - let keyboard: Vec> = vec![ - vec![InlineKeyboardButton::callback("Просмотрено", format!("viewed {}", video.id)), InlineKeyboardButton::callback("В бан", format!("ban {}", video.id))] - ]; - bot.send_message(msg.chat.id, out).parse_mode(ParseMode::Html).reply_markup(InlineKeyboardMarkup::new(keyboard)).await?; - } else { - bot.send_message(msg.chat.id, "Не найдено.").await?; - } - Ok(()) -} - -async fn insufficient_rights(bot: Bot, msg: Message, cmd: Command) -> anyhow::Result<()> { - let rights = Rights::None; - if let Some(user) = check_subscription(&bot, &msg.from.ok_or(anyhow::anyhow!("Message not from user!"))?.id).await { - match cmd { - Command::Start => { - bot.send_sticker( - msg.chat.id, - InputFile::file_id("CAACAgIAAxkBAAECxFlnVeGjr8kRcDNWU30uDII5R1DwNAACKl4AAkxE8UmPev9DDR6RgTYE")) - .emoji("🥳") - .await?; - bot.send_message(msg.chat.id, format!( - "Приветствую {}!\n\ - Отправьте в этот чат ссылку на YouTube видео, чтобы предложить его для просмотра!", - user.full_name() - )).await?; - }, - Command::About => { - bot.send_message(msg.chat.id, about_msg(&rights)).await?; - }, - _ => { - bot.send_message(msg.chat.id, format!( - "?" - )).await?; - } - } - } else { - bot.send_message(msg.chat.id, format!( - "Вы не подписаны на Telegram канал!" - )).await?; - } - Ok(()) -} - -async fn normal_answer(bot: Bot, msg: Message, dialogue: MyDialogue) -> anyhow::Result<()> { - use youtube::*; - if let Some(text) = msg.clone().text() { - if let Some(user) = check_subscription(&bot, &msg.from.ok_or(anyhow::anyhow!("Message not from user!"))?.id).await { - // Get ready! - if let Some(ytid) = extract_youtube_video_id(text) { - let meta = get_video_metadata(&ytid).await?; - // Post - bot.send_message(msg.chat.id, format!( - "Вы уверены что хотите добавить {}", - meta.title - )).parse_mode(ParseMode::Html).reply_markup(inline_yes_or_no()).await?; - let accept = ForAccept { ytid, uid: user.id.0, title: meta.title }; - dialogue.update(DialogueState::AcceptVideo { accept }).await?; - } else { - bot.send_message(msg.chat.id, "Это не похоже на YouTube видео... Долбоёб").await?; - } - } else { - bot.send_message(msg.chat.id, "Вы не подписаны на Telegram канал!").await?; - } - } else { - bot.send_message(msg.chat.id, "Не-а!").await?; - } - Ok(()) -} - -fn about_msg(rights: &Rights) -> String { - format!( - "Doggy-Watch v{VERSION}\n\ - ____________________\n\ - Debug information:\n\ - Rights level: {rights:?}\n\ - Linked channel: {}\n\ - Cooldown duration: {:?}", - *CHANNEL, COOLDOWN_DURATION - ) -} - -async fn add_moderator_from_recived_message(bot: Bot, msg: Message, state: Arc, dialogue: MyDialogue) -> anyhow::Result<()> { - use entity::moderators::ActiveModel; - if let Some(user) = msg.forward_from_user() { - let member = check_subscription(&bot, &user.id).await; - if let Some(user) = member { - let now = Local::now().naive_local(); - let model = ActiveModel { - uid: Set(user.id.0.to_string()), - created_at: Set(now), - }; - if model.insert(&state.db).await.is_ok() { - bot.send_message(msg.chat.id, "Модератор добавлен!").await?; - } else { - bot.send_message(msg.chat.id, "Произошла ошибка!\nМожет данный модератор уже добавлен?").await?; - } - dialogue.exit().await?; - } else { bot.send_message(msg.chat.id, "Ошибка! Не подписан на канал!").await?; } - } else { bot.send_message(msg.chat.id, "Ошибка! Перешлите сообщение!").await?; } Ok(()) } @@ -464,179 +157,46 @@ async fn add_moderator_from_recived_message(bot: Bot, msg: Message, state: Arc InlineKeyboardMarkup { - let keyboard: Vec> = vec![ - vec![InlineKeyboardButton::callback("Да", "yes"), InlineKeyboardButton::callback("Нет", "no")] - ]; - InlineKeyboardMarkup::new(keyboard) -} -fn inline_cancel() -> InlineKeyboardMarkup { - let keyboard: Vec> = vec![ - vec![InlineKeyboardButton::callback("Отменить", "cancel")] - ]; - InlineKeyboardMarkup::new(keyboard) -} - -async fn change_status(bot: Bot, q: CallbackQuery, state: Arc) -> anyhow::Result<()> { - use entity::{videos::{ActiveModel, Entity}, sea_orm_active_enums::Status}; - bot.answer_callback_query(&q.id).await?; - if let Some(msg) = q.regular_message() { - if let Some(data) = q.clone().data { - // .. - let data: Vec<&str> = data.split(" ").collect(); - let text = if data.len() == 2 { - let status = match data[0] { - "ban" => { - Status::Banned - }, - "viewed" => { - Status::Viewed - } - _ => { - anyhow::bail!("Unrecognized status! {data:?}"); - } - }; - let vid: i32 = data[1].parse()?; - - let video = Entity::find_by_id(vid).one(&state.db).await?; - if let Some(model) = video { - let title = model.title.clone(); - let mut video: ActiveModel = model.into(); - video.status = Set(status); - video.updated_at = Set(Some(Local::now().naive_local())); - if video.update(&state.db).await.is_ok() { - &format!("Статус видео \"{title}\" успешно обновлён!") - } else { - "Произошла ошибка обновления записи в базе данных!" - } - } else { - "Не найдено." - } - } else { - "Ошибка распознавания" - }; - bot.send_message(msg.chat_id().unwrap(), text).parse_mode(ParseMode::Html).await?; - // else if let Some(id) = q.inline_message_id { - // bot.edit_message_text_inline(id, text).await?; - // } - } - } - Ok(()) -} - -async fn remove_moderator(bot: Bot, q: CallbackQuery, state: Arc, uid: String, dialogue: MyDialogue) -> anyhow::Result<()> { - use entity::moderators::Entity; - bot.answer_callback_query(&q.id).await?; - if let Some(msg) = q.regular_message() { - if let Some(data) = q.clone().data { - let text= if &data == "yes" { - if Entity::delete_by_id(uid).exec(&state.db).await?.rows_affected != 0 { - "Модератор удалён!" - } else { - "Произошла ошибка!\nПо всей видимости такого модератора не существует." - } - } else { - "Раскулачивание модера отменено." - }; - bot.edit_message_text(msg.chat_id().unwrap(), msg.id, text).await?; - // else if let Some(id) = q.inline_message_id { - // bot.edit_message_text_inline(id, text).await?; - // } - } - } - dialogue.exit().await?; - Ok(()) -} - -async fn accept_video(bot: Bot, q: CallbackQuery, state: Arc, accept: ForAccept, dialogue: MyDialogue) -> anyhow::Result<()> { - use entity::{videos, actions, sea_orm_active_enums::Status}; - bot.answer_callback_query(&q.id).await?; - if let Some(msg) = q.regular_message() { - if let Some(data) = q.clone().data { - let text= if &data == "yes" { - if let Some(last) = state.cooldown.get(&accept.uid) { - if last.elapsed() < COOLDOWN_DURATION { - bot.edit_message_text(msg.chat_id().unwrap(), msg.id, "Боже... Ты слишком груб с этим ботом. Остуди пыл.").await?; - dialogue.exit().await?; - return Ok(()); - } - } - let video = if let Some(video ) = videos::Entity::find().filter(videos::Column::YtId.eq(accept.ytid.clone())).one(&state.db).await? { - Ok(video) - } else { - let video= videos::ActiveModel { - title: Set(accept.title), - yt_id: Set(accept.ytid), - created_at: Set(Local::now().naive_local()), - status: Set(Status::Pending), - ..Default::default() - }; - video.insert(&state.db).await - }; - if let Ok(video) = video { - if let Ok(duplicates) = actions::Entity::find().filter(actions::Column::Uid.eq(accept.uid.to_string())).filter(actions::Column::Vid.eq(video.id)).count(&state.db).await { - if duplicates == 0 { - let action= actions::ActiveModel { - uid: Set(accept.uid.to_string()), - vid: Set(video.id), - created_at: Set(Local::now().naive_local()), - ..Default::default() - }; - if action.insert(&state.db).await.is_ok() { - state.cooldown.insert(accept.uid, Instant::now()); - "Добавлено!" - } else { - videos::Entity::delete_by_id(video.id).exec(&state.db).await?; - "База данных вернула ошибку на этапе создания события!" - } - } else { - "Отправлять одно и тоже видео нельзя!" - } - } else { - "База данных вернула ошибку на этапе проверки дублекатов!" - } - } else { - "База данных вернула ошибку на этапе создания видео!" - } - } else { - "Отменено." - }; - bot.edit_message_text(msg.chat_id().unwrap(), msg.id, text).await?; - // else if let Some(id) = q.inline_message_id { - // bot.edit_message_text_inline(id, text).await?; - // } - } - } - dialogue.exit().await?; - Ok(()) -} - -async fn cancel(bot: Bot, q: CallbackQuery, dialogue: MyDialogue) -> anyhow::Result<()> { +// FIXME: DEPREACTED: WILL BE REPLACED WITH InlineCommand +pub async fn cancel(bot: Bot, q: CallbackQuery, dialogue: MyDialogue) -> anyhow::Result<()> { + // FIXME: ADD CHECK FOR CANCEL DATA && bot.answer_callback_query(&q.id).await?; dialogue.exit().await?; Ok(()) } -// ------------------------ -// FACE CONTROL -// ------------------------ +// ------------------------- +// FACE CONTROL // APP STATE +// ------------------------- -#[derive(Debug)] +struct AppState { + db: DatabaseConnection, + cooldown: DashMap + +} + +impl AppState { + /// Возвращает Result для переданного пользователя + async fn check_rights(&self, uid: &UserId) -> anyhow::Result { + use database::moderators::Entity as Moderators; + + Ok(if let Some(moder) = Moderators::find_by_id(uid.0 as i32).one(&self.db).await? { + Rights::Moderator { can_add_mods: moder.can_add_mods } + } else { + Rights::None + }) + } +} + +#[derive(Debug, Clone)] enum Rights { None, - Moderator, - Administrator -} - -impl From for bool { - fn from(value: Rights) -> Self { - match value { - Rights::None => false, - _ => true - } - } + Moderator { + can_add_mods: bool + }, } +/// Проверка подписки async fn check_subscription(bot: &Bot, uid: &UserId) -> Option { let chat_member = bot .get_chat_member(ChatId(*CHANNEL), *uid).send().await; diff --git a/src/markup.rs b/src/markup.rs new file mode 100644 index 0000000..2a13de1 --- /dev/null +++ b/src/markup.rs @@ -0,0 +1,14 @@ +use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup}; + +pub fn inline_yes_or_no() -> InlineKeyboardMarkup { + let keyboard: Vec> = vec![ + vec![InlineKeyboardButton::callback("Да", "yes"), InlineKeyboardButton::callback("Нет", "no")] + ]; + InlineKeyboardMarkup::new(keyboard) +} +pub fn inline_cancel() -> InlineKeyboardMarkup { + let keyboard: Vec> = vec![ + vec![InlineKeyboardButton::callback("Отменить", "cancel")] + ]; + InlineKeyboardMarkup::new(keyboard) +} \ No newline at end of file diff --git a/youtube/src/lib.rs b/youtube/src/lib.rs index 6eb066a..7ba76d2 100644 --- a/youtube/src/lib.rs +++ b/youtube/src/lib.rs @@ -43,7 +43,10 @@ pub async fn get_video_metadata(vid: &str) -> Result