From 54171ba2ceaefb32acabdbaec1d6543bcc968d21 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 20 Aug 2022 15:44:45 +0200 Subject: [PATCH] feat: moderation commands --- .cargo/config.toml | 32 ++ .env.example | 4 +- Cargo.lock | 483 +++++++++++++++++++++++++++- Cargo.toml | 10 +- configuration.example.json | 6 +- configuration.revanced.json | 8 +- configuration.schema.json | 18 ++ src/commands/configuration.rs | 29 +- src/commands/moderation.rs | 291 ++++++++++++++++- src/db/database.rs | 106 ++++++ src/db/mod.rs | 2 + src/db/model.rs | 32 ++ src/events/guild_member_addition.rs | 5 +- src/events/guild_member_update.rs | 5 +- src/events/message_create.rs | 6 +- src/events/mod.rs | 20 +- src/events/ready.rs | 59 ++++ src/events/thread_create.rs | 11 +- src/logger.rs | 4 +- src/main.rs | 85 ++++- src/model/application.rs | 153 ++++----- src/utils.rs | 58 ---- src/utils/bot.rs | 20 ++ src/utils/decancer.rs | 28 ++ src/utils/embed.rs | 26 ++ src/utils/mod.rs | 6 + src/utils/moderation.rs | 102 ++++++ 27 files changed, 1417 insertions(+), 192 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 src/db/database.rs create mode 100644 src/db/mod.rs create mode 100644 src/db/model.rs create mode 100644 src/events/ready.rs delete mode 100644 src/utils.rs create mode 100644 src/utils/bot.rs create mode 100644 src/utils/decancer.rs create mode 100644 src/utils/embed.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/moderation.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..e9b44a4 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,32 @@ +[build] +rustc-wrapper = 'P:\Programme (64 Bit)\Rust\.cargo\bin\sccache.exe' +[target.x86_64-unknown-linux-gnu] +rustflags = [ + '-Clink-arg=-fuse-ld=lld', + '-Zshare-generics=y', +] +linker = '/usr/bin/clang' + +[target.x86_64-pc-windows-msvc] +rustflags = ['-Zshare-generics=y'] +linker = 'rust-lld.exe' + +[target.x86_64-apple-darwin] +rustflags = [ + '-C', + 'link-arg=-fuse-ld=/usr/local/bin/zld', + '-Zshare-generics=y', + '-Csplit-debuginfo=unpacked', +] +[profile.dev] +opt-level = 0 +debug = 2 +incremental = true +codegen-units = 512 + +[profile.release] +opt-level = 3 +debug = 0 +incremental = false +codegen-units = 256 +split-debuginfo = '...' diff --git a/.env.example b/.env.example index 13a342c..2235b01 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ # The Discord authorization token for the bot, requires the MESSAGE_CONTENT intent -DISCORD_AUTHORIZATION_TOKEN= \ No newline at end of file +DISCORD_AUTHORIZATION_TOKEN= +# The connection string to the MongoDB database +MONGODB_URI='' \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 47878b6..c47c6d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -80,6 +91,25 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bson" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d76085681585d39016f4d3841eb019201fc54d2dd0d92ad1e4fab3bfb32754" +dependencies = [ + "ahash", + "base64", + "hex", + "indexmap", + "lazy_static", + "rand", + "serde", + "serde_bytes", + "serde_json", + "time 0.3.12", + "uuid 1.1.2", +] + [[package]] name = "bumpalo" version = "3.10.0" @@ -153,14 +183,38 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + [[package]] name = "darling" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.1", + "darling_macro 0.14.1", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", ] [[package]] @@ -177,13 +231,24 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", + "quote", + "syn", +] + [[package]] name = "darling_macro" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" dependencies = [ - "darling_core", + "darling_core 0.14.1", "quote", "syn", ] @@ -201,6 +266,12 @@ dependencies = [ "serde", ] +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + [[package]] name = "decancer" version = "1.4.1" @@ -226,6 +297,7 @@ checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -263,6 +335,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-as-inner" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "flate2" version = "1.0.24" @@ -319,12 +403,34 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.21" @@ -346,6 +452,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -400,6 +507,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -409,6 +522,32 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "http" version = "0.2.8" @@ -507,6 +646,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipconfig" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723519edce41262b05d4143ceb95050e4c614f483e78e9fd9e39a8275a84ad98" +dependencies = [ + "socket2", + "widestring", + "winapi", + "winreg 0.7.0", +] + [[package]] name = "ipnet" version = "2.5.0" @@ -540,6 +691,12 @@ version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "lock_api" version = "0.4.7" @@ -559,12 +716,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matches" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "md-5" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658646b21e0b72f7866c7038ab086d3d5e1cd6271f060fd37defb241949d0582" +dependencies = [ + "digest", +] + [[package]] name = "memchr" version = "2.5.0" @@ -608,6 +789,52 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mongodb" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95afe97b0c799fdf69cd960272a2cb9662d077bd6efd84eb722bb9805d47554" +dependencies = [ + "async-trait", + "base64", + "bitflags", + "bson", + "chrono", + "derivative", + "futures-core", + "futures-executor", + "futures-util", + "hex", + "hmac", + "lazy_static", + "md-5", + "os_info", + "pbkdf2", + "percent-encoding", + "rand", + "rustc_version_runtime", + "rustls", + "rustls-pemfile 0.3.0", + "serde", + "serde_bytes", + "serde_with", + "sha-1", + "sha2", + "socket2", + "stringprep", + "strsim", + "take_mut", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-util", + "trust-dns-proto", + "trust-dns-resolver", + "typed-builder", + "uuid 0.8.2", + "webpki-roots", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -661,6 +888,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "os_info" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5209b2162b2c140df493a93689e04f8deab3a67634f5bc7a553c0a98e5b8d399" +dependencies = [ + "log", + "winapi", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -684,6 +921,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "pbkdf2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +dependencies = [ + "digest", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -727,7 +973,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff861b6a52ec47bc54eb17424c025feeb040e82836036276c25dda045a8a0c" dependencies = [ - "darling", + "darling 0.14.1", "proc-macro2", "quote", "syn", @@ -748,6 +994,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.21" @@ -849,7 +1101,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile", + "rustls-pemfile 1.0.1", "serde", "serde_json", "serde_urlencoded", @@ -862,17 +1114,29 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "webpki-roots", - "winreg", + "winreg 0.10.1", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", ] [[package]] name = "revanced-discord-bot" -version = "1.1.0" +version = "2.2.1" dependencies = [ + "bson", "chrono", "decancer", "dirs", "dotenv", + "mongodb", "poise", "regex", "serde", @@ -898,6 +1162,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31b7153270ebf48bf91c65ae5b0c00e749c4cfad505f66530ac74950249582f" +dependencies = [ + "rustc_version", + "semver", +] + [[package]] name = "rustls" version = "0.20.6" @@ -910,6 +1193,15 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustls-pemfile" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" +dependencies = [ + "base64", +] + [[package]] name = "rustls-pemfile" version = "1.0.1" @@ -947,6 +1239,21 @@ dependencies = [ "untrusted", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.142" @@ -966,6 +1273,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.142" @@ -983,6 +1299,7 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -1010,6 +1327,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling 0.13.4", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serenity" version = "0.11.5" @@ -1053,6 +1392,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1093,12 +1443,28 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.99" @@ -1110,6 +1476,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + [[package]] name = "thiserror" version = "1.0.32" @@ -1161,8 +1533,15 @@ dependencies = [ "libc", "num_threads", "serde", + "time-macros", ] +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + [[package]] name = "tinyvec" version = "1.6.0" @@ -1298,6 +1677,51 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trust-dns-proto" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "lazy_static", + "log", + "rand", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -1325,6 +1749,17 @@ dependencies = [ "webpki", ] +[[package]] +name = "typed-builder" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typemap_rev" version = "0.1.5" @@ -1392,6 +1827,25 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "uuid" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +dependencies = [ + "getrandom", + "serde", +] + [[package]] name = "valuable" version = "0.1.0" @@ -1521,6 +1975,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + [[package]] name = "winapi" version = "0.3.9" @@ -1586,6 +2046,15 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 0df735b..90dfdee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ keywords = ["ReVanced"] license = "GPL-3.0" name = "revanced-discord-bot" repository = "https://github.com/revanced/revanced-discord-bot" -version = "1.1.0" +version = "2.2.1" edition = "2021" [profile.release] @@ -17,8 +17,10 @@ codegen-units = 1 panic = "abort" [dependencies] -poise = "0.3.0" -decancer = "1.4.1" +bson = "2.4" +mongodb = "2.3" +poise = "0.3" +decancer = "1.4" tokio = { version = "1.20.1", features = ["rt-multi-thread"] } dotenv = "0.15" serde = { version = "1.0", features = ["derive"] } @@ -26,6 +28,6 @@ serde_json = "1.0" regex = "1.0" serde_regex = "1.1" chrono = "0.4" -dirs = "4.0.0" +dirs = "4.0" tracing = { version = "0.1", features = ["max_level_debug", "release_max_level_info"] } tracing-subscriber = "0.3" \ No newline at end of file diff --git a/configuration.example.json b/configuration.example.json index 581c4c4..cc2a239 100644 --- a/configuration.example.json +++ b/configuration.example.json @@ -1,7 +1,11 @@ { "$schema": "./configuration.schema.json", "general": { - "embed_color": 0 + "embed_color": 0, + "mute": { + "role": 0, + "take": [0] + } }, "administrators": { "roles": [0], diff --git a/configuration.revanced.json b/configuration.revanced.json index 229d771..c264e2c 100644 --- a/configuration.revanced.json +++ b/configuration.revanced.json @@ -1,6 +1,10 @@ -{ +{ "general": { - "embed_color": 14908858 + "embed_color": 14908858, + "mute": { + "role": 953984696491061289, + "take": [996121272897519687] + } }, "administrators": { "roles": [973886585294704640], diff --git a/configuration.schema.json b/configuration.schema.json index e577d2b..4b5023e 100644 --- a/configuration.schema.json +++ b/configuration.schema.json @@ -10,6 +10,24 @@ "properties": { "embed_color": { "$ref": "#/$defs/color" + }, + "mute": { + "type": "object", + "properties": { + "role": { + "type": "integer", + "description": "The id of the role." + }, + "take": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "A list of role ids which will be revoked from the user.", + "minItems": 1, + "uniqueItems": true + } + } } } }, diff --git a/src/commands/configuration.rs b/src/commands/configuration.rs index eab6c6e..5792087 100644 --- a/src/commands/configuration.rs +++ b/src/commands/configuration.rs @@ -1,18 +1,18 @@ use tracing::debug; -use crate::utils::load_configuration; +use crate::utils::bot::load_configuration; use crate::{Context, Error}; -#[poise::command(slash_command, prefix_command)] +#[poise::command(slash_command)] pub async fn reload(ctx: Context<'_>) -> Result<(), Error> { // Update the configuration let configuration = load_configuration(); // Use the embed color from the updated configuration let embed_color = configuration.general.embed_color; // Also save the new configuration to the user data - *ctx.data().write().await = configuration; + *ctx.data().write().await.configuration.write().await = configuration; - debug!("{:?} reloaded the configuration.", ctx.author().name); + debug!("{} reloaded the configuration.", ctx.author().name); ctx.send(|f| { f.ephemeral(true).embed(|f| { @@ -25,18 +25,31 @@ pub async fn reload(ctx: Context<'_>) -> Result<(), Error> { Ok(()) } -#[poise::command(slash_command, prefix_command)] +#[poise::command(slash_command)] pub async fn stop(ctx: Context<'_>) -> Result<(), Error> { - debug!("{:?} stopped the bot.", ctx.author().name); + debug!("{} stopped the bot.", ctx.author().name); - let color = ctx.data().read().await.general.embed_color; + let color = ctx + .data() + .read() + .await + .configuration + .read() + .await + .general + .embed_color; ctx.send(|f| { f.ephemeral(true) .embed(|f| f.description("Stopped the bot.").color(color)) }) .await?; - ctx.discord().shard.shutdown_clean(); + ctx.framework() + .shard_manager() + .lock() + .await + .shutdown_all() + .await; Ok(()) } diff --git a/src/commands/moderation.rs b/src/commands/moderation.rs index 85d7407..39d0daa 100644 --- a/src/commands/moderation.rs +++ b/src/commands/moderation.rs @@ -1 +1,290 @@ -// TODO mute/kick/ban/warn via database \ No newline at end of file +use bson::{doc, Document}; +use chrono::{Duration, Utc}; +use mongodb::options::{UpdateModifications, UpdateOptions}; +use poise::serenity_prelude::{self as serenity, Member, RoleId}; +use tracing::{debug, trace}; + +use crate::db::model::Muted; +use crate::utils::moderation::{queue_unmute_member, respond_mute_command, ModerationKind}; +use crate::{Context, Error}; + +#[poise::command(slash_command)] +pub async fn unmute( + ctx: Context<'_>, + #[description = "The member to unmute"] member: Member, +) -> Result<(), Error> { + ctx.defer().await.expect("Failed to defer"); + + let data = &ctx.data().read().await; + let configuration = data.configuration.read().await; + + if let Some(pending_unmute) = data.pending_unmutes.read().await.get(&member.user.id.0) { + trace!("Cancelling pending unmute for {}", member.user.id.0); + pending_unmute.abort(); + } + + respond_mute_command( + &ctx, + ModerationKind::Unmute( + queue_unmute_member( + ctx.discord(), + &data.database, + &member, + configuration.general.mute.role, + 0, + ) + .await + .unwrap(), + ), + &member.user, + configuration.general.embed_color, + ) + .await +} + +#[poise::command(slash_command)] +pub async fn mute( + ctx: Context<'_>, + #[description = "The member to mute"] mut member: Member, + #[description = "Seconds"] seconds: Option, + #[description = "Minutes"] minutes: Option, + #[description = "Hours"] hours: Option, + #[description = "Days"] days: Option, + #[description = "Months"] months: Option, + #[description = "Months"] reason: String, +) -> Result<(), Error> { + let now = Utc::now(); + let mut mute_duration = Duration::zero(); + + if let Some(seconds) = seconds { + mute_duration = mute_duration + .checked_add(&Duration::seconds(seconds)) + .unwrap(); + } + if let Some(minutes) = minutes { + mute_duration = mute_duration + .checked_add(&Duration::minutes(minutes)) + .unwrap(); + } + if let Some(hours) = hours { + mute_duration = mute_duration.checked_add(&Duration::hours(hours)).unwrap(); + } + if let Some(days) = days { + mute_duration = mute_duration.checked_add(&Duration::days(days)).unwrap(); + } + if let Some(months) = months { + const DAYS_IN_MONTH: i64 = 30; + mute_duration = mute_duration + .checked_add(&Duration::days(months * DAYS_IN_MONTH)) + .unwrap(); + } + + let unmute_time = now + mute_duration; + + let data = ctx.data().read().await; + let configuration = data.configuration.read().await; + let embed_color = configuration.general.embed_color; + let mute = &configuration.general.mute; + let mute_role_id = mute.role; + let take = &mute.take; + let is_currently_muted = member.roles.iter().any(|r| r.0 == mute_role_id); + + let result = + if let Err(add_role_result) = member.add_role(&ctx.discord().http, mute_role_id).await { + Some(Error::from(add_role_result)) + } else { + let removed_roles = member + .roles + .iter() + .filter(|r| take.contains(&r.0)) + .map(|r| r.to_string()) + .collect::>(); + + let removed = member + .remove_roles( + &ctx.discord().http, + &take.iter().map(|&r| RoleId::from(r)).collect::>(), + ) + .await; + + if let Err(remove_role_result) = removed { + Some(Error::from(remove_role_result)) + } else { + // Roles which were removed from the user + let updated: Document = Muted { + guild_id: Some(member.guild_id.0.to_string()), + expires: Some(unmute_time.timestamp() as u64), + reason: Some(reason.clone()), + taken_roles: if is_currently_muted { + // Prevent the bot from overriding the "take" field. + // This would happen otherwise, because the bot would accumulate the users roles and then override the value in the database + // resulting in the user being muted to have no roles to add back later. + None + } else { + Some(removed_roles) + }, + ..Default::default() + } + .into(); + + if let Err(database_update_result) = data + .database + .update::( + "muted", + Muted { + user_id: Some(member.user.id.0.to_string()), + ..Default::default() + } + .into(), + UpdateModifications::Document(doc! { "$set": updated}), + Some(UpdateOptions::builder().upsert(true).build()), + ) + .await + { + Some(database_update_result) + } else { + None + } + } + }; + + if let Some(pending_unmute) = data.pending_unmutes.read().await.get(&member.user.id.0) { + trace!("Cancelling pending unmute for {}", member.user.id.0); + pending_unmute.abort(); + } + + data.pending_unmutes.write().await.insert( + member.user.id.0, + queue_unmute_member( + ctx.discord(), + &data.database, + &member, + mute_role_id, + mute_duration.num_seconds() as u64, + ), + ); + + respond_mute_command( + &ctx, + ModerationKind::Mute( + reason, + unmute_time.format("%d/%m/%Y %H:%M").to_string(), + result, + ), + &member.user, + embed_color, + ) + .await +} + +/// Delete recent messages of a user. Cannot delete messages older than 14 days. +#[poise::command(slash_command)] +pub async fn purge( + ctx: Context<'_>, + #[description = "User"] member: Option, + #[description = "Until message"] until: Option, + #[min = 1] + #[max = 1000] + #[description = "Count"] + count: Option, +) -> Result<(), Error> { + // The maximum amount of times to page through messages. If paged over MAX_PAGES amount of times without deleting messages, break. + const MAX_PAGES: i8 = 2; + // The maximal amount of messages that we can fetch at all + const MAX_BULK_DELETE: i64 = 100; + // Discord does not let us bulk-delete messages older than 14 days + const MAX_BULK_DELETE_AGO_SECS: i64 = 60 * 60 * 24 * 14; + + let data = ctx.data().read().await; + let configuration = data.configuration.read().await; + let embed_color = configuration.general.embed_color; + let channel = ctx.channel_id(); + let too_old_timestamp = Utc::now().timestamp() - MAX_BULK_DELETE_AGO_SECS; + + let user = ctx.discord().http.get_current_user().await?; + let image = user + .avatar_url() + .unwrap_or_else(|| user.default_avatar_url()); + + let handle = ctx + .send(|f| { + f.embed(|f| { + f.title("Purging messages") + .description("Accumulating...") + .color(embed_color) + .thumbnail(&image) + }) + }) + .await?; + let mut response = handle.message().await?; + + ctx.defer().await?; + + let count_to_delete = count.unwrap_or(MAX_BULK_DELETE) as usize; + let mut deleted_amount = 0; + let mut empty_pages: i8 = 0; + + loop { + // Filter out messages that are too old + let mut messages = channel + .messages(&ctx.discord(), |m| { + m.limit(count_to_delete as u64).before(response.id) + }) + .await? + .into_iter() + .take_while(|m| m.timestamp.timestamp() > too_old_timestamp) + .collect::>(); + + // Filter for messages from the user + if let Some(ref member) = member { + messages = messages + .into_iter() + .filter(|msg| msg.author.id == member.user.id) + .collect::>(); + + debug!("Filtered messages by {}. Left: {}", member, messages.len()); + } + + // Filter for messages until the g/mutiven id + if let Some(ref message_id) = until { + if let Ok(message_id) = message_id.parse::() { + messages = messages + .into_iter() + .take_while(|m| m.id.0 > message_id) + .collect::>(); + debug!( + "Filtered messages until {}. Left: {}", + message_id, + messages.len() + ); + } + } + + let purge_count = messages.len(); + if purge_count > 0 { + deleted_amount += purge_count; + channel.delete_messages(&ctx.discord(), &messages).await?; + } else { + empty_pages += 1; + } + + if empty_pages >= MAX_PAGES || deleted_amount >= count_to_delete { + break; + } + } + + response + .to_mut() + .edit(&ctx.discord(), |e| { + e.set_embed( + serenity::CreateEmbed::default() + .title("Purge successful") + .field("Deleted messages", deleted_amount.to_string(), false) + .color(embed_color) + .thumbnail(image) + .clone(), + ) + }) + .await?; + Ok(()) +} diff --git a/src/db/database.rs b/src/db/database.rs new file mode 100644 index 0000000..3f3a0d6 --- /dev/null +++ b/src/db/database.rs @@ -0,0 +1,106 @@ +use std::error::Error; + +use bson::Document; +use mongodb::options::{ + ClientOptions, + DeleteOptions, + FindOneAndDeleteOptions, + FindOptions, + InsertOneOptions, + ResolverConfig, + UpdateModifications, + UpdateOptions, +}; +use mongodb::results::{DeleteResult, InsertOneResult, UpdateResult}; +use mongodb::{Client, Collection, Cursor}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +#[derive(Clone)] +pub struct Database { + client: Client, + database: String, +} + +impl Database { + pub async fn new(connection: &str, database: &str) -> Result> { + let options = + ClientOptions::parse_with_resolver_config(&connection, ResolverConfig::cloudflare()) + .await?; + let client = Client::with_options(options)?; + + Ok(Database { + client, + database: database.to_string(), + }) + } + + fn open(&self, collection: &str) -> Collection { + self.client.database(&self.database).collection(collection) + } + + pub async fn update( + &self, + collection: &str, + query: Document, + update_modifications: UpdateModifications, + options: Option, + ) -> Result> { + let result = self + .open::(collection) + .update_one(query, update_modifications, options) + .await?; + + Ok(result) + } + + pub async fn find( + &self, + collection: &str, + filter: Document, + options: Option, + ) -> Result, Box> { + let cursor = self.open(collection).find(filter, options).await?; + + Ok(cursor) + } + + pub async fn find_and_delete( + &self, + collection: &str, + filter: Document, + options: Option, + ) -> Result, Box> { + let result = self + .open(collection) + .find_one_and_delete(filter, options) + .await?; + + Ok(result) + } + + pub async fn insert( + &self, + collection: &str, + doc: T, + options: Option, + ) -> Result> { + let result = self.open(collection).insert_one(doc, options).await?; + + Ok(result) + } + + pub async fn delete( + &self, + collection: &str, + query: Document, + options: Option, + ) -> Result> { + let result = self + .open::(collection) + .delete_one(query, options) + .await?; + + Ok(result) + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..d5077a0 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,2 @@ +pub mod database; +pub mod model; diff --git a/src/db/model.rs b/src/db/model.rs new file mode 100644 index 0000000..5e7a0cb --- /dev/null +++ b/src/db/model.rs @@ -0,0 +1,32 @@ +use std::fmt::Display; + +use bson::Document; +use serde::{Deserialize, Serialize}; + +// Models +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Muted { + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub guild_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub taken_roles: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub expires: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +impl From for Document { + fn from(muted: Muted) -> Self { + bson::to_document(&muted).unwrap() + } +} + +// Display trait +impl Display for Muted { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "muted") + } +} diff --git a/src/events/guild_member_addition.rs b/src/events/guild_member_addition.rs index 610a48f..5bd01d2 100644 --- a/src/events/guild_member_addition.rs +++ b/src/events/guild_member_addition.rs @@ -1,5 +1,6 @@ use super::*; +use crate::utils::decancer::cure; pub async fn guild_member_addition(ctx: &serenity::Context, new_member: &serenity::Member) { - crate::utils::cure(ctx, new_member).await; -} \ No newline at end of file + cure(ctx, new_member).await; +} diff --git a/src/events/guild_member_update.rs b/src/events/guild_member_update.rs index ab553db..ddeb856 100644 --- a/src/events/guild_member_update.rs +++ b/src/events/guild_member_update.rs @@ -1,9 +1,10 @@ use super::*; +use crate::utils::decancer::cure; pub async fn guild_member_update( ctx: &serenity::Context, _old_if_available: &Option, new: &serenity::Member, ) { - crate::utils::cure(ctx, new).await; -} \ No newline at end of file + cure(ctx, new).await; +} diff --git a/src/events/message_create.rs b/src/events/message_create.rs index 40cbfa3..6e005fc 100644 --- a/src/events/message_create.rs +++ b/src/events/message_create.rs @@ -3,6 +3,7 @@ use regex::Regex; use tracing::debug; use super::*; +use crate::utils::bot::get_data_lock; fn contains_match(regex: &[Regex], text: &str) -> bool { regex.iter().any(|r| r.is_match(text)) @@ -14,10 +15,13 @@ pub async fn message_create(ctx: &serenity::Context, new_message: &serenity::Mes return; } - if let Some(message_response) = get_configuration_lock(ctx) + if let Some(message_response) = get_data_lock(ctx) .await .read() .await + .configuration + .read() + .await .message_responses .iter() .find(|&response| { diff --git a/src/events/mod.rs b/src/events/mod.rs index db1dba9..c9c7e65 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -2,24 +2,14 @@ use std::sync::Arc; use poise::serenity_prelude::{self as serenity, Mutex, RwLock, ShardManager, UserId}; -use crate::model::application::Configuration; -use crate::Error; +use crate::{Data, Error}; mod guild_member_addition; mod guild_member_update; mod message_create; +mod ready; mod thread_create; -// Share the lock reference between the threads in serenity framework -async fn get_configuration_lock(ctx: &serenity::Context) -> Arc> { - ctx.data - .read() - .await - .get::() - .expect("Expected Configuration in TypeMap.") - .clone() -} - pub struct Handler { options: poise::FrameworkOptions, data: T, @@ -55,9 +45,11 @@ impl Handler { // Manually dispatch events from serenity to poise #[serenity::async_trait] -impl serenity::EventHandler for Handler>> { - async fn ready(&self, _ctx: serenity::Context, ready: serenity::Ready) { +impl serenity::EventHandler for Handler>> { + async fn ready(&self, ctx: serenity::Context, ready: serenity::Ready) { *self.bot_id.write().await = Some(ready.user.id); + + ready::load_muted_members(&ctx, &ready).await; } async fn message(&self, ctx: serenity::Context, new_message: serenity::Message) { diff --git a/src/events/ready.rs b/src/events/ready.rs new file mode 100644 index 0000000..4ffd052 --- /dev/null +++ b/src/events/ready.rs @@ -0,0 +1,59 @@ +use chrono::Utc; +use tracing::trace; + +use super::*; +use crate::db::model::Muted; +use crate::utils::bot::get_data_lock; +use crate::utils::moderation::queue_unmute_member; + +pub async fn load_muted_members(ctx: &serenity::Context, _: &serenity::Ready) { + let data = get_data_lock(ctx).await; + let data = data.read().await; + let database = &data.database; + let mute_role_id = data.configuration.read().await.general.mute.role; + let mut pending_unmutes = data.pending_unmutes.write().await; + + let mut cursor = database + .find::( + "muted", + Muted { + ..Default::default() + } + .into(), + None, + ) + .await + .unwrap(); + + let http_ref = &ctx.http; + + while cursor.advance().await.unwrap() { + let current: Muted = cursor.deserialize_current().unwrap(); + let guild_id = current.guild_id.unwrap().parse::().unwrap(); + let member_id = current.user_id.unwrap().parse::().unwrap(); + + if let Ok(member) = http_ref + .get_guild(guild_id) + .await + .unwrap() + .member(http_ref, member_id) + .await + { + let amount_left = + std::cmp::max(current.expires.unwrap() as i64 - Utc::now().timestamp(), 0); + + pending_unmutes.insert( + member.user.id.0, + queue_unmute_member( + ctx, + database, + &member, + mute_role_id, + amount_left as u64, // i64 as u64 is handled properly here + ), + ); + } else { + trace!("Failed to find member {} in guild {}", member_id, guild_id); + } + } +} diff --git a/src/events/thread_create.rs b/src/events/thread_create.rs index 75840f7..2906e28 100644 --- a/src/events/thread_create.rs +++ b/src/events/thread_create.rs @@ -1,6 +1,7 @@ use tracing::{debug, error}; use super::*; +use crate::utils::bot::get_data_lock; pub async fn thread_create(ctx: &serenity::Context, thread: &serenity::GuildChannel) { if thread.member.is_some() { @@ -10,8 +11,14 @@ pub async fn thread_create(ctx: &serenity::Context, thread: &serenity::GuildChan debug!("Thread created: {:?}", thread); - let configuration_lock = get_configuration_lock(ctx).await; - let thread_introductions = &configuration_lock.read().await.thread_introductions; + let data_lock = get_data_lock(ctx).await; + let configuration_lock = data_lock.read().await; + + let thread_introductions = &configuration_lock + .configuration + .read() + .await + .thread_introductions; if let Some(introducer) = thread_introductions.iter().find(|introducer| { introducer diff --git a/src/logger.rs b/src/logger.rs index ab3b834..93984d9 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,4 +1,4 @@ pub fn init() { - // TODO: log to file - tracing_subscriber::fmt::init(); + // TODO: log to file + tracing_subscriber::fmt::init(); } diff --git a/src/main.rs b/src/main.rs index 50e537c..196d0cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,35 @@ +use std::collections::HashMap; use std::env; use std::sync::Arc; -use commands::configuration; +use commands::{configuration, moderation}; +use db::database::Database; use events::Handler; -use poise::serenity_prelude::{self as serenity, RwLock}; -use utils::load_configuration; +use poise::serenity_prelude::{self as serenity, RwLock, UserId}; +use tokio::task::JoinHandle; +use tracing::{error, trace}; +use utils::bot::load_configuration; use crate::model::application::Configuration; mod commands; +mod db; mod events; mod logger; mod model; mod utils; type Error = Box; -type Context<'a> = poise::Context<'a, Arc>, Error>; +type Context<'a> = poise::Context<'a, Arc>, Error>; -impl serenity::TypeMapKey for Configuration { - type Value = Arc>; +impl serenity::TypeMapKey for Data { + type Value = Arc>; +} + +pub struct Data { + configuration: Arc>, + database: Arc, + pending_unmutes: Arc>>>>, } #[tokio::main] @@ -34,13 +45,40 @@ async fn main() { configuration::register(), configuration::reload(), configuration::stop(), + moderation::mute(), + moderation::unmute(), + moderation::purge(), ]; poise::set_qualified_names(&mut commands); - let configuration = Arc::new(RwLock::new(load_configuration())); + let configuration = load_configuration(); + + let owners = configuration + .administrators + .users + .iter() + .cloned() + .map(UserId) + .collect::>() + .into_iter() + .collect(); + + let data = Arc::new(RwLock::new(Data { + configuration: Arc::new(RwLock::new(configuration)), + database: Arc::new( + Database::new( + &env::var("MONGODB_URI").expect("MONGODB_URI environment variable not set"), + "revanced_discord_bot", + ) + .await + .unwrap(), + ), + pending_unmutes: Arc::new(RwLock::new(HashMap::new())), + })); let handler = Arc::new(Handler::new( poise::FrameworkOptions { + owners, commands, on_error: |error| { Box::pin(async { @@ -52,7 +90,9 @@ async fn main() { command_check: Some(|ctx| { Box::pin(async move { if let Some(member) = ctx.author_member().await { - let administrators = &ctx.data().read().await.administrators; + let data_lock = &ctx.data().read().await; + let configuration = &data_lock.configuration.read().await; + let administrators = &configuration.administrators; if !(administrators .users @@ -69,6 +109,25 @@ async fn main() { .any(|member_role| member_role.0 == role_id) })) { + if let Err(e) = ctx + .channel_id() + .send_message(&ctx.discord().http, |m| { + m.embed(|e| { + e.title("Permission error") + .description( + "You do not have permission to use this command.", + ) + .color(configuration.general.embed_color) + .thumbnail(member.user.avatar_url().unwrap_or_else( + || member.user.default_avatar_url(), + )) + }) + }) + .await + { + error!("Error sending message: {:?}", e) + } + trace!("{} is not an administrator.", member.user.name); return Ok(false); // Not an administrator, don't allow command execution } } @@ -83,12 +142,12 @@ async fn main() { }, ..Default::default() }, - configuration.clone(), // Pass configuration as user data for the framework + data.clone(), // Pass configuration as user data for the framework )); let mut client = serenity::Client::builder( env::var("DISCORD_AUTHORIZATION_TOKEN") - .expect("Could not load Discord authorization token"), + .expect("DISCORD_AUTHORIZATION_TOKEN environment variable not set"), serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT | serenity::GatewayIntents::GUILD_MEMBERS, @@ -97,11 +156,7 @@ async fn main() { .await .unwrap(); - client - .data - .write() - .await - .insert::(configuration); + client.data.write().await.insert::(data); handler .set_shard_manager(client.shard_manager.clone()) diff --git a/src/model/application.rs b/src/model/application.rs index 6bc5613..c85dac5 100644 --- a/src/model/application.rs +++ b/src/model/application.rs @@ -1,8 +1,6 @@ -use std::{ - fs::{self, File}, - io::{Read, Result, Write}, - path::Path, -}; +use std::fs::{self, File}; +use std::io::{Read, Result, Write}; +use std::path::Path; use dirs::config_dir; use regex::Regex; @@ -19,139 +17,150 @@ pub struct Configuration { const CONFIG_PATH: &str = "configuration.json"; impl Configuration { - fn save(&self) -> Result<()> { - let sys_config_dir = config_dir().expect("find config dir"); + fn save(&self) -> Result<()> { + let sys_config_dir = config_dir().expect("find config dir"); - fs::create_dir_all(format!("{}/revanced-discord-bot", sys_config_dir.to_string_lossy())) - .expect("create config dir"); + fs::create_dir_all(format!( + "{}/revanced-discord-bot", + sys_config_dir.to_string_lossy() + )) + .expect("create config dir"); - let mut file = File::create(CONFIG_PATH)?; - let json = serde_json::to_string_pretty(&self)?; - file.write_all(json.as_bytes())?; - Ok(()) - } + let mut file = File::create(CONFIG_PATH)?; + let json = serde_json::to_string_pretty(&self)?; + file.write_all(json.as_bytes())?; + Ok(()) + } - pub fn load() -> Result { - let sys_config_dir = config_dir().expect("Can not find the configuration directory."); - let sys_config = - format!("{}/revanced-discord-bot/{CONFIG_PATH}", sys_config_dir.to_string_lossy()); + pub fn load() -> Result { + let sys_config_dir = config_dir().expect("Can not find the configuration directory."); + let sys_config = format!( + "{}/revanced-discord-bot/{CONFIG_PATH}", + sys_config_dir.to_string_lossy() + ); - // config file in current dir - let mut file = if Path::new(CONFIG_PATH).exists() { - File::open(CONFIG_PATH)? - } - // config file in system dir (on *nix: `~/.config/revanced-discord-bot/`) - else if Path::new(&sys_config).exists() { - File::open(sys_config)? - } - // create defalt config - else { - let default_config = Configuration::default(); - default_config.save()?; + // config file in current dir + let mut file = if Path::new(CONFIG_PATH).exists() { + File::open(CONFIG_PATH)? + } + // config file in system dir (on *nix: `~/.config/revanced-discord-bot/`) + else if Path::new(&sys_config).exists() { + File::open(sys_config)? + } + // create defalt config + else { + let default_config = Configuration::default(); + default_config.save()?; - File::open(sys_config)? - }; + File::open(sys_config)? + }; - let mut buf = String::new(); - file.read_to_string(&mut buf)?; + let mut buf = String::new(); + file.read_to_string(&mut buf)?; - Ok(serde_json::from_str(&buf)?) - } + Ok(serde_json::from_str(&buf)?) + } } #[derive(Default, Serialize, Deserialize)] pub struct General { pub embed_color: i32, + pub mute: Mute, } +#[derive(Default, Serialize, Deserialize)] +pub struct Mute { + pub role: u64, + pub take: Vec, +} #[derive(Default, Serialize, Deserialize)] pub struct Administrators { - pub roles: Vec, - pub users: Vec, + pub roles: Vec, + pub users: Vec, } #[derive(Serialize, Deserialize)] pub struct Introduction { - pub channels: Vec, - pub response: Response, + pub channels: Vec, + pub response: Response, } #[derive(Serialize, Deserialize)] pub struct MessageResponse { - pub includes: Includes, - pub excludes: Excludes, - pub condition: Condition, - pub response: Response, + pub includes: Includes, + pub excludes: Excludes, + pub condition: Condition, + pub response: Response, } #[derive(Serialize, Deserialize)] pub struct Response { - pub message: Option, - pub embed: Option, + pub message: Option, + pub embed: Option, } #[derive(Serialize, Deserialize)] pub struct Embed { - pub title: String, - pub description: String, - pub color: i32, - pub fields: Vec, - pub footer: Footer, - pub image: Image, - pub thumbnail: Thumbnail, - pub author: Author, + pub title: String, + pub description: String, + pub color: i32, + pub fields: Vec, + pub footer: Footer, + pub image: Image, + pub thumbnail: Thumbnail, + pub author: Author, } #[derive(Serialize, Deserialize)] pub struct Field { - pub name: String, - pub value: String, - pub inline: bool, + pub name: String, + pub value: String, + pub inline: bool, } #[derive(Serialize, Deserialize)] pub struct Footer { - pub text: String, - pub icon_url: String, + pub text: String, + pub icon_url: String, } #[derive(Serialize, Deserialize)] pub struct Image { - pub url: String, + pub url: String, } #[derive(Serialize, Deserialize)] pub struct Thumbnail { - pub url: String, + pub url: String, } #[derive(Serialize, Deserialize)] pub struct Author { - pub name: String, - pub icon_url: String, - pub url: String, + pub name: String, + pub icon_url: String, + pub url: String, } #[derive(Serialize, Deserialize)] pub struct Includes { - pub channels: Vec, - #[serde(rename = "match", with = "serde_regex")] - pub match_field: Vec, + pub channels: Vec, + #[serde(rename = "match", with = "serde_regex")] + pub match_field: Vec, } #[derive(Serialize, Deserialize)] pub struct Excludes { - pub roles: Vec, - #[serde(rename = "match", with = "serde_regex")] - pub match_field: Vec, + pub roles: Vec, + #[serde(rename = "match", with = "serde_regex")] + pub match_field: Vec, } #[derive(Serialize, Deserialize)] pub struct Condition { - pub user: User, + pub user: User, } #[derive(Serialize, Deserialize)] pub struct User { - pub server_age: i64, + pub server_age: i64, } diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 8b5afa4..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,58 +0,0 @@ -use decancer::Decancer; -use poise::serenity_prelude::{self as serenity, CreateEmbed}; -use tracing::{error, info}; - -use crate::model::application::Configuration; - -const DECANCER: Decancer = Decancer::new(); - -pub(crate) fn load_configuration() -> Configuration { - Configuration::load().expect("Failed to load configuration") -} - -trait PoiseEmbed { - fn create_embed(self, embed: &mut CreateEmbed) -> &mut CreateEmbed; -} - -impl PoiseEmbed for crate::model::application::Embed { - fn create_embed(self, embed: &mut CreateEmbed) -> &mut CreateEmbed { - embed - .title(self.title) - .description(self.description) - .color(self.color) - .fields( - self.fields - .iter() - .map(|field| (field.name.clone(), field.value.clone(), field.inline)), - ) - .footer(|f| { - f.text(self.footer.text); - f.icon_url(self.footer.icon_url) - }) - .thumbnail(self.thumbnail.url) - .image(self.image.url) - .author(|a| a.name(self.author.name).icon_url(self.author.icon_url)) - } -} - -pub async fn cure(ctx: &serenity::Context, member: &serenity::Member) { - let display_name = member.display_name(); - let name = display_name.to_string(); - - let cured_user_name = DECANCER.cure(&name); - - if name.to_lowercase() == cured_user_name { - return; // username is already cured - } - - match member - .guild_id - .edit_member(&ctx.http, member.user.id, |edit_member| { - edit_member.nickname(cured_user_name) - }) - .await - { - Ok(_) => info!("Cured user {}", member.user.tag()), - Err(err) => error!("Failed to cure user {}: {}", name, err), - } -} diff --git a/src/utils/bot.rs b/src/utils/bot.rs new file mode 100644 index 0000000..b7010cf --- /dev/null +++ b/src/utils/bot.rs @@ -0,0 +1,20 @@ +use std::sync::Arc; + +use poise::serenity_prelude::{self as serenity, RwLock}; + +use crate::model::application::Configuration; +use crate::Data; + +pub fn load_configuration() -> Configuration { + Configuration::load().expect("Failed to load configuration") +} + +// Share the lock reference between the threads in serenity framework +pub async fn get_data_lock(ctx: &serenity::Context) -> Arc> { + ctx.data + .read() + .await + .get::() + .expect("Expected Configuration in TypeMap.") + .clone() +} diff --git a/src/utils/decancer.rs b/src/utils/decancer.rs new file mode 100644 index 0000000..27bde07 --- /dev/null +++ b/src/utils/decancer.rs @@ -0,0 +1,28 @@ +use ::decancer::Decancer; +use tracing::{error, info}; + +use super::*; + +const DECANCER: Decancer = Decancer::new(); + +pub async fn cure(ctx: &serenity::Context, member: &serenity::Member) { + let display_name = member.display_name(); + let name = display_name.to_string(); + + let cured_user_name = DECANCER.cure(&name); + + if name.to_lowercase() == cured_user_name { + return; // username is already cured + } + + match member + .guild_id + .edit_member(&ctx.http, member.user.id, |edit_member| { + edit_member.nickname(cured_user_name) + }) + .await + { + Ok(_) => info!("Cured user {}", member.user.tag()), + Err(err) => error!("Failed to cure user {}: {}", name, err), + } +} diff --git a/src/utils/embed.rs b/src/utils/embed.rs new file mode 100644 index 0000000..b6d0815 --- /dev/null +++ b/src/utils/embed.rs @@ -0,0 +1,26 @@ +use poise::serenity_prelude::CreateEmbed; + +trait PoiseEmbed { + fn create_embed(self, embed: &mut CreateEmbed) -> &mut CreateEmbed; +} + +impl PoiseEmbed for crate::model::application::Embed { + fn create_embed(self, embed: &mut CreateEmbed) -> &mut CreateEmbed { + embed + .title(self.title) + .description(self.description) + .color(self.color) + .fields( + self.fields + .iter() + .map(|field| (field.name.clone(), field.value.clone(), field.inline)), + ) + .footer(|f| { + f.text(self.footer.text); + f.icon_url(self.footer.icon_url) + }) + .thumbnail(self.thumbnail.url) + .image(self.image.url) + .author(|a| a.name(self.author.name).icon_url(self.author.icon_url)) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..c07315a --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,6 @@ +use poise::serenity_prelude::{self as serenity, Member, RoleId}; + +pub mod bot; +pub mod decancer; +pub mod embed; +pub mod moderation; diff --git a/src/utils/moderation.rs b/src/utils/moderation.rs new file mode 100644 index 0000000..cda69b6 --- /dev/null +++ b/src/utils/moderation.rs @@ -0,0 +1,102 @@ +use tokio::task::JoinHandle; + +use super::*; +use crate::db::database::Database; +use crate::db::model::Muted; +use crate::{Context, Error}; + +pub enum ModerationKind { + Mute(String, String, Option), // Reason, Expires, Error + Unmute(Option), // Error +} + +pub fn queue_unmute_member( + ctx: &serenity::Context, + database: &Database, + member: &Member, + mute_role_id: u64, + mute_duration: u64, +) -> JoinHandle> { + let ctx = ctx.clone(); + let database = database.clone(); + let mut member = member.clone(); + + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(mute_duration)).await; + + let delete_result = database + .find_and_delete::( + "muted", + Muted { + user_id: Some(member.user.id.0.to_string()), + ..Default::default() + } + .into(), + None, + ) + .await; + + if let Err(database_remove_result) = delete_result { + Some(database_remove_result) + } else if let Some(find_result) = delete_result.unwrap() { + let taken_roles = find_result + .taken_roles + .unwrap() + .into_iter() + .map(|r| RoleId::from(r.parse::().unwrap())) + .collect::>(); + + if let Err(add_role_result) = member.add_roles(&ctx.http, &taken_roles).await { + Some(Error::from(add_role_result)) + } else if let Err(remove_result) = member.remove_role(ctx.http, mute_role_id).await { + Some(Error::from(remove_result)) + } else { + None + } + } else { + None + } + }) +} + +pub async fn respond_mute_command( + ctx: &Context<'_>, + moderation: ModerationKind, + user: &serenity::User, + embed_color: i32, +) -> Result<(), Error> { + let tag = user.tag(); + let image = user + .avatar_url() + .unwrap_or_else(|| user.default_avatar_url()); + + ctx.send(|f| { + f.embed(|f| { + match moderation { + ModerationKind::Mute(reason, expires, error) => match error { + Some(err) => f.title(format!("Failed to mute {}", tag)).field( + "Exception", + err.to_string(), + false, + ), + None => f.title(format!("Muted {}", tag)), + } + .field("Reason", reason, false) + .field("Expires", expires, false), + ModerationKind::Unmute(error) => match error { + Some(err) => f.title(format!("Failed to unmute {}", tag)).field( + "Exception", + err.to_string(), + false, + ), + None => f.title(format!("Unmuted {}", tag)), + }, + } + .color(embed_color) + .thumbnail(image) + }) + }) + .await?; + + Ok(()) +}