commit ebdc182e86afd906e25531877a1d486fd9cee6a8 Author: BuyMyMojo Date: Sat Jun 29 01:22:22 2024 +1000 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06e2b3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ +builds/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +# Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# IDE stuff +*/.idea +*/.dccache + +# Added by cargo +/target + +# My trace log files +*_rusted-fbt.verbose.log +*_rusted-fbt.info.log \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..504fa2b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3218 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +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 = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "async-channel" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-trait" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "async-tungstenite" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b71b31561643aa8e7df3effe284fa83ab1a840e52294c5f4bd7bfd8b2becbb" +dependencies = [ + "futures-io", + "futures-util", + "log", + "pin-project-lite", + "tokio", + "tokio-rustls 0.23.4", + "tungstenite", + "webpki-roots 0.22.5", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.1", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "bytecheck" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11cac2c12b5adc6570dad2ee1b87eff4955dac476fe12d81e5fdd352e52406f" +dependencies = [ + "bytecheck_derive", + "ptr_meta", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e576ebe98e605500b3c8041bb888e966653577172df6dd97398714eb30b9bf" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + +[[package]] +name = "castaway" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" + +[[package]] +name = "cc" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "chrono-tz" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1369bc6b9e9a7dfdae2055f6ec151fe9c554a9d23d357c0237cee2e25eaabb7" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2f5ebdc942f57ed96d560a6d1a459bae5851102a25d5bf89dc04ae453e31ecf" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "4.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.10.0", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "colored" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +dependencies = [ + "is-terminal", + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "curl" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2 0.4.7", + "winapi", +] + +[[package]] +name = "curl-sys" +version = "0.4.59+curl-7.86.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "winapi", +] + +[[package]] +name = "cxx" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 1.0.103", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "darling" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" +dependencies = [ + "darling_core 0.14.2", + "darling_macro 0.14.2", +] + +[[package]] +name = "darling" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +dependencies = [ + "darling_core 0.20.1", + "darling_macro 0.20.1", +] + +[[package]] +name = "darling_core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.103", +] + +[[package]] +name = "darling_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.37", +] + +[[package]] +name = "darling_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" +dependencies = [ + "darling_core 0.14.2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "darling_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +dependencies = [ + "darling_core 0.20.1", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.4", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide 0.5.4", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "h2" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.7", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls 0.21.7", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" +dependencies = [ + "equivalent", + "hashbrown 0.14.1", + "serde", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.3", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "isahc" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" +dependencies = [ + "async-channel", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener", + "futures-lite", + "http", + "log", + "mime", + "once_cell", + "polling", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + +[[package]] +name = "iso8601-duration" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b51dd97fa24074214b9eb14da518957573f4dec3189112610ae1ccec9ac464" +dependencies = [ + "nom", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa4b4af834c6cfd35d8763d359661b90f2e45d8f750a0849156c7f4671af09c" +dependencies = [ + "base64 0.13.1", + "ring", + "serde", + "serde_json", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "cfg-if", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "libnghttp2-sys" +version = "0.1.7+1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + +[[package]] +name = "linkify" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780" +dependencies = [ + "memchr", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "meilisearch-sdk" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e980fc979f653877693451f5b31d065a6f607c45988ac117a8316c90f73c7cdc" +dependencies = [ + "async-trait", + "futures", + "isahc", + "iso8601-duration", + "js-sys", + "jsonwebtoken", + "log", + "serde", + "serde_json", + "time", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "merge" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" +dependencies = [ + "merge_derive", + "num-traits", +] + +[[package]] +name = "merge_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi 0.1.19", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +dependencies = [ + "num-traits", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owo-colors" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55" + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.4", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.42.0", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "pastemyst" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "508b2e93089ce1ea43b8659570c1c65d459bfae0a2779a44d6193e60a6983b5f" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "poise" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649248bcf202ebd1fe3202a7eaf2f705466c92f8d32a644ef565d9221e3ff6f1" +dependencies = [ + "async-trait", + "derivative", + "futures-core", + "futures-util", + "log", + "once_cell", + "parking_lot 0.12.1", + "poise_macros", + "regex", + "serenity", + "tokio", +] + +[[package]] +name = "poise_macros" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05960fe87728cef0533d13f8ead89da228f5eb7840bee5602eeaa8db150084a0" +dependencies = [ + "darling 0.14.2", + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "polling" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.103", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "pulldown-cmark" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca36dea94d187597e104a5c8e4b07576a8a45aa5db48a65e12940d3eb7461f55" +dependencies = [ + "bitflags 1.3.2", + "getopts", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redis" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f49cdc0bb3f412bf8e7d1bd90fe1d9eb10bc5c399ba90973c14662a27b3f8ba" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.4.7", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79af64b4b6362ffba04eef3a4e10829718a4896dac19daa741851c86781edf95" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.7", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.24.1", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 0.25.2", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rkyv" +version = "0.7.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cec2b3485b07d96ddfd3134767b8a447b45ea4eb91448d0a35180ec0ffd5ed15" +dependencies = [ + "bytecheck", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eaedadc88b53e36dd32d940ed21ae4d850d5916f2581526921f553a72ac34c4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rusted-fbt" +version = "2.7.2" +dependencies = [ + "anyhow", + "chrono", + "chrono-tz", + "clap 4.4.4", + "colored", + "csv", + "derivative", + "futures", + "linkify", + "maplit", + "meilisearch-sdk", + "merge", + "once_cell", + "pastemyst", + "poise", + "rand", + "redis", + "regex", + "reqwest", + "serde", + "serde_json", + "serde_with", + "strip_markdown", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", + "unix-time", + "uwuify", + "winres", +] + +[[package]] +name = "rustix" +version = "0.38.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys 0.36.1", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +dependencies = [ + "base64 0.21.2", + "chrono", + "hex", + "indexmap 1.9.2", + "indexmap 2.0.1", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +dependencies = [ + "darling 0.20.1", + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "serenity" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fd5e7b5858ad96e99d440138f34f5b98e1b959ebcd3a1036203b30e78eb788" +dependencies = [ + "async-trait", + "async-tungstenite", + "base64 0.13.1", + "bitflags 1.3.2", + "bytes", + "cfg-if", + "chrono", + "dashmap", + "flate2", + "futures", + "mime", + "mime_guess", + "parking_lot 0.12.1", + "percent-encoding", + "reqwest", + "rustversion", + "serde", + "serde-value", + "serde_json", + "time", + "tokio", + "tracing", + "typemap_rev", + "url", +] + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel", + "futures-core", + "futures-io", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strip_markdown" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c754386109f9adc8ca62513c51cf81cc2d8502588064103aa8e9f69b4276da" +dependencies = [ + "log", + "pulldown-cmark", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.4", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.7", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.7", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "nu-ansi-term", + "parking_lot 0.12.1", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "tungstenite" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand", + "rustls 0.20.7", + "sha-1", + "thiserror", + "url", + "utf-8", + "webpki", +] + +[[package]] +name = "typemap_rev" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5b74f0a24b5454580a79abb6994393b09adf0ab8070f15827cb666255de155" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unix-time" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d919c7a852a6cf0fc00baf8114303e7a6d6c8c8049e5c740aa1fc1b86a22b8" +dependencies = [ + "rkyv", + "serde", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uwuify" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db6840b7adcfd2e866c79157cc890ecdbbc1f739607134039ae64eaa6c07e24" +dependencies = [ + "clap 2.34.0", + "owo-colors", + "parking_lot 0.11.2", + "thiserror", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.103", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "wasm-streams" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" +dependencies = [ + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.0", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm 0.42.0", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4263060 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,74 @@ +[[bin]] +name = "rusted-fbt" +path = "src/main.rs" + +[lib] +name = "rusted_fbt_lib" +path = "src/lib/lib.rs" + +[package] +name = "rusted-fbt" +version = "2.7.2" +edition = "2021" +publish = false + +[features] +default = [ + "database", +] # Comment out this line when adding new imports and functions to see what needs a #[cfg(feature = "database")] +database = [] +beta = [] + +[dependencies] +poise = { version = "0.5.6", features = [ + "cache", + "collector", +] } +tokio = { version = "1.32.0", features = ["full"] } +csv = "1.2.2" +serde = "1.0.188" +serde_json = "1.0.107" +uwuify = "0.2.2" +chrono = "0.4.31" +clap = { version = "4.0.32", features = ["derive"] } +maplit = "1.0.2" +colored = "2.0.4" +strip_markdown = "0.2.0" +reqwest = "0.11.20" +futures = "0.3.28" +redis = { version = "0.23.3", features = ["aio", "tokio-comp"] } +merge = { version = "0.1.0", features = ["std", "num", "derive"] } +derivative = "2.2.0" +rand = "0.8.5" +unix-time = "0.1.5" +regex = "1.9.5" +once_cell = "1.18.0" +chrono-tz = "0.8.3" +meilisearch-sdk = "0.17.0" +serde_with = "3.3.0" +tracing = { version = "0.1.37", features = ["async-await"] } +tracing-subscriber = { version = "0.3.17", features = [ + "parking_lot", + "registry", +] } +linkify = "0.10.0" +anyhow = "1.0.75" +thiserror = "1.0.49" +pastemyst = "1.0.0" +# oxipng = "5.0.1" +# mozjpeg = "0.9.3" +# lz4_flex = "0.9.3" + +[target.'cfg(windows)'.build-dependencies] +winres = "0.1.12" + +[profile.dev.package."*"] +opt-level = 1 + +[profile.release] +opt-level = 3 +lto = "thin" + +[profile.release-full] +inherits = "release" +lto = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..2abcb61 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# FBT Security - Rust diff --git a/assets/DILLIGAF.ogg b/assets/DILLIGAF.ogg new file mode 100644 index 0000000..c10563c Binary files /dev/null and b/assets/DILLIGAF.ogg differ diff --git a/assets/Discord_meme.mp4 b/assets/Discord_meme.mp4 new file mode 100644 index 0000000..4d1ff03 Binary files /dev/null and b/assets/Discord_meme.mp4 differ diff --git a/assets/FBT_Security_Unedited.ogg b/assets/FBT_Security_Unedited.ogg new file mode 100644 index 0000000..31a6911 Binary files /dev/null and b/assets/FBT_Security_Unedited.ogg differ diff --git a/assets/PomfPomf.ogg b/assets/PomfPomf.ogg new file mode 100644 index 0000000..0abfb5b Binary files /dev/null and b/assets/PomfPomf.ogg differ diff --git a/assets/amogus.gif b/assets/amogus.gif new file mode 100644 index 0000000..d01dcb9 Binary files /dev/null and b/assets/amogus.gif differ diff --git a/assets/astro.ogg b/assets/astro.ogg new file mode 100644 index 0000000..db9ecb0 Binary files /dev/null and b/assets/astro.ogg differ diff --git a/assets/egirls.ogg b/assets/egirls.ogg new file mode 100644 index 0000000..ee387b9 Binary files /dev/null and b/assets/egirls.ogg differ diff --git a/assets/erp-tonight.mp3 b/assets/erp-tonight.mp3 new file mode 100644 index 0000000..f1621cd Binary files /dev/null and b/assets/erp-tonight.mp3 differ diff --git a/assets/femboys.mp3 b/assets/femboys.mp3 new file mode 100644 index 0000000..f92247f Binary files /dev/null and b/assets/femboys.mp3 differ diff --git a/assets/icon.ico b/assets/icon.ico new file mode 100644 index 0000000..b1ac067 Binary files /dev/null and b/assets/icon.ico differ diff --git a/assets/languages.txt b/assets/languages.txt new file mode 100644 index 0000000..92bf18e --- /dev/null +++ b/assets/languages.txt @@ -0,0 +1,110 @@ +Supported languages and their language codes (case-sensitive!): +(Download this file to see all supported languages) + +Afrikaans : af +Albanian : sq +Amharic : am +Arabic : ar +Armenian : hy +Azerbaijani : az +Basque : eu +Belarusian : be +Bengali : bn +Bosnian : bs +Bulgarian : bg +Catalan : ca +Cebuano : ceb +Chichewa : ny +Chinese (simplified) : zh-cn +Chinese (traditional) : zh-tw +Corsican : co +Croatian : hr +Czech : cs +Danish : da +Dutch : nl +English : en +Esperanto : eo +Estonian : et +Filipino : tl +Finnish : fi +French : fr +Frisian : fy +Galician : gl +Georgian : ka +German : de +Greek : el +Gujarati : gu +Haitian creole : ht +Hausa : ha +Hawaiian : haw +Hebrew : iw +Hebrew : he +Hindi : hi +Hmong : hmn +Hungarian : hu +Icelandic : is +Igbo : ig +Indonesian : id +Irish : ga +Italian : it +Japanese : ja +Javanese : jw +Kannada : kn +Kazakh : kk +Khmer : km +Korean : ko +Kurdish (kurmanji) : ku +Kyrgyz : ky +Lao : lo +Latin : la +Latvian : lv +Lithuanian : lt +Luxembourgish : lb +Macedonian : mk +Malagasy : mg +Malay : ms +Malayalam : ml +Maltese : mt +Maori : mi +Marathi : mr +Mongolian : mn +Myanmar (burmese) : my +Nepali : ne +Norwegian : no +Odia : or +Pashto : ps +Persian : fa +Polish : pl +Portuguese : pt +Punjabi : pa +Romanian : ro +Russian : ru +Samoan : sm +Scots gaelic : gd +Serbian : sr +Sesotho : st +Shona : sn +Sindhi : sd +Sinhala : si +Slovak : sk +Slovenian : sl +Somali : so +Spanish : es +Sundanese : su +Swahili : sw +Swedish : sv +Tajik : tg +Tamil : ta +Telugu : te +Thai : th +Turkish : tr +Ukrainian : uk +Urdu : ur +Uyghur : ug +Uzbek : uz +Vietnamese : vi +Welsh : cy +Xhosa : xh +Yiddish : yi +Yoruba : yo +Zulu : zu diff --git a/assets/lyrics.txt b/assets/lyrics.txt new file mode 100644 index 0000000..188ccb1 --- /dev/null +++ b/assets/lyrics.txt @@ -0,0 +1,51 @@ +Astro-naut +What you know about rollin' down in the deep? +When your brain goes numb, you can call that mental freeze +When these people talk too much, put that shit in slow motion, yeah +I feel like an astronaut in the ocean, ayy +What you know about rollin' down in the deep? +When your brain goes numb, you can call that mental freeze +When these people talk too much, put that shit in slow motion, yeah +I feel like an astronaut in the ocean +She say that I'm cool (damn straight) +I'm like "yeah, that's true" (that's true) +I believe in G-O-D (ayy) +Don't believe in T-H-O-T +She keep playing me dumb (play me) +I'ma play her for fun (uh-huh) +Y'all don't really know my mental +Lemme give you the picture like stencil +Falling out, in a drought +No flow, rain wasn't pouring down (pouring down) +See, that pain was all around +See, my mode was kinda lounged +Didn't know which-which way to turn +Flow was cool but I still felt burnt +Energy up, you can feel my surge +I'ma kill everything like this purge (ayy) +Let's just get this straight for a second, I'ma work +Even if I don't get paid for progression, I'ma get it (get it) +Everything that I do is electric +I'ma keep it in a motion, keep it moving like kinetic, ayy (yeah, yeah, yeah, yeah) +Put this shit in a frame, better know I don't blame +Everything that I say, man I seen you deflate +Let me elevate, this ain't a prank +Have you walkin' on a plank, la-la-la-la-la, like +Both hands together, God, let me pray (now let me pray) +Uh, I've been going right, right around, call that relay (Masked Wolf) +Pass the baton, back and I'm on +Swimming in the pool, Kendrick Lamar, uh +Want a piece of this, a piece of mine, my peace a sign +Can you please read between the lines? +My rhyme's inclined to break your spine +They say that I'm so fine +You could never match my grind +Please do not, not waste my time +What you know about rollin' down in the deep? +When your brain goes numb, you can call that mental freeze +When these people talk too much, put that shit in slow motion, yeah +I feel like an astronaut in the ocean, ayy +What you know about rollin' down in the deep? +When your brain goes numb, you can call that mental freeze +When these people talk too much, put that shit in slow motion, yeah +I feel like an astronaut in the ocean \ No newline at end of file diff --git a/assets/pog-frog.gif b/assets/pog-frog.gif new file mode 100644 index 0000000..2de3059 Binary files /dev/null and b/assets/pog-frog.gif differ diff --git a/assets/toxic.gif b/assets/toxic.gif new file mode 100644 index 0000000..4b6b081 Binary files /dev/null and b/assets/toxic.gif differ diff --git a/assets/user.gif b/assets/user.gif new file mode 100644 index 0000000..54a3317 Binary files /dev/null and b/assets/user.gif differ diff --git a/assets/white.mp4 b/assets/white.mp4 new file mode 100644 index 0000000..19cd33e Binary files /dev/null and b/assets/white.mp4 differ diff --git a/assets/wrestle.mp4 b/assets/wrestle.mp4 new file mode 100644 index 0000000..6000972 Binary files /dev/null and b/assets/wrestle.mp4 differ diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..3fd6821 --- /dev/null +++ b/build.ps1 @@ -0,0 +1 @@ +$Env:RUSTFLAGS='-C target-cpu=native'; cargo build --release \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..bf14e46 --- /dev/null +++ b/build.rs @@ -0,0 +1,21 @@ +use std::io; +#[cfg(windows)] +use winres::WindowsResource; + +#[allow(clippy::unnecessary_wraps)] +// I'm just keeping this as the example had it, no need to warn me about this since it doesn't affect the actual code +/// This code will set the windows program icon to the fbt logo +/// +/// # Errors +/// +/// This function will return an error if the icon file cannot be found at compile time. +fn main() -> io::Result<()> { + #[cfg(windows)] + { + WindowsResource::new() + // This path can be absolute, or relative to your crate root. + .set_icon("assets/icon.ico") + .compile()?; + } + Ok(()) +} diff --git a/clippy.sh b/clippy.sh new file mode 100644 index 0000000..2c3610a --- /dev/null +++ b/clippy.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cargo +nightly clippy --all-features -Z unstable-options "$@" -- -D clippy::correctness -W clippy::style -W clippy::complexity -W clippy::perf -W clippy::nursery -W clippy::pedantic -W clippy::cargo -A clippy::too_many_lines +# cargo +nightly clippy --all-features -Z unstable-options -- -D clippy::correctness -W clippy::style -W clippy::complexity -W clippy::perf -W clippy::nursery -W clippy::pedantic -W clippy::cargo -A clippy::too_many_lines \ No newline at end of file diff --git a/haswell.Dockerfile b/haswell.Dockerfile new file mode 100644 index 0000000..2b6c506 --- /dev/null +++ b/haswell.Dockerfile @@ -0,0 +1,14 @@ +FROM rust:latest as builder +WORKDIR /usr/src/rusted-fbt +COPY . . +RUN env RUSTFLAGS="-C target-cpu=haswell" cargo install --path . + +FROM debian:buster-slim +# RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/* +COPY --from=builder /usr/local/cargo/bin/rusted-fbt /usr/local/bin/rusted-fbt +run apt update -y +run apt install ca-certificates -y +run apt update -y +# This is the only dependancie missing as far as I can tell which is great +run apt install libssl-dev -y +CMD ["rusted-fbt"] \ No newline at end of file diff --git a/slow build.ps1 b/slow build.ps1 new file mode 100644 index 0000000..972c03b --- /dev/null +++ b/slow build.ps1 @@ -0,0 +1 @@ +$Env:RUSTFLAGS='-C target-cpu=native'; cargo build --profile=release-full-lto \ No newline at end of file diff --git a/src/commands/_deprecated.rs b/src/commands/_deprecated.rs new file mode 100644 index 0000000..c028be0 --- /dev/null +++ b/src/commands/_deprecated.rs @@ -0,0 +1,224 @@ +// https://sqlitebrowser.org/ +/// Transfer from sqlite DB, get the json files using "File>Export>Table(s) to json" in sqlitebrowser +#[cfg(feature = "database")] +#[allow(non_snake_case, non_camel_case_types)] // Keeping these badly names variables since that's what they are called in the SQLite DB +#[poise::command(slash_command, category = "Admin", owners_only, hide_in_help)] +async fn sqlite_transfer( + ctx: Context<'_>, + #[description = "vrc_data.json"] vrc_data: Attachment, + // #[description = "no_bot_perms.json"] no_bot_perms: Attachment, + #[description = "guild_channels.json"] guild_channels: Attachment, + #[description = "authorized_users.json"] authorized_users: Attachment, + #[description = "Cleared_IDs.json"] cleard_ids: Attachment, + #[description = "Monitored Guilds.json"] monitored_guilds: Attachment, +) -> Result<(), Error> { + ctx.defer().await?; + + #[derive(Debug, Deserialize)] + struct AuthorizedUser { + user_id: u64, + guild_id: u64, + } + + #[derive(Debug, Deserialize)] + struct ClearedID { + Discord_ID: u64, + Name: String, + Where_found: String, + Cleared_Reason: String, + } + + #[derive(Debug, Deserialize)] + struct MonitoredGuild { + Guild_Name: String, + Guild_ID: u64, + Invite_link: Option, + Updated: Option, + DMCA_Takedown_Nuked: Option, + } + + #[derive(Debug, Deserialize)] + struct GuildChannel { + guild: u64, + channel_id: u64, + kick_active: u64, + Name: String, + } + + #[derive(Debug, Deserialize)] + struct vrc_data { + vrc_id: Option, + guild_id: Option, + name: Option, + discord_id: u64, + reason: String, + image: Option, + extra: Option, + } + + let mut con = open_redis_connection().await?; + let mut pipe = redis::pipe(); + + let msg = ctx.say("Downloading files").await?; + + let authorized_users_json = authorized_users.download().await?; + let guild_channel_json = guild_channels.download().await?; + let cleard_ids_json = cleard_ids.download().await?; + let monitored_guilds_json = monitored_guilds.download().await?; + let vrc_data_json = vrc_data.download().await?; + + msg.edit(ctx, |b| b.content("Converting json to structs")) + .await?; + + let auth_user_vec: Vec = + serde_json::from_str(std::str::from_utf8(&authorized_users_json)?)?; + let guild_channel_vec: Vec = + serde_json::from_str(std::str::from_utf8(&guild_channel_json)?)?; + let cleard_id_vec: Vec = + serde_json::from_str(std::str::from_utf8(&cleard_ids_json)?)?; + let monitored_guilds_vec: Vec = + serde_json::from_str(std::str::from_utf8(&monitored_guilds_json)?)?; + let vrc_data_vec: Vec = serde_json::from_str(std::str::from_utf8(&vrc_data_json)?)?; + + msg.edit(ctx, |b| b.content("Preparing authorized_users data")) + .await?; + + for authed_user in auth_user_vec { + pipe.cmd("SADD").arg(&[ + format!("authed-server-users:{}", authed_user.guild_id), + format!("{}", authed_user.user_id), + ]); + } + + msg.edit(ctx, |b| b.content("Preparing guild_channels data")) + .await?; + + for guild_settings in guild_channel_vec { + let formatted = GuildSettings { + channel_id: format!("{}", guild_settings.channel_id), + kick: match guild_settings.kick_active { + 1 => true, + 0 => false, + _ => false, + }, + server_name: guild_settings.Name, + }; + + let json = serde_json::to_string(&formatted).unwrap(); + pipe.cmd("JSON.SET").arg(&[ + format!("guild-settings:{}", guild_settings.guild), + "$".to_string(), + json, + ]); + } + + msg.edit(ctx, |b| b.content("Preparing cleard_ids data")) + .await?; + + for cleared_id in cleard_id_vec { + let formatted: ClearedUser = ClearedUser { + user_id: format!("{}", cleared_id.Discord_ID), + username: cleared_id.Name, + where_found: cleared_id.Where_found, + reason: cleared_id.Cleared_Reason, + }; + + let json = serde_json::to_string(&formatted).unwrap(); + pipe.cmd("JSON.SET").arg(&[ + format!("cleared-user:{}", cleared_id.Discord_ID), + "$".to_string(), + json, + ]); + } + + msg.edit(ctx, |b| b.content("Preparing monitored_guilds data")) + .await?; + + for guild in monitored_guilds_vec { + let formatted: MonitoredGuildInfo = MonitoredGuildInfo { + guild_name: guild.Guild_Name.to_string(), + guild_id: format!("{}", guild.Guild_ID), + invite_link: match guild.Invite_link { + None => "N/A".to_string(), + Some(link) => link.to_string(), + }, + updated: match guild.Updated { + None => "Never".to_string(), + Some(date) => date.to_string(), + }, + status: match guild.DMCA_Takedown_Nuked { + None => "Unknown".to_string(), + Some(status) => status.to_string(), + }, + }; + + let json = serde_json::to_string(&formatted).unwrap(); + pipe.cmd("JSON.SET").arg(&[ + format!("monitored-guild:{}", guild.Guild_ID), + "$".to_string(), + json, + ]); + } + + msg.edit(ctx, |b| b.content("Preparing vrc_data")).await?; + + let mut parsed_ids: HashSet = HashSet::new(); + + for user_data in vrc_data_vec { + match parsed_ids.contains(&format!("{}", user_data.discord_id)) { + false => { + let mut new_user = UserInfo { + vrc_id: user_data.vrc_id, + username: user_data.name, + discord_id: Some(format!("{}", user_data.discord_id)), + offences: Vec::new(), + }; + + let offense = vec![Offense { + guild_id: match user_data.guild_id { + None => "N/A".to_string(), + Some(gid) => format!("{}", gid), + }, + reason: user_data.reason, + image: user_data.image, + extra: user_data.extra, + }]; + new_user.offences = offense; + + let json = serde_json::to_string(&new_user).unwrap(); + pipe.cmd("JSON.SET").arg(&[ + format!("user:{}", user_data.discord_id), + "$".to_string(), + json, + ]); + + parsed_ids.insert(format!("{}", user_data.discord_id)); + } + true => { + let offense = Offense { + guild_id: match user_data.guild_id { + None => "N/A".to_string(), + Some(gid) => format!("{}", gid), + }, + reason: user_data.reason, + image: user_data.image, + extra: user_data.extra, + }; + + let json = serde_json::to_string(&offense).unwrap(); + pipe.cmd("JSON.ARRAPPEND") + .arg(format!("user:{}", user_data.discord_id)) + .arg("$.offences".to_string()) + .arg(json); + } + } + } + + msg.edit(ctx, |b| b.content("Uploading data to DB")).await?; + + pipe.query_async(&mut con).await?; + + msg.edit(ctx, |b| b.content("All done!")).await?; + + Ok(()) +} \ No newline at end of file diff --git a/src/commands/admin.rs b/src/commands/admin.rs new file mode 100644 index 0000000..b0c57e0 --- /dev/null +++ b/src/commands/admin.rs @@ -0,0 +1,516 @@ +use clap::Parser; +use core::time; +use poise::serenity_prelude::Attachment; +use poise::serenity_prelude::{self as serenity, Activity, Member, OnlineStatus}; +use poise::serenity_prelude::{ChannelId, Colour}; +use rand::seq::SliceRandom; +use rand::Rng; +use rusted_fbt_lib::checks::guild_auth_check; +use rusted_fbt_lib::structs::GuildSettings; +use rusted_fbt_lib::utils::{auth, open_redis_connection, set_guild_settings}; +use rusted_fbt_lib::{ + args::Args, + checks::bot_admin_check, + types::{Context, Error}, + utils::{inc_execution_count, verbose_mode}, +}; +use std::collections::HashSet; +use std::ops::Add; +use std::process::exit; +use tokio::time::sleep; +use tracing::instrument; +use tracing::{event, Level}; + +#[instrument(skip(ctx))] +#[poise::command( + slash_command, + category = "Admin", + check = "bot_admin_check", + hide_in_help +)] +/// Sends message to specified user ID +pub async fn botmsg( + ctx: Context<'_>, + #[description = "User ID"] user: serenity::User, + #[description = "Message"] msg: String, +) -> Result<(), Error> { + ctx.defer().await?; + + user.direct_message(ctx, |f| f.content(&msg)).await?; + + ctx.say(format!("Sent message to: {}", user.name)).await?; + ctx.say(format!("Message: {}", &msg)).await?; + + Ok(()) +} + +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "Admin", + required_permissions = "BAN_MEMBERS", + required_bot_permissions = "BAN_MEMBERS", + guild_only, + ephemeral +)] +/// Explains how to ban a list of users with `ban +pub async fn ban_help(ctx: Context<'_>) -> Result<(), Error> { + let args = Args::parse(); + + ctx.say("To ban a single user the easiest way is with the slash command `/ban ban_user @USER/ID` since you don't need to provide message deletion numbers or a reason.").await?; + ctx.say(format!("In order to ban multiple people please use this command as a a prefix command like so:\n```\n{}ban ban_user \"Reason in qutoation marks\" 0(A number from 0 to 7, how many days worth of messages you want to delet) userID1 userID2 userID3\n```", args.prefix)).await?; + + Ok(()) +} + +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "Admin", + required_permissions = "BAN_MEMBERS", + required_bot_permissions = "BAN_MEMBERS", + guild_only, + ephemeral +)] +/// Explains how to ban a list of users with `ban +pub async fn ban_user( + ctx: Context<'_>, + #[description = "Ban reason"] reason: Option, + #[description = "How many days of messages to purge (Max of 7)"] + #[min = 0] + #[max = 7] + dmd: Option, + #[description = "Member(s) to ban"] members: Vec, +) -> Result<(), Error> { + let delete_count: u8 = dmd.map_or(0u8, |num| { + let num_check = num; + if num_check.le(&7u8) { + num + } else { + 7u8 + } + }); + + let reason_sanitised = reason.map_or_else(|| "Banned via bot ban command".to_string(), |r| r); + + // TODO: Change to your own emojis! + + // Mojo test server emoji version + // let phrase_list = vec!["has been ejected", "banned quietly", "terminated", "thrown out", ""]; + + // FBT Emoji version + let phrase_list = ["has been ejected", "banned quietly", "terminated", "thrown out", ""]; + + match ctx.guild() { + Some(guild) => match members.len() { + 0 => { + let args = Args::parse(); + + ctx.say("You must provide at least one user to ban!") + .await?; + ctx.say(format!("In order to ban multiple people please use this command as a a prefix command like so:\n```\n{}ban userID1 userID2 userID3\n```", args.prefix)).await?; + } + 1 => { + let member = guild.member(ctx, members[0].user.id).await?; + if let Err(error) = member + .ban_with_reason(ctx, delete_count, reason_sanitised.clone()) + .await + { + if verbose_mode() { + ctx.say(format!( + "Failed to ban {} because of {:?}", + member.display_name(), + error + )) + .await?; + } else { + ctx.say(format!("Failed to ban {}", member.display_name())) + .await?; + } + // 0u8 + } else { + let phrase = phrase_list + .choose(&mut rand::thread_rng()) + .expect("Unable to get meme phrase for ban"); + ctx.say(format!("{} has been {phrase}", member.display_name())) + .await?; + // 0u8 + }; + } + _ => { + for member in members { + if let Err(error) = member + .ban_with_reason(ctx, delete_count, reason_sanitised.clone()) + .await + { + if verbose_mode() { + ctx.say(format!( + "Failed to ban {} because of {:?}", + member.display_name(), + error + )) + .await?; + } else { + ctx.say(format!("Failed to ban {}", member.display_name())) + .await?; + } + // 0u8 + } else { + let phrase = phrase_list + .choose(&mut rand::thread_rng()) + .expect("Unable to get meme phrase for ban"); + ctx.say(format!("{} has been {phrase}", member.display_name())) + .await?; + // 0u8 + }; + } + } + }, + None => { + ctx.say("This must be ran from inside a guild").await?; + } + } + Ok(()) +} + +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "Admin", + required_permissions = "BAN_MEMBERS", + required_bot_permissions = "BAN_MEMBERS", + guild_only, + subcommands("ban_help", "ban_user") +)] +/// Ban a member or list of members by ID or Mention +pub async fn ban(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("Run `/ban ban_help` to learn how to use this command and then use ") + .await?; + + Ok(()) +} + +// TODO: Change to your own emojis! +#[instrument(skip(ctx))] +#[poise::command(prefix_command, slash_command, category = "Admin", owners_only)] +/// Literally just shoot the bot! +pub async fn shutdown(ctx: Context<'_>) -> Result<(), Error> { + let pewpew = ctx + .say("<:GunPoint:908506214915276851> <:FBT:795660945627676712>") + .await?; + sleep(time::Duration::from_secs(1)).await; + pewpew + .edit(ctx, |b| { + b.content("<:GunPoint:908506214915276851> 💥 <:FBT:795660945627676712>") + }) + .await?; + sleep(time::Duration::from_secs(1)).await; + pewpew + .edit(ctx, |b| { + b.content("<:GunPoint:908506214915276851> <:FBT:795660945627676712>") + }) + .await?; + sleep(time::Duration::from_secs(1)).await; + pewpew + .edit(ctx, |b| { + b.content("<:GunPoint:908506214915276851> 🩸 <:FBT:795660945627676712> 🩸") + }) + .await?; + sleep(time::Duration::from_secs(1)).await; + ctx.say("Exiting now!").await?; + + #[cfg(feature = "database")] + inc_execution_count().await?; + + let activity = Activity::playing("Sleeping"); + let status = OnlineStatus::Offline; + + ctx.serenity_context() + .set_presence(Some(activity), status) + .await; + + exit(0) +} + +#[cfg(feature = "database")] +/// Authorize someone in this guild +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "Admin", + check = "guild_auth_check", + guild_only +)] +pub async fn authorize( + ctx: Context<'_>, + #[description = "User to authorise in this server"] user: Member, +) -> Result<(), Error> { + ctx.defer_or_broadcast().await?; + + let uid = format!("{}", user.user.id.as_u64()); + + let mut con = open_redis_connection().await?; + + // * json format: {users:[ID1, ID2, IDect]} + let key_list: Option> = redis::cmd("SMEMBERS") + .arg(format!( + "authed-server-users:{}", + ctx.guild_id().unwrap().as_u64() + )) + .clone() + .query_async(&mut con) + .await?; + + if let Some(list) = key_list { + if list.contains(&uid) { + ctx.say("User already authorised in this server!").await?; + } else { + match auth(ctx, &mut con, uid).await { + Ok(()) => { + ctx.say(format!( + "{} is now authorized to use commands in this server!", + user.display_name() + )) + .await?; + } + Err(_error) if !verbose_mode() => { + ctx.say(format!("Failed to auth {}!", user.display_name())) + .await?; + } + Err(error) => { + ctx.say(format!( + "Failed to auth {}! Caused by {:?}", + user.display_name(), + error + )) + .await?; + } + } + } + } else { + auth(ctx, &mut con, uid).await?; + + ctx.say(format!( + "{} is now authorized to use commands in this server!", + user.display_name() + )) + .await?; + } + + Ok(()) +} + +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "Admin", + check = "bot_admin_check" +)] +/// Send annoucement to any server that has been setup +pub async fn announcement( + ctx: Context<'_>, + #[description = "Title of announcement embed"] title: String, + #[description = "Message to send to all servers (As a .txt file!)"] message_file: Attachment, +) -> Result<(), Error> { + ctx.defer_or_broadcast().await?; + + let message_content = message_file.download().await?; + let message = std::str::from_utf8(&message_content)?; + + let mut con = open_redis_connection().await?; + + let key_list: Vec = redis::cmd("KEYS") + .arg("guild-settings:*") + .clone() + .query_async(&mut con) + .await?; + + let mut key_pipe = redis::pipe(); + + for key in key_list { + key_pipe.cmd("JSON.GET").arg(key); + } + + let setting_entries: Vec = key_pipe.atomic().query_async(&mut con).await?; + + let mut guild_settings_collection = Vec::new(); + for settings in setting_entries { + let gs: GuildSettings = serde_json::from_str(&settings)?; + + guild_settings_collection.push(gs); + } + + // TODO: Change to custom announcement message! + + let mut count: u64 = 0; + for guild in guild_settings_collection.clone() { + let colour = &mut rand::thread_rng().gen_range(0..10_000_000); + match ChannelId(guild.channel_id.parse::()?).send_message(ctx, |f| { + f.embed(|e| { + e.title(format!("New announcement from FBT Security: {}", title.clone())) + .description(message) + .color(Colour::new(*colour)) + .author(|a| { + a.icon_url("https://cdn.discordapp.com/avatars/743269383438073856/959512463b1559b14818590d8c8a9d2a.webp?size=4096") + .name("FBT Security") + }) + .thumbnail("https://media.giphy.com/media/U4sfHXAALLYBQzPcWk/giphy.gif") + }) + }).await { + Err(e)=>{ + event!( + Level::INFO, + "Failed to send announcement to a server because of" = ?e + ); + }, + Ok(msg) => { + count = count.add(1); + println!("Sent to: {}", msg.link()); + }, + }; + } + + ctx.say(format!( + "Sent annoucement to {}/{} servers!", + count, + guild_settings_collection.len() + )) + .await?; + + Ok(()) +} + +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "Admin", + required_permissions = "ADMINISTRATOR", + guild_only +)] +/// Request an FBT staff member to come and auth your server +pub async fn request_setup( + ctx: Context<'_>, + #[description = "Do you want to kick accounts that are under 90 days old"] alt_protection: bool, +) -> Result<(), Error> { + ctx.defer().await?; + let link = ChannelId(*ctx.channel_id().as_u64()) + .create_invite(ctx, |f| f.temporary(false).max_age(0).unique(false)) + .await?; + + + // TODO: this channel is where the bot alterts you when a server is requesting use of the bot's moderation stuff + ChannelId(953_435_498_318_286_898).send_message(ctx, |f| { + f.content(format!("{0} is requesting authentication! {1}\n They requested for alt protection to be: `{alt_protection}`", ctx.guild().unwrap().name, link.url())) + }).await?; + + ctx.send(|b| b.content("Request sent, sit tight!\nOnce an administrator joins make sure to give them permissions to acess the channel so they can set it up!").ephemeral(true)).await?; + + Ok(()) +} + +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command( + slash_command, + category = "Admin", + check = "guild_auth_check", + guild_only +)] +/// Setup your server's settings +pub async fn setup( + ctx: Context<'_>, + #[description = "Do you want to kick accounts that are under 90 days old when they join"] + alt_protection: bool, +) -> Result<(), Error> { + let mut con = open_redis_connection().await?; + + let guild_settings_json_in: Option = redis::cmd("JSON.GET") + .arg(format!( + "guild-settings:{}", + ctx.guild_id().unwrap().as_u64() + )) + .clone() + .query_async(&mut con) + .await?; + + let ch_id = format!("{}", ctx.channel_id().as_u64()); + let g_name = ctx + .partial_guild() + .await + .expect("Unable to get Guild info") + .name; + + if let Some(json_in) = guild_settings_json_in { + let mut settings: GuildSettings = serde_json::from_str(&json_in)?; + settings.channel_id = ch_id.clone(); + settings.kick = alt_protection; + settings.server_name = g_name; + + set_guild_settings(ctx, &mut con, settings).await?; + ctx.say(format!("Settings have been updated for your server!\nChannel for kick messages and bot announcements: <#{0}>.\nAlt protection: {alt_protection:?}.", ch_id.clone())).await?; + } else { + let settings = GuildSettings { + channel_id: ch_id.clone(), + kick: alt_protection, + server_name: g_name, + }; + + set_guild_settings(ctx, &mut con, settings).await?; + ctx.say(format!("Settings have been created for your server!\nChannel for kick messages and bot announcements: <#{0}>.\nAlt protection: {alt_protection:?}.", ch_id.clone())).await?; + } + + Ok(()) +} + +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "Admin", + check = "guild_auth_check", + guild_only +)] +/// Set your server's alt protection policy +pub async fn toggle_kick(ctx: Context<'_>) -> Result<(), Error> { + let mut con = open_redis_connection().await?; + + let guild_settings_json_in: Option = redis::cmd("JSON.GET") + .arg(format!( + "guild-settings:{}", + ctx.guild_id().unwrap().as_u64() + )) + .clone() + .query_async(&mut con) + .await?; + + match guild_settings_json_in { + // Update settings + Some(json_in) => { + let mut settings: GuildSettings = serde_json::from_str(&json_in)?; + settings.kick = !settings.kick; + + set_guild_settings(ctx, &mut con, settings.clone()).await?; + ctx.say(format!( + "Settings have been updated for your server!\nAlt protection: {:?}.", + settings.kick + )) + .await?; + } + // TODO: change to custom message + // This should not be able to trigger because of the auth check but better safe than sorry + None => { + ctx.say("Your server has not been setup by a bot admin yet! Please context a bot admin or azuki to get authorised.").await?; + } + } + + Ok(()) +} diff --git a/src/commands/database.rs b/src/commands/database.rs new file mode 100644 index 0000000..4732e4f --- /dev/null +++ b/src/commands/database.rs @@ -0,0 +1,1216 @@ +use meilisearch_sdk::client::Client; +use merge::Merge; +use poise::serenity_prelude::Attachment; +use poise::serenity_prelude::{colours, AttachmentType, Colour, Member, UserId}; +use rand::Rng; +use rusted_fbt_lib::checks::guild_auth_check; +use rusted_fbt_lib::structs::{BlacklistHit, CsvEntry, Offense, UserInfo}; +use rusted_fbt_lib::utils::{open_redis_connection, verbose_mode}; +use rusted_fbt_lib::vars::BlacklistOutput; +use rusted_fbt_lib::vars::{BOT_ADMINS, BOT_IDS, MEILISEARCH_API_KEY, MEILISEARCH_HOST}; +use rusted_fbt_lib::{ + checks::bot_admin_check, + types::{Context, Error}, +}; +use std::collections::HashSet; +use std::ops::{Add, Mul}; +use tracing::instrument; +use tracing::{event, Level}; + +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "DB", + member_cooldown = 1, + required_permissions = "BAN_MEMBERS", + check = "guild_auth_check", + guild_only +)] +/// Add an ID to the bot +pub async fn add( + ctx: Context<'_>, + #[description = "Person to add to DB. Must be a user ID."] id: String, + #[description = "Reason for being added to the DB."] reason: String, + #[description = "Server ID this took place in. (Leave blank to use your own server ID)"] + guild_id: Option, + #[description = "ID or URL to VRChat account."] vrc_id: Option, + #[description = "Link to image or google drive folder of images."] image: Option, + #[description = "Anything extra you want to add."] extra: Option, +) -> Result<(), Error> { + ctx.defer().await?; + + let mut is_not_user = false; + + let uid = id.trim().parse::().map_or_else( + |_| { + is_not_user = true; + 0 + }, + |i| i, + ); + + if is_not_user { + ctx.send(|b| { + b.content("Make sure you supplied a user ID") + .ephemeral(true) + }) + .await?; + } else { + let gid: String = guild_id.map_or_else(|| ctx.guild_id().unwrap().to_string(), |url| url); + + let mut con = open_redis_connection().await?; + + let result: Option = redis::cmd("JSON.GET") + .arg(format!("user:{uid}")) + .clone() + .query_async(&mut con) + .await?; + + let uname = (UserId::from(uid).to_user(ctx).await) + .map_or_else(|_| "NoUsernameFoundInDB".to_string(), |u| u.tag()); + + match result { + None => { + let mut new: UserInfo = UserInfo { + vrc_id: None, + username: Some(uname), + discord_id: Some(format!("{uid}")), + offences: Vec::new(), + }; + + let mut new_offense = Offense { + guild_id: gid, + reason: reason.clone(), + image: image.clone().or_else(|| Some("N/A".to_string())), + extra: extra.clone().or_else(|| Some("N/A".to_string())), + }; + + match vrc_id { + None => { + new.vrc_id = Some("N/A".to_string()); + } + Some(url) => { + new.vrc_id = Some(url); + } + } + + match image { + None => { + new_offense.image = Some("N/A".to_string()); + } + Some(url) => { + new_offense.image = Some(url); + } + } + + match extra { + None => { + new_offense.extra = Some("N/A".to_string()); + } + Some(url) => { + new_offense.extra = Some(url); + } + } + + new.offences.push(new_offense); + + let json_user = serde_json::to_string(&new).unwrap(); + + redis::cmd("JSON.SET") + .arg(format!("user:{uid}")) + .arg("$".to_string()) + .arg(json_user) + .clone() + .query_async(&mut con) + .await?; + } + Some(_) => { + let mut new_offense = Offense { + guild_id: gid, + reason: reason.clone(), + image: image.clone().or_else(|| Some("N/A".to_string())), + extra: extra.clone().or_else(|| Some("N/A".to_string())), + }; + + match image { + None => { + new_offense.image = Some("N/A".to_string()); + } + Some(url) => { + new_offense.image = Some(url); + } + } + + match extra { + None => { + new_offense.extra = Some("N/A".to_string()); + } + Some(url) => { + new_offense.extra = Some(url); + } + } + + let json_offense = serde_json::to_string(&new_offense).unwrap(); + + redis::cmd("JSON.ARRAPPEND") + .arg(format!("user:{uid}")) + .arg("$.offences".to_string()) + .arg(json_offense) + .clone() + .query_async(&mut con) + .await?; + } + } + + ctx.say(format!("<@{uid}> added into DB!\nReason: {reason}")) + .await?; + } + + Ok(()) +} + +// TODO: Change into sub command when we add vrc DB back +/// Search DB for yourself or with ID +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "DB", + member_cooldown = 5, + check = "guild_auth_check", + guild_only +)] +pub async fn search( + ctx: Context<'_>, + #[description = "Member to search for. This must be a user ID."] user_id: String, +) -> Result<(), Error> { + use pastemyst::paste::*; + use pastemyst::str; + use poise::serenity_prelude::User; + + ctx.defer().await?; + + // let u = id.user.clone(); + + let uid = user_id.trim().parse::()?; + + let u_opt: Option = match UserId::from(uid).to_user(ctx).await { + Ok(user) => Some(user), + Err(error) => { + if verbose_mode() { + ctx.say(format!( + "ID must be a user ID, make sure you coppied the right one! Error: {:?}", + error + )) + .await?; + } else { + ctx.say("ID must be a user ID, make sure you coppied the right one!") + .await?; + } + + None + } + }; + + if let Some(u) = u_opt { + let result = check_username_against_db(uid).await?; + + match result { + None => { + ctx.say(format!("No result found for {uid}.")).await?; + } + Some(hit) => { + let hit_string: String = hit; + let user: UserInfo = serde_json::from_str(hit_string.as_str())?; + + let username = user.username.unwrap_or_else(|| u.tag()); + let id = user.discord_id.unwrap_or(format!("{}", uid.clone())); + let vrc = user.vrc_id.unwrap_or_else(|| "N/A".to_string()); + + let field_count = user.offences.len() as u64; + + if field_count.mul(7).add(4).ge(&25_u64) { + let mut offences = String::new(); + + let mut i: u64 = 0; + for hit in user.offences.clone() { + i = i.add(1); + + let image = hit.image.unwrap_or_else(|| "N/A".to_string()); + let extra = hit.extra.unwrap_or_else(|| "N/A".to_string()); + offences.push_str(format!("\n\nOffence {i}:\n Guild ID: {0}\n Reason: {1}\n Image(s): {image}\n Extra info: {extra}", hit.guild_id, hit.reason).as_str()); + } + + let msg_start = format!( + "Result found!\nUser has {} hit(s).\nUsername logged in DB: {}\nCurrent username: {}\nUser ID: {}\nVRChat ID: {}", + user.offences.clone().len() as u64, + username, + u.tag(), + id, + vrc + ); + + if (offences.chars().count() + msg_start.chars().count()) < 2000 { + // for format! merges msg_start and offences + ctx.say(format!("{msg_start}```{offences}```")).await?; + } else { + let txt_contents = format!("{msg_start}\n\n{offences}"); + ctx.send(|f| { + f.content("Result found!\nThis user has so many hits that we can't display them all in Discord.\nBellow is a `.txt` file with all of their offences.") + .ephemeral(false) + .attachment(AttachmentType::Bytes { + data: std::borrow::Cow::Borrowed(txt_contents.as_bytes()), + filename: format!("{id}.txt"), + }) + }) + .await?; + } + } else { + let mut fields: Vec<(String, String, bool)> = vec![ + ("Username logged in DB:".to_string(), username.clone(), true), + ("Current username:".to_string(), u.tag(), true), + ("User ID:".to_string(), id.clone(), true), + ("VRChat ID:".to_string(), vrc.clone(), true), + ]; + + let mut i: u64 = 0; + + // let mut buffer_len: usize = 0; + + // for offense in user.offences.clone() { + // buffer_len += offense.reason.chars().into_iter().count(); + // } + + for offense in user.offences.clone() { + i = i.add(1); + fields.push(("Offense:".to_string(), format!("#{i}"), false)); + + let image = offense.image.unwrap_or_else(|| "N/A".to_string()); + let extra = offense.extra.unwrap_or_else(|| "N/A".to_string()); + + fields.push(("Guild ID:".to_string(), offense.guild_id, true)); + fields.push(( + "Reason:".to_string(), + { + if offense.reason.chars().into_iter().count() > 1000 { + let pasties: Vec = vec![PastyObject { + _id: str!(""), + language: str!(pastemyst::data::language::MARKDOWN), + title: format!("{}'s long offence", username.clone()), + code: offense.reason, + }]; + + let data: CreateObject = CreateObject { + title: format!("{}'s long offence", username.clone()), + expiresIn: String::from("1d"), + isPrivate: false, + isPublic: false, + tags: String::from(""), + pasties: pasties, + }; + + let paste = create_paste(data)?; + + format!("https://paste.myst.rs/{}", paste._id) + } else { + offense.reason + } + }, + true, + )); + fields.push(("Image(s):".to_string(), image, true)); + fields.push(("Extra info:".to_string(), extra, true)); + } + + let colour = &mut rand::thread_rng().gen_range(0..10_000_000); + + let msg = ctx.send(|b| { + b.embed(|e| { + e.title("Result found!") + .description(format!( + "User has {} hit(s).", + user.offences.clone().len() as u64 + )) + .fields(fields) + .color(Colour::new(*colour)) + .thumbnail(u.avatar_url().unwrap_or_else(|| "https://discord.com/assets/1f0bfc0865d324c2587920a7d80c609b.png".to_string())) + }) + }) + .await; + if msg.is_err() { + let msg_start = format!( + "Result found!\nUser has {} hit(s).\nUsername logged in DB: {}\nCurrent username: {}\nUser ID: {}\nVRChat ID: {}", + user.offences.clone().len() as u64, + username, + u.tag(), + id, + vrc + ); + + let mut offences = String::new(); + + let mut i: u64 = 0; + for hit in user.offences.clone() { + i = i.add(1); + + let image = hit.image.unwrap_or_else(|| "N/A".to_string()); + let extra = hit.extra.unwrap_or_else(|| "N/A".to_string()); + offences.push_str(format!("\n\nOffence {i}:\n Guild ID: {0}\n Reason: {1}\n Image(s): {image}\n Extra info: {extra}", hit.guild_id, hit.reason).as_str()); + } + + let txt_contents = format!("{msg_start}\n\n{offences}"); + ctx.send(|f| { + f.content("Result found!\nThere was some kinda of error sending the fancy version, most likely one of the field (for example the images) was too long.\nBellow is a `.txt` file with all of their offences formated as nicely as I could make it.") + .ephemeral(false) + .attachment(AttachmentType::Bytes { + data: std::borrow::Cow::Borrowed(txt_contents.as_bytes()), + filename: format!("{username}_{id}.txt"), + }) + }) + .await?; + } + } + } + } + } + + let unix_timecode = rusted_fbt_lib::utils::snowflake_to_unix(u128::from(ctx.author().id.0)); + + #[allow(clippy::cast_possible_truncation)] + // this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN + let date_time_stamp = chrono::NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0) + .unwrap_or(chrono::NaiveDateTime::MIN); + + let age = chrono::Utc::now() + .naive_utc() + .signed_duration_since(date_time_stamp) + .num_days(); + + let is_user_in_db: Option = check_username_against_db(ctx.author().id.0).await.unwrap(); + + + // TODO: set your own channel ID! + // log user name, id, guild name, id and url to channel + poise::serenity_prelude::ChannelId(0000000000000000000) + .send_message(ctx, |f| { + f.embed(|e| { + e.title("DB search info") + .field("Username", ctx.author().name.clone(), true) + .field("User ID", ctx.author().id.0.to_string(), true) + .field("User Account age (days)", age, true) + .field("Source Server Name", ctx.guild().unwrap().name, true) + .field( + "Source Server ID", + ctx.guild().unwrap().id.0.to_string(), + true, + ) + .field("User ID Provided", user_id, true) + .field( + "Is user in DB (The person who ran command)", + format!("{}", is_user_in_db.is_some()), + false, + ) + }) + }) + .await?; + + Ok(()) +} + +pub async fn check_username_against_db(uid: u64) -> anyhow::Result> { + let mut con = open_redis_connection().await?; + let result: Option = redis::cmd("JSON.GET") + .arg(format!("user:{uid}")) + .clone() + .query_async(&mut con) + .await?; + Ok(result) +} + +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "DB", + member_cooldown = 5, + check = "guild_auth_check", + guild_only +)] +pub async fn remove_guild( + ctx: Context<'_>, + #[description = "ID of guild to remove from the DB."] guild_id: String, +) -> Result<(), Error> { + ctx.defer().await?; + + let uid = guild_id.trim().to_string(); + + let mut con = open_redis_connection().await?; + + // Create redis pipeline + let mut pipe = redis::pipe(); + + // Get all user keys + let key_list: Vec = redis::cmd("KEYS") + .arg("user:*") + .clone() + .query_async(&mut con) + .await?; + + // I wounder if rayon can be used here? + for key in key_list { + pipe.cmd("JSON.GET").arg(key); + } + + // Should only fail if the DB is empty, if the DB is empty we have worse problems.. + let blacklist_entries_json: Vec = pipe.atomic().query_async(&mut con).await?; + + // Vec os user info structs + let mut blacklist_entries: Vec = Vec::new(); + + // Convert json to structs + for entry in blacklist_entries_json { + let entry: UserInfo = serde_json::from_str(entry.as_str())?; + blacklist_entries.push(entry); + } + + // Vec of user info structs that have offences in the guild we want to remove + let mut guild_offences: Vec = Vec::new(); + + // Get all users that have offences in the guild we want to remove + for entry in blacklist_entries { + let entry_clone = entry.clone(); + for offence in entry.offences { + if offence.guild_id.eq(&uid) { + guild_offences.push(entry_clone); + break; + } + } + } + + // Create redis pipeline + let mut pipe2 = redis::pipe(); + + // Remove the guild from all users that have offences in it + for entry in guild_offences.clone() { + let mut offences: Vec = Vec::new(); + + for offence in entry.clone().offences { + if !offence.guild_id.eq(&uid) { + offences.push(offence); + } + } + + let mut user = entry.clone(); + user.offences = offences; + + let json_user = serde_json::to_string(&user)?; + + pipe2 + .cmd("JSON.SET") + .arg(format!("user:{}", user.discord_id.unwrap())) + .arg("$".to_string()) + .arg(json_user) + .clone() + .query_async(&mut con) + .await?; + } + + // Edit all entries at once + pipe2.atomic().query_async(&mut con).await?; + + // Respond to the user with the amount of users that had offences in the guild we removed + ctx.say(format!( + "Removed guild {} from {} users.", + guild_id, + guild_offences.len() + )) + .await?; + + Ok(()) +} + +/// Update the search engine entries +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command(slash_command, category = "DB", owners_only)] +pub async fn update_search_engine(ctx: Context<'_>) -> Result<(), Error> { + ctx.defer_ephemeral().await?; + + // Create a client (without sending any request so that can't fail) + let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + + // connect to index "entries" + let entries = client.index("entries"); + + let msg = ctx + .send(|b| b.content("Connected to search engine")) + .await?; + + // connect to redis and pull all blacklist entries into a vec + + let mut con = open_redis_connection().await?; + let mut pipe = redis::pipe(); + + msg.edit(ctx, |b| b.content("Connected to DB")).await?; + + // Get all user keys + let key_list: Vec = redis::cmd("KEYS") + .arg("user:*") + .clone() + .query_async(&mut con) + .await?; + + msg.edit(ctx, |b| b.content("Fetching all DB entries")) + .await?; + + // I wounder if rayon can be used here? + for key in key_list { + pipe.cmd("JSON.GET").arg(key); + } + + // Should only fail if the DB is empty, if the DB is empty we have larger problems.. + let blacklist_entries: Vec = pipe.atomic().query_async(&mut con).await?; + + let mut black_listed_users = Vec::new(); + for user in blacklist_entries { + let user: UserInfo = serde_json::from_str(&user).expect("It was 701"); + + black_listed_users.push(user); + } + + msg.edit(ctx, |b| b.content("Updating search engine")) + .await?; + + // push entries to meidisearch + match entries + .add_documents(black_listed_users.as_slice(), Some("discord_id")) + .await + { + Ok(_) => {} + Err(err) => { + event!( + Level::INFO, + suggestion = "This error can probably be ignored?", + error = ?err + ); + } + } + + msg.edit(ctx, |b| { + b.content("All DB entries sent to search engine, any changes or new entries should be avaliable in a couple minutes.") + }).await?; + + event!(Level::INFO, "Search engine was updated."); + + Ok(()) +} + +/// Check your entire server against the database. Now with output options! +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "DB", + guild_cooldown = 30, + check = "guild_auth_check", + guild_only +)] +pub async fn footprint_lookup( + ctx: Context<'_>, + #[description = "What format do you want the results as?"] output_format: Option< + BlacklistOutput, + >, +) -> Result<(), Error> { + ctx.defer().await?; + + let mut con = open_redis_connection().await?; + + // let key_list: Vec = redis::cmd("KEYS") + // .arg("user:*") + // .clone() + // .query_async(&mut con) + // .await?; + + let mut key_pipe = redis::pipe(); + + let mut i = 1; + let mut guild_members: Vec = Vec::new(); + + while i < ctx.guild().unwrap().member_count { + let latest = guild_members.last().map(|u| u.user.id); + + let mut guild_members_temp = ctx + .guild() + .unwrap() + .members(ctx, Some(1000), latest) + .await + .expect("It was 710"); + + i = i.add(1000); + guild_members.append(&mut guild_members_temp); + } + + // let mut blacklist_entries: Vec = Vec::new(); + + guild_members.clone().into_iter().for_each(|key| { + key_pipe + .cmd("JSON.GET") + .arg(format!("user:{}", key.user.id.as_u64())); + }); + + let opt_blacklist_entries: Vec> = + key_pipe.atomic().query_async(&mut con).await?; + + let mut blacklist_entries: Vec = Vec::new(); + + for ent in opt_blacklist_entries { + let _ = ent.map_or((), |thing| { + blacklist_entries.push(thing); + }); + } + + let mut black_listed_users = Vec::new(); + for user in blacklist_entries { + let user: UserInfo = serde_json::from_str(&user)?; + + black_listed_users.push(user); + } + + let mut hit_count: u64 = 0; + + // Setup CSV + let mut blacklist_file = csv::Writer::from_writer(vec![]); + blacklist_file + .write_record([ + "Discord ID", + "Username", + "Guild ID", + "Reason", + "Related image(s)", + "Extra details", + ]) + .expect("Unable to repare CSV"); + + // Setup "Json" + let mut struct_hit_list: Vec = Vec::new(); + + for member in guild_members { + let member_id = format!("{}", member.user.id.as_u64()); + let hit_list: Vec = black_listed_users + .clone() + .into_iter() + .filter(|blu| { + blu.discord_id + .as_ref() + .expect("Missing ID in BL user somehow?") + == &member_id + }) + .collect(); + + // Moves to next loop if empty + if hit_list.is_empty() { + continue; + } + + hit_count += 1; + + // TODO: Add .json and other output options + // TODO: Add compact output format: + // @user1 - 1 + // @user2 - 5 + // @user3 - 2 + + // Write discord specific instances to CSV + for hit in hit_list { + let cloned_hit = hit.clone(); + let username = UserId::from(cloned_hit.discord_id.as_ref().unwrap().parse::()?) + .to_user(ctx) + .await? + .name + .to_string(); + let user_id = cloned_hit + .discord_id + .expect("Missing ID in BL user somehow?") + .to_string(); + + match output_format { + None | Some(BlacklistOutput::Csv) => { + for offense in hit.offences { + blacklist_file.write_record(&[ + format!("'{}'", user_id.clone()), + format!("'{}'", username.clone()), + format!("'{}'", offense.guild_id), + format!("'{}'", offense.reason), + format!( + "'{}'", + offense + .image + .map_or_else(|| "N/A".to_string(), |image| image) + ), + format!( + "'{}'", + offense + .extra + .map_or_else(|| "N/A".to_string(), |extra| extra) + ), + ])?; + } + } + Some( + BlacklistOutput::Json | BlacklistOutput::Chat | BlacklistOutput::CompactChat, + ) => { + for offense in hit.offences { + struct_hit_list.push(BlacklistHit { + user_id: user_id.clone(), + username: username.clone(), + guild_id: offense.guild_id.clone(), + reason: offense.reason.clone(), + image: (offense + .image + .map_or_else(|| "N/A".to_string(), |image| image)) + .to_string(), + extra: (offense + .extra + .map_or_else(|| "N/A".to_string(), |extra| extra)) + .to_string(), + }); + } + } + } + } + } + + if hit_count > 0 { + match output_format { + None | Some(BlacklistOutput::Csv) => { + // ? There has to be a btter way to handle this + let blf_as_bytes = String::from_utf8(blacklist_file.into_inner().unwrap()) + .expect("Unable to convert CSV data to string") + .as_bytes() + .to_owned(); + + ctx.send(|b| { + b.content(format!( + "Your server has {} bad actor(s).\nA .csv file is attaches with all of the results, you can open this in any text editor but is more suited for google sheets/excel.\nrun `/ban ban_help` to find out how to ban multiple users at once.", + hit_count + )) + .attachment(AttachmentType::Bytes { + data: std::borrow::Cow::Borrowed(&blf_as_bytes), + filename: format!("{}_footprint_results.csv", + match ctx.guild() { + Some(g) => g.name, + None => "your".to_string() + }), + }) + }) + .await?; + } + Some(BlacklistOutput::Json) => { + let json_str = serde_json::to_string_pretty(&struct_hit_list)?; + + let blf_as_bytes = json_str.as_bytes().to_owned(); + + ctx.send(|b| { + b.content(format!( + "Your server has {} bad actor(s).\nA .json file is attaches with all of the results.\nYou requested a json file so I'm going to assume you know what you're doing!", + hit_count + )) + .attachment(AttachmentType::Bytes { + data: std::borrow::Cow::Borrowed(&blf_as_bytes), + filename: format!("{}_footprint_results.json", + match ctx.guild() { + Some(g) => g.name, + None => "your".to_string() + }), + }) + }) + .await?; + } + + Some(BlacklistOutput::Chat) => { + let mut message_content: Vec = Vec::new(); + + for hit in struct_hit_list { + let hit_msg = format!( + "<@{}>/{0} was found in your server for the following reason: `{}`\nExtras: {}\nImages: {}", + hit.user_id, + hit.reason, + hit.extra, + hit.image + ); + + let combined_message = message_content.join("\n-----\n"); + + // The +5 is to account for the `\n-----\n` + if (combined_message.chars().count() + hit_msg.chars().count() + 5) < 2000 { + message_content.push(hit_msg); + } else { + match ctx.say(combined_message.clone()).await { + Ok(_) => {} + Err(err) => { + event!( + Level::ERROR, + suggestion = "Message might be too large again?", + error = ?err, + msg_content = combined_message, + ); + panic!(); + } + } + + message_content.clear(); + + if hit_msg.chars().count() > 2000 { + ctx.send(|f| { + f.content(format!( + "<@{}>/{0} was found in your server for the following reason:", + hit.user_id + )) + .ephemeral(false) + .attachment( + AttachmentType::Bytes { + data: std::borrow::Cow::Borrowed(hit_msg.as_bytes()), + filename: format!( + "{}-{}_extra_large_offence.txt", + hit.user_id, hit.guild_id + ), + }, + ) + }) + .await?; + } else { + message_content.push(hit_msg); + } + } + } + + if !message_content.is_empty() { + ctx.say(message_content.join("\n-----\n")).await?; + } + + ctx.say(format!("Found {hit_count} user(s) from the database!\nYou can easily ban them by right clicking on their @")).await?; + } + Some(BlacklistOutput::CompactChat) => { + let mut message_content: Vec = Vec::new(); + + let mut unique_hits: HashSet = HashSet::new(); + + for hit in struct_hit_list { + unique_hits.insert(format!("Found: <@{0}>/{0}", hit.user_id)); + } + + for unique_hit in unique_hits { + if message_content.len() == 20 { + ctx.say(message_content.join("\n")).await?; + message_content.clear(); + } else { + message_content.push(unique_hit); + } + } + + if !message_content.is_empty() { + ctx.say(message_content.join("\n")).await?; + } + + ctx.say(format!("Found {hit_count} user(s) from the database!\nYou can easily ban them by right clicking on their @")).await?; + } + } + } else { + ctx.say("Looks like the server is squeaky clean and free from cringe users, good job!") + .await?; + } + + let unix_timecode = rusted_fbt_lib::utils::snowflake_to_unix(u128::from(ctx.author().id.0)); + + #[allow(clippy::cast_possible_truncation)] + // this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN + let date_time_stamp = chrono::NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0) + .unwrap_or(chrono::NaiveDateTime::MIN); + + let age = chrono::Utc::now() + .naive_utc() + .signed_duration_since(date_time_stamp) + .num_days(); + + let is_user_in_db: Option = + check_username_against_db(ctx.author().id.00).await.unwrap(); + + + // TODO: set your own channel ID! + // log user name, id, guild name, id and url to channel + poise::serenity_prelude::ChannelId(0000000000000000000) + .send_message(ctx, |f| { + f.embed(|e| { + e.title("Entire server footprint request") + .field("Username", ctx.author().name.clone(), true) + .field("User ID", ctx.author().id.0.to_string(), true) + .field("User Account age (days)", age, true) + .field("Source Server Name", ctx.guild().unwrap().name, true) + .field( + "Source Server ID", + ctx.guild().unwrap().id.0.to_string(), + true, + ) + .field( + "Is user in DB (The person who ran command)", + format!("{}", is_user_in_db.is_some()), + false, + ) + }) + }) + .await?; + + Ok(()) +} + +/// Process an entire CSV into the Redis database (V2.0) +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "DB", + required_permissions = "ADMINISTRATOR", + check = "bot_admin_check", + guild_only +)] +pub async fn excel( + ctx: Context<'_>, + #[description = "CSV file to upload"] csv_file: Attachment, + #[description = "ID of server the users are from"] guild_id: String, + #[description = "Reason for being added to the DB"] reason: String, +) -> Result<(), Error> { + ctx.defer_or_broadcast().await?; + + #[allow(clippy::case_sensitive_file_extension_comparisons)] + // Not actually case sensitive thanks to `.to_lowercase()` + if csv_file.url.to_lowercase().ends_with(".csv") { + let embed_message = ctx + .send(|b| { + b.embed(|e| { + e.description("Uploading entried to DB now") + .color(colours::css::WARNING) + .thumbnail("https://media1.giphy.com/media/3o7bu3XilJ5BOiSGic/giphy.gif") + }) + }) + .await?; + + // Download file and convert to csv reader + let response = reqwest::get(csv_file.url).await?; + let csv_content = response.text().await?; + let mut csv_reader = csv::Reader::from_reader(csv_content.as_bytes()); + + // Start timer + let start = tokio::time::Instant::now(); + + let mut records = Vec::new(); + + // Converts each line csv into a CsvEntry object + for record in csv_reader.deserialize() { + let entry: CsvEntry = match record { + Err(err) => { + if verbose_mode() { + ctx.send(|b| { + b.embed(|e| { + e.description(format!("Failed to read a line of the CSV because: {err:?}")) + .color(colours::css::DANGER) + .thumbnail("https://media0.giphy.com/media/TqiwHbFBaZ4ti/giphy.gif?cid=ecf05e476gqwddci6tjtal5ohkp9ql3tq3m3scilolen1jh8&rid=giphy.gif&ct=g") + }) + }).await?; + } else { + ctx.say("Failed to read a line of the CSV, probably a , in someone's name.\nYou can probably ignore this.".to_string()) + .await?; + } + CsvEntry { + AuthorID: "0".to_string(), + Author: "0".to_string(), + } + } + Ok(out) => out, + }; + + if entry.AuthorID == *"0" && entry.Author == *"0" { + continue; + } + + records.push(entry); + } + + #[allow(clippy::iter_with_drain)] + let records_set: HashSet<_> = records.drain(..).collect(); // dedup + records.extend(records_set.into_iter()); + + // End conversion + + let mut con = open_redis_connection().await?; + let mut pipe = redis::pipe(); + + let mut users = Vec::new(); + let mut user_ids = Vec::new(); + + for entry in records { + // TODO: Add more checks once other lists are setup? + if !BOT_IDS.contains(&entry.AuthorID.parse::()?) + || BOT_ADMINS.contains(&entry.AuthorID.parse::()?) + { + let offense = vec![Offense { + guild_id: guild_id.parse().expect("Invalid Guild ID"), + reason: reason.clone(), + image: None, + extra: None, + }]; + + let user = UserInfo { + vrc_id: None, + username: Some(entry.Author), + discord_id: Some(entry.AuthorID), + offences: offense, + }; + + users.push(user.clone()); + user_ids.push(user.discord_id); + } + } + + #[allow(clippy::iter_with_drain)] + let user_ids_set: HashSet<_> = user_ids.drain(..).collect(); // dedup + user_ids.extend(user_ids_set.into_iter()); + + let actual_count = user_ids.len() as u64; + + let mut combined_users = Vec::new(); + + let key_list: Vec = redis::cmd("KEYS") + .arg("user:*") + .clone() + .query_async(&mut con) + .await?; + + let mut key_pipe = redis::pipe(); + + for key in key_list { + key_pipe.cmd("JSON.GET").arg(key); + } + + let old_users: Vec = key_pipe.atomic().query_async(&mut con).await?; + + for old_user in old_users { + let user: UserInfo = serde_json::from_str(&old_user)?; + + users.push(user); + } + + for id in user_ids { + let matches: Vec = users + .clone() + .into_iter() + .filter(|u| u.discord_id == id) + .collect(); + + let mut out_user = UserInfo { + vrc_id: None, + username: None, + discord_id: None, + offences: Vec::new(), + }; + + // Merge every match of this user into out_user + for user in matches { + out_user.merge(user); + } + let out_user_offences_set: HashSet<_> = out_user.offences.drain(..).collect(); // dedup + out_user.offences.extend(out_user_offences_set.into_iter()); + + combined_users.push(out_user); + } + + for user in combined_users { + let json_user = serde_json::to_string(&user).unwrap(); + + // Queue up JSON.SET commands + pipe.cmd("JSON.SET").arg(&[ + format!( + "user:{}", + user.discord_id.expect("Invalid Discord ID in entry") + ), + "$".to_string(), + json_user, + ]); + } + + // Upload all entries at once + pipe.atomic().query_async(&mut con).await?; + + // End timer + let duration = start.elapsed(); + + embed_message.edit(ctx, |b| { + b.embed(|e| { + e.description("Completed upload!".to_string()) + .color(colours::css::POSITIVE) + .thumbnail("https://media3.giphy.com/media/mJHSkWKziszrkcNJPo/giphy.gif?cid=ecf05e47sli83f591onowkgacia9xezewha5pcoj6651yszz&rid=giphy.gif&ct=g") + .fields([ + ("Upload time", format!("{duration:?}"), false), + ("Entries processed", format!("{actual_count}"), false) + ]) + }) + }).await?; + } else { + ctx.say("The file needs to be a `.csv`!").await?; + } + + Ok(()) +} + +/// Print out the meaning behind C/R/L/T in blacklist entries +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command(prefix_command, slash_command, category = "DB")] +pub async fn key(ctx: Context<'_>) -> Result<(), Error> { + ctx.say( + "This is the Key for the Database: C/R/L/T\nC = Crasher, R = Ripper, L = Leaker, T = Toxic", + ) + .await?; + + Ok(()) +} + +/// Add user id to alt protection whitelist +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + check = "bot_admin_check", + category = "DB" +)] +pub async fn whitelist(ctx: Context<'_>, user_id: String) -> Result<(), Error> { + let mut con = open_redis_connection().await?; + + // get kick-whitelist as a hashset and check if user is already in it + let kick_whitelist: HashSet = redis::cmd("SMEMBERS") + .arg("kick-whitelist") + .clone() + .query_async(&mut con) + .await?; + + if kick_whitelist.contains(&user_id) { + ctx.say("User is already in the whitelist!").await?; + } else { + // add user to kick-whitelist + redis::cmd("SADD") + .arg("kick-whitelist") + .arg(user_id) + .query_async(&mut con) + .await?; + + ctx.say("User added to the whitelist!").await?; + } + + Ok(()) +} diff --git a/src/commands/fun.rs b/src/commands/fun.rs new file mode 100644 index 0000000..4b46033 --- /dev/null +++ b/src/commands/fun.rs @@ -0,0 +1,176 @@ +use core::time; + +use poise::serenity_prelude::{self as serenity, AttachmentType}; +use rusted_fbt_lib::enums::WaifuTypes; +use rusted_fbt_lib::types::{Context, Error}; +use serde::{Deserialize, Serialize}; +use tokio::time::sleep; +use tracing::instrument; +use uwuifier::uwuify_str_sse; + +#[instrument(skip(ctx))] +#[poise::command(prefix_command, slash_command, category = "Fun", member_cooldown = 15)] +/// This user is cringe +pub async fn cringe( + ctx: Context<'_>, + #[description = "Optionally call this user cringe"] user: Option, +) -> Result<(), Error> { + let camera_message = ctx.say("").await?; + + sleep(time::Duration::from_secs(1)).await; + + camera_message + .edit(ctx, |b| { + b.content("") + }) + .await?; + + sleep(time::Duration::from_secs(1)).await; + + camera_message + .edit(ctx, |b| b.content("")) + .await?; + + match user { + None => { + ctx.say("Yep, that's going in my cringe compilation") + .await?; + } + Some(user) => { + ctx.send(|m| { + m.content(format!( + "Yep <@{}>, that's going in my cringe compilation", + user.id + )) + }) + .await?; + } + } + + Ok(()) +} + +/// OwOifys your message +#[instrument(skip(ctx))] +#[poise::command(prefix_command, slash_command, category = "Fun")] +pub async fn owo(ctx: Context<'_>, #[description = "Message"] msg: String) -> Result<(), Error> { + ctx.say(uwuify_str_sse(msg.as_str())).await?; + + Ok(()) +} + +/// Replies with pog pog pog and pog frog! +#[instrument(skip(ctx))] +#[poise::command(prefix_command, slash_command, category = "Fun", member_cooldown = 10)] +pub async fn pog(ctx: Context<'_>) -> Result<(), Error> { + ctx.send(|f| { + f.content("Pog pog pog!") + .ephemeral(false) + .attachment(AttachmentType::Bytes { + data: std::borrow::Cow::Borrowed(include_bytes!("../../assets/pog-frog.gif")), + filename: String::from("pog-frog.gif"), + }) + }) + .await?; + + Ok(()) +} + +/// Sends a random waifu (SFW) +#[instrument(skip(ctx))] +#[poise::command(slash_command, category = "Fun", member_cooldown = 5)] +pub async fn waifu( + ctx: Context<'_>, + #[description = "What waifu do you want?"] waifu_type: Option, +) -> Result<(), Error> { + #[derive(Debug, Serialize, Deserialize, Clone)] + struct Waifu { + url: String, + } + + let choice: String = match waifu_type { + None => "waifu".to_string(), + Some(WaifuTypes::Neko) => "neko".to_string(), + Some(WaifuTypes::Megumin) => "megumin".to_string(), + Some(WaifuTypes::Bully) => "bully".to_string(), + Some(WaifuTypes::Cuddle) => "cuddle".to_string(), + Some(WaifuTypes::Cry) => "cry".to_string(), + Some(WaifuTypes::Kiss) => "kiss".to_string(), + Some(WaifuTypes::Lick) => "lick".to_string(), + Some(WaifuTypes::Pat) => "pat".to_string(), + Some(WaifuTypes::Smug) => "smug".to_string(), + Some(WaifuTypes::Bonk) => "bonk".to_string(), + Some(WaifuTypes::Blush) => "blush".to_string(), + Some(WaifuTypes::Smile) => "smile".to_string(), + Some(WaifuTypes::Wave) => "wave".to_string(), + Some(WaifuTypes::Highfive) => "highfive".to_string(), + Some(WaifuTypes::Handhold) => "handhold".to_string(), + Some(WaifuTypes::Nom) => "nom".to_string(), + Some(WaifuTypes::Bite) => "bite".to_string(), + Some(WaifuTypes::Glomp) => "glomp".to_string(), + Some(WaifuTypes::Slap) => "slap".to_string(), + Some(WaifuTypes::Kill) => "kill".to_string(), + Some(WaifuTypes::Happy) => "happy".to_string(), + Some(WaifuTypes::Wink) => "wink".to_string(), + Some(WaifuTypes::Poke) => "poke".to_string(), + Some(WaifuTypes::Dance) => "dance".to_string(), + Some(WaifuTypes::Cringe) => "cringe".to_string(), + }; + + let response = reqwest::get(format!("https://api.waifu.pics/sfw/{choice}")).await?; + let waifu: Waifu = response.json().await?; + + // let waifu: Waifu = serde_json::from_str(json_content.as_str())?; + + ctx.send(|b| b.embed(|e| e.title("Your waifu:").image(waifu.url))) + .await?; + + Ok(()) +} + +/// Replies with pong! +#[instrument(skip(ctx))] +#[poise::command(prefix_command, slash_command, category = "Fun", member_cooldown = 15)] +pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("Pong! 🏓").await?; + + Ok(()) +} + +/// That's toxic +#[instrument(skip(ctx))] +#[poise::command(slash_command, category = "Fun", member_cooldown = 15)] +pub async fn toxic( + ctx: Context<'_>, + #[description = "Optionally call this user cringe"] user: Option, +) -> Result<(), Error> { + const GIF_NAME: &str = "toxic.gif"; + let toxic_gif = include_bytes!("../../assets/toxic.gif"); + + ctx.defer().await?; + + match user { + None => { + ctx.send(|m| { + m.content("That's toxic!") + .attachment(AttachmentType::Bytes { + data: std::borrow::Cow::Borrowed(toxic_gif), // include_bytes! directly embeds the gif file into the executable at compile time. + filename: GIF_NAME.to_string(), + }) + }) + .await?; + } + Some(user) => { + ctx.send(|m| { + m.content(format!("<@{}>, That's toxic!", user.id.as_u64())) + .attachment(AttachmentType::Bytes { + data: std::borrow::Cow::Borrowed(toxic_gif), // include_bytes! directly embeds the gif file into the executable at compile time. + filename: GIF_NAME.to_string(), + }) + }) + .await?; + } + } + + Ok(()) +} diff --git a/src/commands/info.rs b/src/commands/info.rs new file mode 100644 index 0000000..1a1282a --- /dev/null +++ b/src/commands/info.rs @@ -0,0 +1,147 @@ +use poise::serenity_prelude::{ChannelId, Colour}; +use rand::Rng; +use rusted_fbt_lib::{ + types::{Context, Error}, + utils::open_redis_connection, + vars::{FEEDBACK_CHANNEL_ID, HELP_EXTRA_TEXT, VERSION}, +}; +#[cfg(feature = "database")] +use std::time::{SystemTime, UNIX_EPOCH}; +use tracing::instrument; +use tracing::{event, Level}; + +#[instrument(skip(ctx))] +#[poise::command(prefix_command, track_edits, slash_command, category = "Info")] +/// Show this help menu +pub async fn help( + ctx: Context<'_>, + #[description = "Specific command to show help about"] + #[autocomplete = "poise::builtins::autocomplete_command"] + command: Option, +) -> Result<(), Error> { + poise::builtins::help( + ctx, + command.as_deref(), + poise::builtins::HelpConfiguration { + extra_text_at_bottom: HELP_EXTRA_TEXT, + show_context_menu_commands: true, + ..Default::default() + }, + ) + .await?; + Ok(()) +} + +#[cfg(feature = "database")] +#[instrument(skip(ctx))] +#[poise::command(slash_command, category = "Info", member_cooldown = 10, ephemeral)] +/// Provide feedback for the bot team to look at! +pub async fn feedback( + ctx: Context<'_>, + #[description = "Feedback you want to provide"] feedback: String, +) -> Result<(), Error> { + ctx.defer_ephemeral().await?; + let mut con = open_redis_connection().await?; + + redis::cmd("SET") + .arg(format!( + "feedback:{}-{}-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + ctx.author().id.as_u64(), + ctx.author().tag() + )) + .arg(feedback.clone()) + .clone() + .query_async(&mut con) + .await?; + + let colour = &mut rand::thread_rng().gen_range(0..10_000_000); + ChannelId(FEEDBACK_CHANNEL_ID) + .send_message(ctx, |f| { + f.embed(|e| { + e.title("New feedback!".to_string()) + .description(feedback) + .color(Colour::new(*colour)) + .author(|a| a.icon_url(ctx.author().face()).name(ctx.author().tag())) + .thumbnail("https://media.giphy.com/media/U4sfHXAALLYBQzPcWk/giphy.gif") + }) + }) + .await?; + + ctx.say("Thank you for the feedback! It has been sent directly to our developers.") + .await?; + + Ok(()) +} + +#[instrument(skip(ctx))] +#[poise::command(prefix_command, slash_command, category = "Info", member_cooldown = 10)] +/// Have some info about the bot +pub async fn about(ctx: Context<'_>) -> Result<(), Error> { + let guild_count = ctx + .serenity_context() + .cache + .guilds() + .len() + .clone() + .to_string(); + + // TODO: change to your own URLs + let mut fields = vec![ + ("Help page:", "[https://fbtsecurity.fbtheaven.com/](https://fbtsecurity.fbtheaven.com/)", false), + ("Remove any of your info from the bot:", "[Delete your data](https://fbtsecurity.fbtheaven.com/data-and-privacy-policy#delete-your-data)", false), + ("Bot version:", VERSION.unwrap_or("unknown"), false), + ("Server count:", &guild_count, false), + ]; + + // TODO: reduce the ammount of #[cfg(feature = "database")] here!! + + #[cfg(feature = "database")] + let mut con = open_redis_connection().await?; + + #[cfg(feature = "database")] + let execution_count: String = redis::cmd("GET") + .arg("status:commands-executed") + .clone() + .query_async(&mut con) + .await?; + + #[cfg(feature = "database")] + let mut new_field = vec![( + "Total commands run since 2.0.18:", + execution_count.as_str(), + false, + )]; + + #[cfg(feature = "database")] + fields.append(&mut new_field); + + // TODO: change to your own URLs + ctx.send(|f| { + f.embed(|e| { + e.title("About") + .url("https://fbtsecurity.fbtheaven.com/") + .author(|a| { + a.name("FBT Staff") + .url("https://fbtsecurity.fbtheaven.com/") + }) + .fields(fields) + .footer(|foot| { + foot.text("Time mojo spent on V2.0+") + }) + .image(format!("https://wakatime.com/badge/user/fd57ff6b-f3f1-4957-b9c6-7e09bc3f0559/project/d2f87f17-8c44-4835-b4f6-f0089e52515f.png?rand={}", rand::thread_rng().gen_range(0..1_000_000_000))) + }) + }) + .await?; + + #[cfg(feature = "database")] + event!( + Level::INFO, + "Total commands run since 2.0.18" = execution_count.parse::().unwrap() + 1 + ); + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..5fa6f83 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,6 @@ +pub mod admin; +pub mod database; +pub mod fun; +pub mod info; +pub mod tickets; +pub mod tools; diff --git a/src/commands/tickets.rs b/src/commands/tickets.rs new file mode 100644 index 0000000..9c55440 --- /dev/null +++ b/src/commands/tickets.rs @@ -0,0 +1,225 @@ +use poise::serenity_prelude::{self as serenity}; +use poise::serenity_prelude::{ChannelId, RoleId}; +use poise::serenity_prelude::{PermissionOverwrite, PermissionOverwriteType, Permissions}; +use rusted_fbt_lib::enums::CloseTicketFail; +use rusted_fbt_lib::types::{Context, Error}; +use rusted_fbt_lib::utils::verbose_mode; +use rusted_fbt_lib::vars::{CLOSED_TICKET_CATEGORY, FBT_GUILD_ID, TICKET_CATEGORY}; +use tracing::instrument; + +#[instrument(skip(ctx))] +#[poise::command(slash_command, category = "Ticket", guild_only)] +/// Create new ticket (FBT discord only!) +pub async fn new_ticket( + ctx: Context<'_>, + #[description = "An optional topic to put on the ticket"] topic: Option, +) -> Result<(), Error> { + ctx.defer_ephemeral().await?; + + if *ctx.guild_id().unwrap().as_u64() == FBT_GUILD_ID { + let mut channels = Vec::new(); + + for channel in ctx.guild().unwrap().channels { + let parent_id = match channel.1.clone() { + serenity::Channel::Guild(g) => g.parent_id, + _ => None, + }; + + if let Some(cat) = parent_id { + if *cat.as_u64() == TICKET_CATEGORY { + channels.push(channel.0.name(ctx).await.unwrap()); + } + } + } + + let mut existing_ticket = false; + + for ch in channels { + if ch.starts_with(&ctx.author().name) { + existing_ticket = true; + } + } + + if existing_ticket { + ctx.send(|b| { + b.content("You already have a ticket open, you cannot open another!") + .ephemeral(true) + }) + .await?; + } else { + + // TODO: change these IDs to be your own server roles + + // Assuming a guild has already been bound. + let perms = vec![ + PermissionOverwrite { + allow: Permissions::READ_MESSAGE_HISTORY + | Permissions::VIEW_CHANNEL + | Permissions::SEND_MESSAGES + | Permissions::ADD_REACTIONS + | Permissions::EMBED_LINKS + | Permissions::ATTACH_FILES + | Permissions::USE_EXTERNAL_EMOJIS, + deny: Permissions::empty(), + kind: PermissionOverwriteType::Member(ctx.author().id), + }, + PermissionOverwrite { + allow: Permissions::all(), + deny: Permissions::SEND_TTS_MESSAGES, + kind: PermissionOverwriteType::Role(RoleId::from(737_168_134_569_590_888)), // Secretary (Probably not needed) + }, + PermissionOverwrite { + allow: Permissions::all(), + deny: Permissions::SEND_TTS_MESSAGES, + kind: PermissionOverwriteType::Role(RoleId::from(820_914_502_220_513_330)), // Admin (Probably not needed) + }, + PermissionOverwrite { + allow: Permissions::all(), + deny: Permissions::SEND_TTS_MESSAGES, + kind: PermissionOverwriteType::Role(RoleId::from(874_898_210_534_096_907)), // Mods + }, + PermissionOverwrite { + allow: Permissions::all(), + deny: Permissions::SEND_TTS_MESSAGES, + kind: PermissionOverwriteType::Role(RoleId::from(1_005_994_060_416_294_942)), // World admin panel + }, + PermissionOverwrite { + allow: Permissions::all(), + deny: Permissions::SEND_TTS_MESSAGES, + kind: PermissionOverwriteType::Role(RoleId::from(1_046_937_023_400_919_091)), // World admin panel trainee + }, + PermissionOverwrite { + allow: Permissions::empty(), + deny: Permissions::all(), + kind: PermissionOverwriteType::Role(RoleId::from(737_168_134_502_350_849)), // @everyone + }, + ]; + + match ctx + .guild() + .expect("") + .create_channel(ctx, |c| { + c.category(ChannelId::from(TICKET_CATEGORY)) + .name(format!( + "{}-{}", + ctx.author().name, + chrono::offset::Utc::now().format("%s") + )) + .permissions(perms) + .topic(topic.unwrap_or_else(|| "A new ticket".to_string())) + }) + .await + { + Ok(ch) => { + ctx.send(|b| { + b.content(format!( + "Ticket created! Find it here: <#{}>", + ch.id.as_u64() + )) + .ephemeral(true) + }) + .await?; + + ch.say( + ctx, + format!("New ticket opened by <@{}>!", ctx.author().id.as_u64()), + ) + .await?; + } + Err(error) => { + let err_msg = if verbose_mode() { + format!("Failed to create ticket. Reason: {error:?}") + } else { + "Failed to create ticket".to_string() + }; + + ctx.send(|b| b.content(err_msg).ephemeral(true)).await?; + } + } + } + } + + Ok(()) +} + +#[poise::command(slash_command, category = "Ticket", guild_only)] +/// Closes the current ticket (FBT discord only!) +pub async fn close_ticket(ctx: Context<'_>) -> Result<(), Error> { + ctx.defer_ephemeral().await?; + + if *ctx.guild_id().unwrap().as_u64() == FBT_GUILD_ID { + let mut failed = CloseTicketFail::False; + + let current_channel = ctx.channel_id().to_channel(ctx).await?; + let chnnl_name = ctx + .channel_id() + .name(ctx) + .await + .unwrap_or_else(|| "Unkown Ticket".to_string()); + + let parent_id = match current_channel { + serenity::Channel::Guild(g) => g.parent_id, + _ => None, + }; + + match parent_id { + None => { + failed = CloseTicketFail::False; + } + Some(channel_category) => { + if *channel_category.as_u64() == TICKET_CATEGORY { + match ctx + .channel_id() + .edit(ctx, |c| { + c.category(Some(ChannelId::from(CLOSED_TICKET_CATEGORY))) + .name(format!( + "{}-{}", + chnnl_name, + chrono::offset::Utc::now().format("%s") + )) + }) + .await + { + Ok(_) => {} + Err(fail_reason) => { + failed = CloseTicketFail::SerenityError(fail_reason); + } + } + } else { + failed = CloseTicketFail::IncorrectCategory; + } + } + } + + match failed { + CloseTicketFail::False => { + ctx.say("Ticket closed!").await?; + } + CloseTicketFail::IncorrectCategory => { + ctx.send(|b| { + b.content(format!( + "This can only be ran inside of a channel under <#{}>!", + TICKET_CATEGORY + )) + .ephemeral(true) + }) + .await?; + } + CloseTicketFail::SerenityError(error) => { + ctx.send(|b| { + b.content(format!( + "Failed to close ticker because of following error:\n{}", + error + )) + .ephemeral(true) + }) + .await?; + } + } + } else { + ctx.say("This command must be ran inside of FBT's discord") + .await?; + } + + Ok(()) +} diff --git a/src/commands/tools.rs b/src/commands/tools.rs new file mode 100644 index 0000000..33c71e4 --- /dev/null +++ b/src/commands/tools.rs @@ -0,0 +1,320 @@ +use chrono::NaiveDateTime; +use poise::serenity_prelude::{self as serenity, AttachmentType, RichInvite}; +use rusted_fbt_lib::{ + checks::guild_auth_check, + types::{Context, Error}, + utils::snowflake_to_unix, +}; +use serde::Deserialize; +use tracing::instrument; + +use crate::commands::database::check_username_against_db; + +#[instrument(skip(ctx))] +#[poise::command(slash_command, track_edits, category = "Tools")] +/// Display your or another user's account creation date +pub async fn account_age( + ctx: Context<'_>, + #[description = "Selected user"] user: Option, +) -> Result<(), Error> { + let user = user.as_ref().unwrap_or_else(|| ctx.author()); + + let uid = *user.id.as_u64(); + + let unix_timecode = snowflake_to_unix(u128::from(uid)); + + #[allow(clippy::cast_possible_truncation)] + // this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN + let date_time_stamp = + NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0).unwrap_or(NaiveDateTime::MIN); + + let age = chrono::Utc::now() + .naive_utc() + .signed_duration_since(date_time_stamp) + .num_days(); + + ctx.say(format!( + "{}'s account was created at {}.\nSo They are {} days old.", + user.name, + user.created_at(), + age + )) + .await?; + + Ok(()) +} + +/// Gets the creation date or a Snowflake ID +#[instrument(skip(ctx))] +#[poise::command(prefix_command, slash_command, category = "Tools")] +pub async fn creation_date( + ctx: Context<'_>, + #[description = "ID of User/Message/Channel/ect"] snowflake_id: u128, +) -> Result<(), Error> { + let unix_timecode = snowflake_to_unix(snowflake_id); + + #[allow(clippy::cast_possible_truncation)] + // this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN + let date_time_stamp = + NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0).unwrap_or(NaiveDateTime::MIN); + + ctx.say(format!("Created/Joined on {date_time_stamp}")) + .await?; + + Ok(()) +} + +/// qmit +#[instrument(skip(ctx))] +#[poise::command(owners_only, slash_command, hide_in_help)] +pub async fn bot_owner_tool_1(ctx: Context<'_>) -> Result<(), Error> { + ctx.defer_ephemeral().await?; + + let guild_list = ctx.serenity_context().cache.guilds(); + + let mut invites: Vec = Vec::new(); + + for guild in guild_list { + let guild_invites: Option> = (guild.invites(ctx).await).ok(); + + if guild_invites.clone().is_some() { + invites.append(&mut guild_invites.unwrap()); + } + } + + // let shit_list = format!("All invites the bot can see:\n\n{:?}", invites); + + let mut new_list: String = "Every invite the bot can see, grouped by guild:\n\n[\n".to_string(); + + for invite in invites { + new_list.push_str(format!("{},\n", serde_json::to_string(&invite)?).as_str()); + } + + new_list.push(']'); + + ctx.send(|b| { + b.content("All bot invites:".to_string()) + .attachment(AttachmentType::Bytes { + data: std::borrow::Cow::Borrowed(new_list.as_bytes()), + filename: format!("{}_invites.txt", ctx.id()), + }) + }) + .await?; + + Ok(()) +} + +/// Get's all avaliable info from a Discord Invite +#[instrument(skip(ctx))] +#[poise::command( + prefix_command, + slash_command, + category = "Tools", + member_cooldown = 5, + check = "guild_auth_check", + guild_only +)] +pub async fn invite_info( + ctx: Context<'_>, + #[description = "Invite URL"] invite_url: String, +) -> Result<(), Error> { + use linkify::LinkFinder; + + #[derive(Debug, Deserialize, Clone)] + struct InviteObject { + #[serde(rename(deserialize = "type"))] + _type: u64, + code: String, + inviter: InviterObject, + expires_at: Option, + guild: PartialGuild, + guild_id: String, + channel: PartialChannel, + approximate_member_count: u64, + approximate_presence_count: u64, + } + + #[derive(Debug, Deserialize, Clone)] + struct InviterObject { + id: String, + username: String, + #[allow(dead_code)] + avatar: Option, + discriminator: Option, + #[allow(dead_code)] + public_flags: u64, + #[allow(dead_code)] + flags: u64, + #[allow(dead_code)] + banner: Option, + #[allow(dead_code)] + accent_color: Option, + global_name: Option, + #[allow(dead_code)] + avatar_decoration_data: Option, + #[allow(dead_code)] + banner_color: Option, + } + + #[derive(Debug, Deserialize, Clone)] + struct PartialGuild { + #[allow(dead_code)] + id: String, + name: String, + #[allow(dead_code)] + splash: Option, + #[allow(dead_code)] + banner: Option, + description: Option, + #[allow(dead_code)] + icon: Option, + #[allow(dead_code)] + features: Vec, + #[allow(dead_code)] + verification_level: u64, + vanity_url_code: Option, + #[allow(dead_code)] + nsfw_level: u64, + #[allow(dead_code)] + nsfw: bool, + premium_subscription_count: u64, + } + + #[derive(Debug, Deserialize, Clone)] + struct PartialChannel { + id: String, + #[serde(rename(deserialize = "type"))] + _type: u64, + name: String, + } + + let finder = LinkFinder::new(); + let links: Vec<_> = finder.links(&invite_url).collect(); + + if links.is_empty() { + ctx.say("No valid links found").await?; + return Ok(()); + } + + let link_str = links[0].as_str().to_owned(); + + let (_link, invite_code) = link_str.split_at(19); + + let response = reqwest::get(format!( + "https://discord.com/api/v10/invites/{invite_code}?with_counts=true" + )) + .await?; + let response_formatted: Option = response.json().await?; + + if response_formatted.is_none() { + ctx.say("Invite not found").await?; + return Ok(()); + } + + let invite = response_formatted.unwrap(); + + let invite_info_fields = vec![ + ("Code:", invite.code, false), + ("Expires:", invite.expires_at.unwrap_or_default(), false), + ("Destination channel name:", invite.channel.name, false), + ("Destination channel ID:", invite.channel.id, false), + ]; + + let guild_info_fields = vec![ + ("Server name:", invite.guild.name, false), + ("Server ID:", invite.guild_id, false), + ( + "Server Description:", + invite.guild.description.unwrap_or_default(), + true, + ), + ( + "Vanity URL code:", + invite.guild.vanity_url_code.unwrap_or_default(), + false, + ), + ( + "Server boosts count:", + format!("{}", invite.guild.premium_subscription_count), + false, + ), + ( + "Approx member count:", + format!("{}", invite.approximate_member_count), + false, + ), + ( + "Approx online user count:", + format!("{}", invite.approximate_presence_count), + false, + ), + ]; + + let inviter_info_fields = vec![ + ("Username:", invite.inviter.username, false), + ( + "Global username:", + invite.inviter.global_name.unwrap_or_default(), + false, + ), + ("User ID:", invite.inviter.id.clone(), false), + ( + "Discriminator(Eg: #0001):", + invite.inviter.discriminator.unwrap_or_default(), + false, + ), + ]; + + ctx.send(|f| { + f.embed(|e| e.title("Invite Info").fields(invite_info_fields)) + .embed(|e| e.title("Guild Info").fields(guild_info_fields)) + .embed(|e| e.title("Inviter Info").fields(inviter_info_fields)) + }) + .await?; + + let unix_timecode = snowflake_to_unix(u128::from(ctx.author().id.0)); + + #[allow(clippy::cast_possible_truncation)] + // this shouldn't be able to break but just in case I'm making the `unwrap_or` output NaiveDateTime::MIN + let date_time_stamp = + NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0).unwrap_or(NaiveDateTime::MIN); + + let age = chrono::Utc::now() + .naive_utc() + .signed_duration_since(date_time_stamp) + .num_days(); + + let is_user_in_db: Option = + check_username_against_db(invite.inviter.id.parse::().unwrap()) + .await + .unwrap(); + + + // TODO: set your own channel ID! + + // log user name, id, guild name, id and url to channel + serenity::ChannelId() + .send_message(ctx, |f| { + f.embed(|e| { + e.title("User requested invite info") + .field("Username", ctx.author().name.clone(), true) + .field("User ID", ctx.author().id.0.to_string(), true) + .field("User Account age (days)", age, true) + .field("Source Server Name", ctx.guild().unwrap().name, true) + .field( + "Source Server ID", + ctx.guild().unwrap().id.0.to_string(), + true, + ) + .field("Url provided", link_str, true) + .field( + "Is User in DB", + format!("{}", is_user_in_db.is_some()), + false, + ) + }) + }) + .await?; + + Ok(()) +} diff --git a/src/lib/args.rs b/src/lib/args.rs new file mode 100644 index 0000000..33495c2 --- /dev/null +++ b/src/lib/args.rs @@ -0,0 +1,36 @@ +use crate::enums::{DebugLevel, LogDebugLevel}; +use crate::vars::BOT_TOKEN; +use clap::Parser; + +/// This is where CLI args are set +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +pub struct Args { + /// Discord bot token + #[clap( + short, + long, + default_value = BOT_TOKEN + )] + pub token: String, + + /// Command prefix for message commands + #[clap(short, long, default_value = "`")] + pub prefix: String, + + /// Output extra information in discord reply errors + #[clap(short, long)] + pub verbose: bool, + + /// Print out list of guilds in cache on startup + #[clap(long("gpc"))] + pub print_guild_cache: bool, + + /// emit debug information to both stdout and a file + #[clap(value_enum, long, default_value = "most")] + pub debug: DebugLevel, + + /// emit debug information to both stdout and a file + #[clap(value_enum, long, default_value = "most")] + pub debug_log: LogDebugLevel, +} diff --git a/src/lib/checks.rs b/src/lib/checks.rs new file mode 100644 index 0000000..a12b1e0 --- /dev/null +++ b/src/lib/checks.rs @@ -0,0 +1,100 @@ +use crate::types::{Context, Error}; +use crate::vars::BOT_ADMINS; +use std::collections::HashSet; + +/// Check if command user is in the `BOT_ADMINS` list +/// +/// # Errors +/// +/// This function will never return an error. +#[allow(clippy::unused_async, clippy::missing_errors_doc)] // async is used by command checks but clippy can't tell +pub async fn bot_admin_check(ctx: Context<'_>) -> Result { + // ? The bellow commented out code is for quick testing, automatic fails on my ID + // match ctx.author().id.as_u64() { + // 383507911160233985 => Ok(false), + // _ => { + // match BOT_ADMINS.contains(ctx.author().id.as_u64()) { + // true => Ok(true), + // false => Ok(false), + // } + // } + // } + if BOT_ADMINS.contains(ctx.author().id.as_u64()) { + Ok(true) + } else { + Ok(false) + } +} + +// ? This might not be needed, I thinik it's a left over from before we dud guild based authing +// ! Remove the _ if put into use! +#[cfg(feature = "database")] +#[deprecated( + since = "0.1.12", + note = "left over from before we dud guild based auth" +)] +#[allow(clippy::unused_async, clippy::missing_errors_doc)] // no need to lint dead code +pub async fn _bot_auth_check(_ctx: Context<'_>) -> Result { + // if let Ok(res) = bot_admin_check(ctx).await { + // if res { + // return Ok(true); + // } + // } + + // let mut con = open_redis_connection().await?; + + // let key_list: HashSet = redis::cmd("SMEMBERS") + // .arg("user-lists:authorised-users") + // .clone() + // .query_async(&mut con) + // .await?; + + // if key_list.contains(ctx.author().id.as_u64()) { + // Ok(true) + // } else { + // ctx.say("You are not authorized to use this command! Please contact a bot admin or Azuki!") + // .await?; + // Ok(false) + // } + Ok(false) +} + +/// Checks if a user is authorised to use the bot in the current server +/// +/// # Errors +/// +/// This function will return an error if unable to connet to or query DB. +#[cfg(feature = "database")] +pub async fn guild_auth_check(ctx: Context<'_>) -> Result { + use crate::utils::open_redis_connection; + + if let Ok(res) = bot_admin_check(ctx).await { + if res { + return Ok(true); + } + } + + let mut con = open_redis_connection().await?; + + let key_list: Option> = redis::cmd("SMEMBERS") + .arg(format!( + "authed-server-users:{}", + ctx.guild_id() + .unwrap_or(poise::serenity_prelude::GuildId(0)) + .as_u64() + )) + .clone() + .query_async(&mut con) + .await?; + + match key_list { + None => Ok(false), + Some(list) if !list.contains(&format!("{}", ctx.author().id.as_u64())) => { + Ok({ + ctx.say("You are not authorized to use this command! Please contact a bot admin or Azuki!").await?; + false + }) + } + Some(_list) => Ok(true), + } +} diff --git a/src/lib/enums.rs b/src/lib/enums.rs new file mode 100644 index 0000000..ea3b5a9 --- /dev/null +++ b/src/lib/enums.rs @@ -0,0 +1,57 @@ +use poise::serenity_prelude::{self as serenity}; + +#[derive(Debug, poise::ChoiceParameter)] +pub enum WaifuTypes { + Neko, + Megumin, + Bully, + Cuddle, + Cry, + Kiss, + Lick, + Pat, + Smug, + Bonk, + Blush, + Smile, + Wave, + Highfive, + Handhold, + Nom, + Bite, + Glomp, + Slap, + Kill, + Happy, + Wink, + Poke, + Dance, + Cringe, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, clap::ValueEnum)] +pub enum DebugLevel { + Off, + Some, + Most, + All, +} + +impl DebugLevel { + #[must_use] + pub fn enabled(&self) -> bool { + *self != Self::Off + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, clap::ValueEnum)] +pub enum LogDebugLevel { + Most, + All, +} + +pub enum CloseTicketFail { + False, + IncorrectCategory, + SerenityError(serenity::Error), +} diff --git a/src/lib/event_handlers.rs b/src/lib/event_handlers.rs new file mode 100644 index 0000000..20368cc --- /dev/null +++ b/src/lib/event_handlers.rs @@ -0,0 +1,574 @@ +use crate::structs::{GuildSettings, UserInfo, WaybackResponse, WaybackStatus}; +use crate::utils::snowflake_to_unix; +use crate::vars::FBT_GUILD_ID; +use chrono::NaiveDateTime; +use chrono::Utc; +use chrono_tz::Australia::Melbourne; +use colored::Colorize; +use poise::serenity_prelude::{self as serenity, ChannelId, Colour, MessageUpdateEvent}; +use rand::Rng; +use std::collections::HashMap; +use tracing::{event, Level}; + +// TODO: Change to the ID of a channel you want all DMs sent to the bot to be relayed to +const DM_CHANNEL_ID: u64 = 0000000000000000000; + +/// If enabled on a server it will warn them on black listed users joining +/// +/// # Panics +/// +/// Panics if unable to parse channel ID from DB to u64. +/// +/// # Errors +/// +/// This function will return an error if; +/// - Fails to contact redis DB. +/// - Fails to get guild settings from DB. +/// - Fails to ask Redis for coresponding DB entry for user. +/// - Fails to send message to channel. +#[cfg(feature = "database")] +pub async fn bl_warner( + ctx: &serenity::Context, + member: &serenity::Member, +) -> Result<(), Box> { + use crate::utils::open_redis_connection; + + let mut con = open_redis_connection().await?; + + let guild_settings_json_in: Option = redis::cmd("JSON.GET") + .arg(format!("guild-settings:{}", member.guild_id.as_u64())) + .clone() + .query_async(&mut con) + .await?; + + let if_on_bl: Option = redis::cmd("JSON.GET") + .arg(format!("user:{}", member.user.id.as_u64())) + .clone() + .query_async(&mut con) + .await?; + + match if_on_bl { + None => {} + Some(user_json) => { + match guild_settings_json_in { + None => {} // Do nothing + // Check guild settings + Some(server_json) => { + let settings: GuildSettings = serde_json::from_str(&server_json)?; + let user: UserInfo = serde_json::from_str(&user_json)?; + + ChannelId::from(settings.channel_id.parse::().unwrap()) + .say( + ctx, + format!( + "<@{}>/{0} Just joined your server with {} offenses on record", + user.discord_id.unwrap(), + user.offences.len() + ), + ) + .await?; + } + } + } + } + + Ok(()) +} + +/// Checks if server has alt protection enabled and then kicks the new member if they are >90 days old +/// +/// # Errors +/// +/// This function will return an error if; +/// - Fails to connect to Redis DB. +/// - Fails to serde guild settings json to `GuildSettings` struct. +/// - Fails to send DM to user getting kicked. +/// - Fails to actually kick member. +/// +#[cfg(feature = "database")] +pub async fn alt_kicker( + ctx: &serenity::Context, + member: &serenity::Member, +) -> Result<(), Box> { + use crate::utils::open_redis_connection; + use std::collections::HashSet; + + let mut con = open_redis_connection().await?; + + let whitelist: HashSet = redis::cmd("SMEMBERS") + .arg("kick-whitelist") + .clone() + .query_async(&mut con) + .await?; + + if whitelist.contains(&member.user.id.0.to_string()) { + return Ok(()); // Don't kick whitelisted users + } + + let guild_settings_json_in: Option = redis::cmd("JSON.GET") + .arg(format!("guild-settings:{}", member.guild_id.as_u64())) + .clone() + .query_async(&mut con) + .await?; + + match guild_settings_json_in { + None => {} // Do nothing + // Check guild settings + Some(json_in) => { + let settings: GuildSettings = serde_json::from_str(&json_in)?; + // Is kicking enabled? + if settings.kick { + let uid = *member.user.id.as_u64(); + + // Trying to handle the pfp here to see if it catches more or maybe most alts really do have the same pfp + let pfp = member + .avatar_url() + .unwrap_or_else(|| { + "https://discord.com/assets/1f0bfc0865d324c2587920a7d80c609b.png" + .to_string() + }) + .clone(); + + let unix_timecode = snowflake_to_unix(u128::from(uid)); + + #[allow(clippy::pedantic)] + // it literally only take's i64, no need to warn about truncation here. + let date_time_stamp = NaiveDateTime::from_timestamp_opt(unix_timecode as i64, 0) + .unwrap_or(NaiveDateTime::MIN); + + let age = chrono::Utc::now() + .naive_utc() + .signed_duration_since(date_time_stamp) + .num_days(); + + // Compare user age + if !age.ge(&90_i64) { + member.user.direct_message(ctx.http.clone(), |f| { + f.content("It looks like your account is under 90 days old, or has been detected as a potential alt. You have been kick from the server!\nYou have not been banned, feel free to join back when your account is over 90 days old.\nRun the `about` slash command or send `help in this chat to find out more.") + }).await?; + member + .kick_with_reason( + ctx.http.clone(), + &format!("Potential alt detected, account was {age:.0} day(s) old"), + ) + .await?; + + let colour = &mut rand::thread_rng().gen_range(0..10_000_000); + + ChannelId(settings.channel_id.parse::()?) + .send_message(ctx.http.clone(), |f| { + f.embed(|e| { + e.title("Alt kicked!") + .description(format!( + "Potential alt detected, account was {:.0} day(s) old", + age + )) + .thumbnail(pfp) + .field("User ID", uid, true) + .field("Name", member.user.name.clone(), true) + .color(Colour::new(*colour)) + }) + }) + .await?; + } + } + } + } + Ok(()) +} + +/// Sends all recieved DMs into a specified channel +/// +/// # Errors +/// +/// This function will return an error if; +/// - Fails to handle message attachments. +/// - Fails to handle message stickers. +/// - Fails to send request to wayback machine. +/// - Fails to send message to DM channel. +// TODO: Handle attachments, list of links? +pub async fn handle_dms( + event: &serenity::Message, + ctx: &serenity::Context, +) -> Result<(), Box> { + if !event.author.bot { + let message = event.clone(); + let uid = *message.author.id.as_u64(); + + let icon = message.author.avatar_url().map_or_else( + || "https://discord.com/assets/1f0bfc0865d324c2587920a7d80c609b.png".to_string(), + |url| url, + ); + + let cache = ctx.http.clone(); + + let colour = &mut rand::thread_rng().gen_range(0..10_000_000); + + let now = Utc::now().with_timezone(&Melbourne); + + let local_time = now.to_string(); + + let timestamp = local_time.to_string(); + + let mut wayback_job_ids = Vec::new(); + + let list_of_files = if message.attachments.is_empty() | message.sticker_items.is_empty() { + "N/A".to_string() + } else { + let mut urls = Vec::new(); + + handle_files(&message, &mut wayback_job_ids, &mut urls).await?; + + // Duped code for stickers, could probably refactor into function + handle_stickers(&message, ctx, &mut wayback_job_ids, &mut urls).await?; + + urls.join("\n \n") + }; + + let mut msg = ChannelId(DM_CHANNEL_ID) + .send_message(cache, |f| { + f.embed(|e| { + e.title("New message:") + .description(message.content.clone()) + .field("Attachments/Stickers:", list_of_files.clone(), false) + .field("User ID", uid, false) + .field("Recieved at:", timestamp.clone(), false) + .author(|a| a.icon_url(icon.clone()).name(message.author.name.clone())) + .color(Colour::new(*colour)) + }) + }) + .await?; + + let mut wayback_urls: Vec = Vec::new(); + + for job in wayback_job_ids { + let mut is_not_done = true; + while is_not_done { + let client = reqwest::Client::new(); + + // TODO: Change to your own wayback machine authorization key + let response = client + .get(format!("https://web.archive.org/save/status/{job}")) + .header("Accept", "application/json") + .header("Authorization", "LOW asdgasdg:fasfaf") // auth key here!! + .send() + .await?; + let response_content = response.text().await?; + let wayback_status: WaybackStatus = serde_json::from_str(&response_content)?; + + if wayback_status.status == *"success" { + wayback_urls.push(format!( + "https://web.archive.org/web/{}", + wayback_status.original_url.unwrap_or_else(|| { + "20220901093722/https://www.dafk.net/what/".to_string() + }) + )); + is_not_done = false; + } + } + } + + if !wayback_urls.is_empty() { + msg.edit(ctx, |f| { + f.embed(|e| { + e.title("New message:") + .description(message.content.clone()) + .field("Attachments/Stickers:", list_of_files, false) + .field( + "Archived Attachments/Stickers:", + wayback_urls.join("\n \n"), + false, + ) + .field("User ID", uid, false) + .field("Recieved at:", timestamp, false) + .author(|a| a.icon_url(icon).name(message.author.name.clone())) + .color(Colour::new(*colour)) + }) + }) + .await?; + } + } + + Ok(()) +} + +/// Handles DM files. +/// +/// # Errors +/// +/// This function will return an error if Failes to contact wayback machine. +async fn handle_files( + message: &serenity::Message, + wayback_job_ids: &mut Vec, + urls: &mut Vec, +) -> Result<(), Box> { + for file in message.attachments.clone() { + let client = reqwest::Client::new(); + + let mut params = HashMap::new(); + params.insert("url".to_string(), file.url.clone()); + params.insert("skip_first_archive".to_string(), "1".to_string()); + + // TODO: Change to your own wayback machine authorization key + + let response = client + .post("https://web.archive.org/save") + .form(¶ms) + .header("Accept", "application/json") + .header("Authorization", "LOW asdgasdg:fasfaf") + .send() + .await?; + + let response_content = response.text().await?; + let wayback_status: WaybackResponse = serde_json::from_str(&response_content)?; + + if wayback_status.status.is_none() { + if let Some(jid) = wayback_status.job_id { + wayback_job_ids.push(jid); + } + } + + urls.push(file.url); + } + Ok(()) +} + +/// Handles DM stickers. +/// +/// # Errors +/// +/// This function will return an error if Failes to contact wayback machine. +async fn handle_stickers( + message: &serenity::Message, + ctx: &serenity::Context, + wayback_job_ids: &mut Vec, + urls: &mut Vec, +) -> Result<(), Box> { + for file in message.sticker_items.clone() { + let client = reqwest::Client::new(); + + let mut params = HashMap::new(); + params.insert( + "url".to_string(), + file.to_sticker(ctx) + .await + .unwrap() + .image_url() + .unwrap() + .clone(), + ); + params.insert("skip_first_archive".to_string(), "1".to_string()); + + // TODO: Change to your own wayback machine authorization key + + let response = client + .post("https://web.archive.org/save") + .form(¶ms) + .header("Accept", "application/json") + .header("Authorization", "LOW asdgasdg:fasfaf") + .send() + .await?; + + let response_content = response.text().await?; + let wayback_status: WaybackResponse = serde_json::from_str(&response_content)?; + + match wayback_status.status { + None => { + if let Some(jid) = wayback_status.job_id { + wayback_job_ids.push(jid); + } + } + Some(_) => {} + } + + urls.push(file.to_sticker(ctx).await.unwrap().image_url().unwrap()); + } + Ok(()) +} + +/// When a message is edited in FBT this function will send the new and old message to a specified channel. +/// +/// # Panics +/// +/// Panics if an author doesn't exist, should be unreachable. +/// +/// # Errors +/// +/// This function will return an error if the message fails to send. +pub async fn handle_msg_edit( + event: MessageUpdateEvent, + old_if_available: &Option, + ctx: &serenity::Context, + new: &Option, +) -> Result<(), Box> { + if event.guild_id.is_some() { + if let Some(author) = event.author.clone() { + if !author.bot { + let old_message = old_if_available.as_ref().map_or_else( + || "Message not stored in cache :(".to_string(), + |msg| msg.content.to_string(), + ); + + let new_message = new.as_ref().map_or_else( + || "Message not stored in cache :(".to_string(), + |msg| msg.content.to_string(), + ); + + let message_url = new.as_ref().map_or_else( + || "URL stored in cache :(".to_string(), + poise::serenity_prelude::Message::link, + ); + + let current_time = Utc::now().with_timezone(&Melbourne); + + let local_time = current_time.to_string(); + + let timestamp = local_time.to_string(); + + // TODO: channel to alert you that a message has been deleted + + ChannelId(891_294_507_923_025_951) + .send_message(ctx.http.clone(), |f| { + f.embed(|e| { + e.title(format!( + "\"{}\" Edited a message", + event.author.clone().unwrap().tag() + )) + .field("Old message content:", old_message, false) + .field("New message content:", new_message, false) + .field("Link:", message_url, false) + .field("Edited at:", timestamp, false) + .footer(|f| { + f.text(format!( + "User ID: {}", + event.author.clone().unwrap().id.as_u64() + )) + }) + .color(Colour::new(0x00FA_A81A)) + }) + }) + .await?; + } + } + }; + Ok(()) +} + +/// Handles messages that have been deleted +/// +/// # Panics +/// +/// Panics if there is no message object in cache. +/// +/// # Errors +/// +/// This function will return an error if unable to send message to channel. +pub async fn handle_msg_delete( + guild_id: &Option, + ctx: &serenity::Context, + channel_id: &ChannelId, + deleted_message_id: &serenity::MessageId, +) -> Result<(), Box> { + match guild_id { + None => {} + Some(gid) => { + // TODO: this logs any delted message in FBT specifically, change to your own server ID + if *gid.as_u64() == 737_168_134_502_350_849 { + match ctx.cache.message(channel_id, deleted_message_id) { + None => {} + Some(msg) => { + if !msg.author.bot { + let message = match ctx.cache.message(channel_id, deleted_message_id) { + None => "Message not stored in cache :(".to_string(), + Some(msg) => format!("{:?}", msg.content), + }; + + let author_id = match message.as_str() { + "Message not stored in cache :(" => 0_u64, + _ => *ctx + .cache + .message(channel_id, deleted_message_id) + .unwrap() + .author + .id + .as_u64(), + }; + + let author_tag = + if message.clone().as_str() == "Message not stored in cache :(" { + "Not in cache#000".to_string() + } else { + format!( + "{:?}", + match ctx.cache.message(channel_id, deleted_message_id) { + Some(msg) => { + msg.author.tag() + } + None => { + String::new() // This just creates "" + } + } + ) + }; + + let now = Utc::now().with_timezone(&Melbourne); + + let local_time = now.to_string(); + + let timestamp = local_time.to_string(); + + // TODO: This is the channel the deleted messages are sent to + + ChannelId(891_294_507_923_025_951) + .send_message(ctx.http.clone(), |f| { + f.embed(|e| { + e.title(format!("{author_tag} deleted a message")) + .field("Message content:", message, false) + .field("Deleted at:", timestamp, false) + .field( + "Channel link:", + format!( + "https://discord.com/channels/{}/{}", + guild_id + .unwrap_or(serenity::GuildId::from( + FBT_GUILD_ID + )) + .as_u64(), + channel_id.as_u64() + ), + false, + ) + .footer(|f| f.text(format!("User ID: {author_id}"))) + .color(Colour::new(0x00ED_4245)) + }) + }) + .await?; + } + } + } + } + } + }; + Ok(()) +} + +/// Prints message and outputs trace if in verbose mode +pub fn handle_resume(event: &serenity::ResumedEvent) { + event!( + Level::INFO, + "ResumedEvent" = format!( + "{}", + "Bot went offline but is online again".bright_red().italic() + ) + ); + + // Is this a good idea? + event!( + Level::TRACE, + "ResumedEvent" = format!( + "{}", + "Bot went offline but is online again".bright_red().italic() + ), + "event" = ?event + ); +} diff --git a/src/lib/lib.rs b/src/lib/lib.rs new file mode 100644 index 0000000..b59629c --- /dev/null +++ b/src/lib/lib.rs @@ -0,0 +1,9 @@ +pub mod args; +pub mod checks; +pub mod enums; +pub mod event_handlers; +pub mod memes; +pub mod structs; +pub mod types; +pub mod utils; +pub mod vars; diff --git a/src/lib/memes.rs b/src/lib/memes.rs new file mode 100644 index 0000000..1b636d9 --- /dev/null +++ b/src/lib/memes.rs @@ -0,0 +1,77 @@ +use crate::vars::FBT_GUILD_ID; +use once_cell::sync::Lazy; +use poise::serenity_prelude::{self as serenity}; +use regex::Regex; +use strip_markdown::strip_markdown; + +// Some times maybe good sometimes maybe shit +pub static POG_RE: Lazy = Lazy::new(|| { + Regex::new( + r"[pP𝓹⍴𝖕𝔭የ𝕡ק🅟🅿ⓟρᑭ𝙥քp̷þp͎₱ᵽ℘ア𝐩𝒑𝓅p̞̈͑̚͞℘p͓̽ք𝓹ᕶp̶p̳p̅][oO0øØ𝓸᥆𝖔𝔬ዐ𝕠๏🅞🅾ⓞσOoÒօoo̷ðo͎の𝗼ᴏᵒ🇴‌𝙤ѻⲟᓍӨo͓̽o͟o̲o̅o̳o̶🄾o̯̱̊͊͢Ꭷσℴ𝒐𝐨][gG9𝓰𝖌𝔤𝕘🅖🅶ⓖɠGg𝑔ցg̷gg͎g̲g͟ǥ₲ɢg͓̽Gɠ𝓰𝙜🇬‌Ꮆᵍɢ𝗴𝐠𝒈𝑔ᧁg𝚐₲Ꮆ𝑔ĝ̽̓̀͑𝘨ງ🄶𝔤ģ]\b", + ).unwrap() +}); + +/// This will read a message and check to see if the message contains the word `pog` +/// +/// # Panics +/// +/// Panics if regex fails to compile, this should be unreachable unless I acidentally change something before compile time. +/// +/// # Errors +/// +/// This function will return an error if . +pub async fn pog_be_gone( + new_message: &serenity::Message, + ctx: &serenity::Context, +) -> Result<(), Box> { + if !new_message.author.bot && !new_message.content.is_empty() { + match new_message.guild(ctx) { + None => {} // Probably a DM, do nothing + Some(guild) => { + if guild.id.as_u64() == &FBT_GUILD_ID { + let lowercase_message = new_message.content.to_lowercase(); + let cleaned_message = strip_markdown(&lowercase_message); + + let words: Vec<&str> = cleaned_message.split(' ').collect(); + let mut hits: Vec<&str> = Vec::new(); + + for word in words { + POG_RE.find(word).map_or((), |pog| { + hits.push(pog.as_str()); + }); + } + + if !hits.is_empty() { + // there is at least 1 pog found + if hits.capacity().gt(&10) { + new_message + .reply( + ctx, + format!( + "Jesus dude, why did you pog {} times?! stop it!", + hits.len() + ), + ) + .await?; + } else { + new_message.reply_mention(ctx, "please refer to the rules and use the term 'poi' instead of 'pog'!").await?; + } + } + } + } + } + }; + Ok(()) +} + +#[cfg(test)] +mod meme_tests { + use super::*; + + #[test] + fn test_regex() { + let pog_test = "pog"; + + assert!(POG_RE.is_match(pog_test)); + } +} diff --git a/src/lib/structs.rs b/src/lib/structs.rs new file mode 100644 index 0000000..cd2914b --- /dev/null +++ b/src/lib/structs.rs @@ -0,0 +1,157 @@ +use merge::Merge; +use serde::{Deserialize, Serialize}; +use serde_with::{As, FromInto}; + +// User data, which is stored and accessible in all command invocations +#[derive(Debug)] +pub struct Data {} + +#[allow(non_snake_case)] +#[derive(Debug, Deserialize, PartialEq, Hash, Eq)] +pub struct CsvEntry { + pub AuthorID: String, + pub Author: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Merge, Clone, Hash, Eq)] +pub struct UserInfo { + #[serde(with = "As::>")] + pub vrc_id: Option, + #[serde(with = "As::>")] + pub username: Option, + #[serde(with = "As::>")] + pub discord_id: Option, + #[merge(strategy = merge::vec::append)] + pub offences: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)] +pub struct GuildSettings { + pub channel_id: String, + pub kick: bool, + pub server_name: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Merge, Hash, Eq)] +pub struct Offense { + #[merge(skip)] + pub guild_id: String, + #[merge(skip)] + pub reason: String, + #[serde(with = "As::>")] + pub image: Option, + #[serde(with = "As::>")] + pub extra: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)] +pub struct GuildAuthList { + pub users: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)] +pub struct ClearedUser { + pub user_id: String, + pub username: String, + pub where_found: String, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)] +pub struct MonitoredGuildInfo { + pub guild_name: String, + pub guild_id: String, + pub invite_link: String, + pub updated: String, + pub status: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)] +pub struct BlacklistHit { + pub user_id: String, + pub username: String, + pub guild_id: String, + pub reason: String, + pub image: String, + pub extra: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WaybackResponse { + pub url: Option, + #[serde(rename = "job_id")] + pub job_id: Option, + pub message: Option, + pub status: Option, + #[serde(rename = "status_ext")] + pub status_ext: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WaybackStatus { + #[serde(rename = "http_status")] + pub http_status: Option, + #[serde(default)] + outlinks: Vec, + pub timestamp: Option, + #[serde(rename = "original_url")] + pub original_url: Option, + resources: Vec, + #[serde(rename = "duration_sec")] + pub duration_sec: Option, + pub status: String, + #[serde(rename = "job_id")] + pub job_id: String, + pub counters: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Counters { + pub outlinks: i64, + pub embeds: i64, +} + +#[derive(Deserialize, Serialize)] +pub struct OptionalString(pub Option); + +impl From for Option { + fn from(val: OptionalString) -> Self { + val.0.map_or_else(|| Some("N/A".to_string()), Some) + } +} + +impl From> for OptionalString { + fn from(val: Option) -> Self { + val.map_or_else(|| Self(Some("N/A".to_string())), |s| Self(Some(s))) + } +} + +#[derive(Deserialize, Serialize)] +pub struct OptionalString2(pub Option); + +impl From for Option { + fn from(val: OptionalString2) -> Self { + val.0.map_or_else( + || Some("N/A".to_string()), + |s| match s.as_str() { + "0" => Some("N/A".to_string()), + x => Some(x.to_string()), + }, + ) + } +} + +impl From> for OptionalString2 { + fn from(val: Option) -> Self { + val.map_or_else(|| Self(Some("N/A".to_string())), |s| Self(Some(s))) + } +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasteResponse { + pub key: String, +} diff --git a/src/lib/types.rs b/src/lib/types.rs new file mode 100644 index 0000000..1111f45 --- /dev/null +++ b/src/lib/types.rs @@ -0,0 +1,4 @@ +use crate::structs::Data; + +pub type Error = Box; +pub type Context<'a> = poise::Context<'a, Data, Error>; diff --git a/src/lib/utils.rs b/src/lib/utils.rs new file mode 100644 index 0000000..c719d63 --- /dev/null +++ b/src/lib/utils.rs @@ -0,0 +1,142 @@ +use crate::args::Args; +use crate::structs::GuildSettings; +use crate::types::Context; +use crate::types::Error; +use crate::vars::REDIS_ADDR; +use clap::Parser; +use tracing::instrument; + +/// Converts a dsicord snowflake to a unix timecode +#[must_use] +pub const fn snowflake_to_unix(id: u128) -> u128 { + const DISCORD_EPOCH: u128 = 1_420_070_400_000; + + ((id >> 22) + DISCORD_EPOCH) / 1000 +} + +/// Quickly checks if the verbose flag was used on launch +#[must_use] +pub fn verbose_mode() -> bool { + let args = Args::parse(); + + args.verbose +} + +/// Open a tokio redis connection +#[cfg(feature = "database")] +#[instrument()] +pub async fn open_redis_connection() -> Result { + let redis_connection = redis::Client::open(REDIS_ADDR)? + .get_tokio_connection() + .await?; + + Ok(redis_connection) +} + +/// Pushes guild settings to DB +#[cfg(feature = "database")] +#[instrument(skip(con))] +pub async fn set_guild_settings( + ctx: Context<'_>, + con: &mut redis::aio::Connection, + settings: GuildSettings, +) -> Result<(), Error> { + let json = serde_json::to_string(&settings).unwrap(); + + let mut pipe = redis::pipe(); + + pipe.cmd("JSON.SET").arg(&[ + format!( + "guild-settings:{}", + ctx.guild_id().expect("Not run inside guild") + ), + "$".to_string(), + json, + ]); + + pipe.atomic().query_async(con).await?; + + Ok(()) +} + +/// Adds the user to a server's auth list in the DB +#[cfg(feature = "database")] +#[instrument(skip(con))] +pub async fn auth( + ctx: Context<'_>, + con: &mut redis::aio::Connection, + uid: String, +) -> Result<(), Error> { + redis::cmd("SADD") + .arg(&[ + format!( + "authed-server-users:{}", + ctx.guild_id().expect("Not run inside guild") + ), + uid, + ]) + .query_async(con) + .await?; + + Ok(()) +} + +/// Increases the total commands run count in the DB +#[cfg(feature = "database")] +#[instrument] +pub async fn inc_execution_count() -> Result<(), Error> { + let mut con = open_redis_connection().await?; + + // increment status:commands-executed in redis DB + redis::cmd("INCR") + .arg("status:commands-executed") + .query_async(&mut con) + .await?; + + Ok(()) +} + +#[cfg(feature = "database")] +#[instrument] +pub async fn is_uid_valid_user(uid: u64, ctx: &Context<'_>) -> anyhow::Result { + let u_opt: Option = + match poise::serenity_prelude::UserId::from(uid) + .to_user(ctx) + .await + { + Ok(user) => Some(user), + Err(error) => { + if verbose_mode() { + ctx.say(format!( + "ID must be a user ID, make sure you coppied the right one! Error: {:?}", + error + )) + .await?; + } else { + ctx.say("ID must be a user ID, make sure you coppied the right one!") + .await?; + } + + None + } + }; + + Ok(u_opt.is_some()) +} + +#[cfg(test)] +mod utils_tests { + + use crate::utils::{snowflake_to_unix, verbose_mode}; + + #[test] + fn snowflake_unix_test() { + assert_eq!(snowflake_to_unix(383_507_911_160_233_985), 1_511_505_811); + } + + #[test] + fn verbose_mode_test() { + // Inverting output since verbose mode is disabled by default + assert!(!verbose_mode()); + } +} diff --git a/src/lib/vars.rs b/src/lib/vars.rs new file mode 100644 index 0000000..482f07b --- /dev/null +++ b/src/lib/vars.rs @@ -0,0 +1,90 @@ +pub const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); + +pub const HELP_EXTRA_TEXT: &str = "Find the documentation website at https://fbtsecurity.fbtheaven.com/\nRun the About command to find out more (/about)"; + +// TODO: change this list to your own bot admin user IDs + +// You need to increase the number in [u64; X] so rust knows the limit of the array +pub const BOT_ADMINS: [u64; 6] = [ + 212_132_817_017_110_528, + 288_186_677_967_585_280, + 211_027_317_068_136_448, + 383_507_911_160_233_985, + 168_600_506_233_651_201, + 231_482_341_921_521_664, +]; // Azuki, Komi, Xeno, Mojo, Ellie, Wundie + +// TODO: you can mass replace the name of this variable easily +// TODO: change to your own guild ID + +pub const FBT_GUILD_ID: u64 = 737_168_134_502_350_849; // FBT's guild ID + +// TODO: this is the channel wehre the feedback command sends it's response for you to read +pub const FEEDBACK_CHANNEL_ID: u64 = 925_599_477_283_311_636; + +//pub const FBT_GUILD_ID: u64 = 838658675916275722; // My test server ID + +// TODO: you need your own Redis DB, this is where you put in the login details and adress of the DB +// format: "redis://USERNAME:PASSWORD@ADDRESS:PORT/DB_INDEX" + +#[cfg(feature = "database")] +pub const REDIS_ADDR: &str = + "redis://:ForSureARealRedisPassword@google.com:6379/0"; + +// TODO: change to your own Meilisearch address +#[cfg(feature = "database")] +pub const MEILISEARCH_HOST: &str = "http://google.com:7777"; + +// TODO: change to your own Meilisearch API key +#[cfg(feature = "database")] +pub const MEILISEARCH_API_KEY: &str = "why-so-strange"; + +// TODO: change to your own bot token +pub const BOT_TOKEN: &str = + "not touching this <3"; + +//TODO: these are popular discord bots, used to ignore their messages and stuff +// Part of blacklist for now but I should add it as a check to the excel command too +#[cfg(feature = "database")] +pub const BOT_IDS: [u64; 22] = [ + 134_133_271_750_639_616, + 155_149_108_183_695_360, + 159_985_870_458_322_944, + 159_985_870_458_322_944, + 184_405_311_681_986_560, + 204_255_221_017_214_977, + 216_437_513_709_944_832, + 235_088_799_074_484_224, + 235_148_962_103_951_360, + 294_882_584_201_003_009, + 351_227_880_153_546_754, + 375_805_687_529_209_857, + 537_429_661_139_861_504, + 550_613_223_733_329_920, + 559_426_966_151_757_824, + 583_995_825_269_768_211, + 625_588_618_525_802_507, + 649_535_344_236_167_212, + 743_269_383_438_073_856, + 743_269_383_438_073_856, + 887_914_294_988_140_565, + 935_372_708_089_315_369, +]; + +// TODO: this is for the ticket system, change to your own ticket category ID. +// it creates new threads in TICKET_CATEGORY and moves them to CLOSED_TICKET_CATEGORY once closed +pub const TICKET_CATEGORY: u64 = 982_769_870_259_240_981; +pub const CLOSED_TICKET_CATEGORY: u64 = 983_228_142_107_918_336; + +#[cfg(feature = "database")] +#[derive(Debug, poise::ChoiceParameter)] +pub enum BlacklistOutput { + #[name = "Chat - Output resulting @, ID and Reasons to chat"] + Chat, + #[name = "Compact Chat - Only send resulting @ and IDs"] + CompactChat, + #[name = "CSV - Output all relevant info as a single .csv file"] + Csv, + #[name = "Json - Output all relevant info as a single .json file"] + Json, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1901fe5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,464 @@ +#![forbid(unsafe_code)] + +//! Please increade the version in the Cargo.toml file by 0.0.1 for &&every minor commit or command and by 0.1.0 for any majoy function rewrite or implamentation + +// TODO: Add ticket ssytem +// ? /close_ticket could check a DB list to see if it contains the channel ID and if it does then close? +// ? If we wan't more info we can store each ticket as a json file and then only close if an entry for the channel exists in DB + +use clap::Parser; +use colored::Colorize; +use commands::database::remove_guild; +use poise::builtins::register_application_commands_buttons; +use poise::serenity_prelude::{self as serenity, ChannelId, Colour, UserId}; +use serenity::model::gateway::Activity; +use serenity::model::user::OnlineStatus; +use std::collections::HashSet; +use std::fs::File; +use tracing::instrument; +use tracing::metadata::LevelFilter; +use tracing::{event, Level}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer}; + +#[macro_use] +extern crate maplit; + +#[cfg(feature = "database")] +use rusted_fbt_lib::utils::open_redis_connection; +use rusted_fbt_lib::vars::FBT_GUILD_ID; +// Import everything from the commands folder +mod commands; +use commands::admin::{ + announcement, authorize, ban, botmsg, request_setup, setup, shutdown, toggle_kick, +}; +#[cfg(feature = "database")] +use commands::database::{ + add, excel, footprint_lookup, key, search, update_search_engine, whitelist, +}; +use commands::fun::{cringe, owo, ping, pog, toxic, waifu}; +use commands::info::{about, feedback, help}; +use commands::tickets::{close_ticket, new_ticket}; +use commands::tools::{account_age, bot_owner_tool_1, creation_date}; + +// New rust librabry to never leave this reposity :D +use rusted_fbt_lib::args::Args; +use rusted_fbt_lib::checks::bot_admin_check; +use rusted_fbt_lib::enums::{DebugLevel, LogDebugLevel}; +use rusted_fbt_lib::event_handlers::{ + alt_kicker, bl_warner, handle_dms, handle_msg_delete, handle_msg_edit, handle_resume, +}; +use rusted_fbt_lib::memes::pog_be_gone; +use rusted_fbt_lib::structs::{Data, PasteResponse}; +use rusted_fbt_lib::types::{Context, Error}; +use rusted_fbt_lib::utils::inc_execution_count; + +use crate::commands::tools::invite_info; + +/// Register application commands in this guild or globally +/// +/// Run with no arguments to register in guild, run with argument "global" to register globally. +#[instrument(skip(ctx))] +#[poise::command(prefix_command, slash_command, hide_in_help, owners_only)] +async fn register(ctx: Context<'_>) -> Result<(), Error> { + register_application_commands_buttons(ctx).await?; + event!(Level::INFO, "Commandwhere registered"); + Ok(()) +} + +/// Custom error handeling +#[instrument] +async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { + // This is our custom error handler + // They are many errors that can occur, so we only handle the ones we want to customize + // and forward the rest to the default handler + match error { + // allow unused_variables because we don't use all the variables + #[allow(unused_variables)] + poise::FrameworkError::Setup { + error, + framework, + data_about_bot, + ctx, + } => { + // Log failed bot setup + event!(Level::ERROR, "Bot setup failed" = ?error); + } + poise::FrameworkError::Command { error, ctx } => { + event!(Level::WARN, "Error in command" = ?ctx.command(), "error" = ?error); + } + poise::FrameworkError::CommandCheckFailed { error, ctx } => { + if ctx.command().name.as_str() == "setup" { + ctx.send(|m| { + m.content("If you can't run this because you don't have the correct permissions then please ask a local admin to run `/request_setup` or `/authorize`!\nAn admin will come and check out your server ASAP after `/request_setup` is executed.") + .ephemeral(true) + }).await + .expect("Failed to tell user about request_setup during error handeling"); + } + + event!(Level::INFO, "CommandCheckFailed" = ?ctx.command(), "error" = ?error); + } + poise::FrameworkError::MissingBotPermissions { + missing_permissions, + ctx, + } => { + ctx.say(format!("I'm currently missing the follow permission(s) required to execute this command:\n\n```{}```\n\nPlease ask a local server admin to fix this in my bot role!", missing_permissions.get_permission_names().join("\n"))).await + .expect("Unable to tell a server what permissions I am missing!"); + } + poise::FrameworkError::CooldownHit { + remaining_cooldown, + ctx, + } => { + ctx.send(|m| { + m.content(format!( + "You are on cooldown try again in {} seconds, moron!", + remaining_cooldown.as_secs() + )) + .ephemeral(true) + }) + .await + .expect("Failed to meme on someone for running a command while on cooldown"); + } + error => { + if let Err(e) = poise::builtins::on_error(error).await { + event!(Level::WARN, info = "Error while handling error (ironic)", error = ?e); + } + } + } +} + +/// Handle events here, should move anything that isn't just println! to a seperate function to avoid the mess that was `on_message` in the python version +#[instrument(skip(ctx, _framework, event, _user_data))] +async fn event_listener( + ctx: &serenity::Context, + event: &poise::Event<'_>, + _framework: poise::FrameworkContext<'_, Data, Error>, + _user_data: &Data, +) -> Result<(), Error> { + match event { + poise::Event::Ready { data_about_bot } => { + println!( + "{} {}{}", + "Bot is now online as".color("Purple"), + data_about_bot.user.name.bright_cyan().bold().underline(), + "!".color("Purple") + ); + + let activity = Activity::playing( + "use /help to see all commands. /request_setup to request extra admin features.", + ); + let status = OnlineStatus::Online; + + // TODO: Store and get from DB so we can change it later and keep it consistent between boots + ctx.set_presence(Some(activity), status).await; + } + poise::Event::Resume { event } => { + handle_resume(event); + } + poise::Event::Message { new_message } => { + if new_message.is_private() { + handle_dms(new_message, ctx).await?; + } else { + pog_be_gone(new_message, ctx).await?; + } + } + poise::Event::GuildMemberAddition { new_member } => { + #[cfg(feature = "database")] + alt_kicker(ctx, new_member).await?; + + #[cfg(feature = "database")] + bl_warner(ctx, new_member).await?; + } + poise::Event::MessageDelete { + channel_id, + deleted_message_id, + guild_id, + } => { + handle_msg_delete(guild_id, ctx, channel_id, deleted_message_id).await?; + } + poise::Event::MessageUpdate { + old_if_available, + new, + event, + } => { + if let Some(n) = new.clone() { + // TODO: put your own guild ID here, this is for tracking message edits + if !n.is_private() && *event.guild_id.unwrap().as_u64() == 737_168_134_502_350_849 { + // I need to learn the overall benifit to using `if let` + handle_msg_edit(event.clone(), old_if_available, ctx, new).await?; + } + } + } + poise::Event::ChannelDelete { channel } => { + if *channel.guild_id.as_u64() == FBT_GUILD_ID { + let messages = ctx.cache.channel_messages_field(channel.id.0, |s| { + s.filter_map(|m| { + if m.channel_id.0 == channel.id.0 { + Some(m.clone()) + } else { + None + } + }) + .collect::>() + }); + + // let messages = match channel.messages(ctx, |b| b.limit(100)).await { + // Ok(vec) => format!("{:?}", vec), + // Err(e) => format!("{:?}", e), + // }; + + let shit_list = format!( + "Stored channel info:\n\n{:?}\n\nLast 100 messages stored in cache:\n\n{:#?}", + channel, messages + ); + + let client = reqwest::Client::new(); + + // TODO: I setup a custom paste bin here uhh you can figure out how to reaplce it or just comment out the poise::Event::ChannelDelete event + + let response = client + .post("https://paste.buymymojo.net/documents") + .body(shit_list) + .header("Accept", "application/json") + .send() + .await?; + + let response_content = response + .text() + .await + .unwrap_or_else(|_| "{'key': 'nope<3'}".to_string()); + + let id: PasteResponse = serde_json::from_str(&response_content)?; + + // TODO: channel to alert users that a channel has been deleted + + ChannelId(891_294_507_923_025_951) + .send_message(ctx.http.clone(), |f| { + f.embed(|e| { + e.title("A channel has been deleted".to_string()) + .field( + "The last cached info from the channel:", + format!("https://paste.buymymojo.net/{}", id.key), + false, + ) + .color(Colour::new(0x00ED_4245)) + }) + }) + .await?; + } + } + poise::Event::CacheReady { guilds } => { + let args = Args::parse(); + + if args.print_guild_cache { + event!(Level::INFO, "Cache is ready"); + } else { + event!(Level::INFO, info = "Cache is ready", guilds = ?guilds); + } + } + _ => {} + } + + Ok(()) +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + let console_level = match args.debug { + DebugLevel::Off => LevelFilter::ERROR, + DebugLevel::Some => LevelFilter::WARN, + DebugLevel::Most => LevelFilter::INFO, + DebugLevel::All => LevelFilter::TRACE, + }; + + let file_level = match args.debug_log { + LogDebugLevel::Most => LevelFilter::DEBUG, + LogDebugLevel::All => LevelFilter::TRACE, + }; + + let console_layer = tracing_subscriber::fmt::layer() + .with_line_number(true) + .with_ansi(true) + .with_thread_names(true) + .with_target(true) + .with_filter(console_level); + let file_layer = if args.debug.enabled() { + match File::create( + std::path::Path::new(&std::env::current_dir().unwrap()).join(format!( + "./{}_rusted-fbt.verbose.log", + chrono::offset::Local::now().timestamp() + )), + ) { + Ok(handle) => { + let file_log = tracing_subscriber::fmt::layer() + .with_line_number(true) + .with_ansi(false) + .with_thread_names(true) + .with_target(true) + .with_writer(handle) + .with_filter(file_level); + Some(file_log) + } + Err(why) => { + eprintln!("ERROR!: Unable to create log output file: {why:?}"); + None + } + } + } else { + None + }; + + let info_file_layer = if args.debug.enabled() { + match File::create( + std::path::Path::new(&std::env::current_dir().unwrap()).join(format!( + "./{}_rusted-fbt.info.log", + chrono::offset::Local::now().timestamp() + )), + ) { + Ok(handle) => { + let file_log = tracing_subscriber::fmt::layer() + .with_line_number(true) + .with_ansi(false) + .with_thread_names(true) + .with_target(true) + .with_writer(handle) + .with_filter(LevelFilter::INFO); + Some(file_log) + } + Err(why) => { + eprintln!("ERROR!: Unable to create log output file: {why:?}"); + None + } + } + } else { + None + }; + + tracing_subscriber::registry() + .with(console_layer) + .with(file_layer) + .with(info_file_layer) + .init(); + + // TODO: Like bot admins, put your own IDs here + + let bot_owners: HashSet = hashset! { + UserId::from(212_132_817_017_110_528), // Azuki + UserId::from(164_694_510_947_794_944), // Cross + UserId::from(383_507_911_160_233_985), // Mojo + }; + + // * This is where we put the functions that we want in discord + #[allow(unused_mut)] + let mut discord_commands = vec![ + about(), + account_age(), + ban(), + botmsg(), + creation_date(), + cringe(), + help(), + owo(), + ping(), + pog(), + register(), + shutdown(), + toxic(), + waifu(), + new_ticket(), + close_ticket(), + bot_owner_tool_1(), + ]; + + // * Any command that requires the DB goes here + #[cfg(feature = "database")] + { + let mut db_vec = vec![ + add(), + announcement(), + authorize(), + footprint_lookup(), + excel(), + feedback(), + remove_guild(), + whitelist(), + request_setup(), + search(), + setup(), + // sqlite_transfer(), // Deprecated + toggle_kick(), + update_search_engine(), + key(), + invite_info(), + ]; + + discord_commands.append(&mut db_vec); + } + + // * Any command that are not complete/working here + #[cfg(feature = "beta")] + { + let mut beta_vec = vec![]; + + discord_commands.append(&mut beta_vec); + } + + let framework = poise::Framework::builder() + .options(poise::FrameworkOptions { + commands: discord_commands, + prefix_options: poise::PrefixFrameworkOptions { + prefix: Some(args.prefix), + ..Default::default() + }, + // The global error handler for all error cases that may occur + on_error: |error| Box::pin(on_error(error)), + event_handler: |ctx, event, framework, user_data| { + Box::pin(event_listener(ctx, event, framework, user_data)) + }, + owners: bot_owners, + // Every command invocation must pass this check to continue execution + #[cfg(feature = "database")] + command_check: Some(|ctx| { + Box::pin(async move { + if bot_admin_check(ctx).await.unwrap() { + return Ok(true); + } + + let mut con = open_redis_connection().await?; + + let key_list: HashSet = redis::cmd("SMEMBERS") + .arg("user-lists:banned-from-bot") + .clone() + .query_async(&mut con) + .await?; + + if key_list.contains(&format!("{}", ctx.author().id.as_u64())) { + Ok({ + println!( + "{}/{} was blocked from using the bot", + ctx.author().id, + ctx.author().name + ); + false + }) + } else { + Ok(true) + } + }) + }), + #[cfg(feature = "database")] + post_command: |_ctx| { + Box::pin(async move { + inc_execution_count().await.expect(""); + }) + }, + ..Default::default() + }) + .token(args.token) + .intents(serenity::GatewayIntents::all()) + .setup(move |_ctx, _ready, _framework| Box::pin(async move { Ok(Data {}) })) + .client_settings(|f| f.cache_settings(|cs| cs.max_messages(5_000))); + + framework.run_autosharded().await.unwrap(); +} diff --git a/westmere.Dockerfile b/westmere.Dockerfile new file mode 100644 index 0000000..db54609 --- /dev/null +++ b/westmere.Dockerfile @@ -0,0 +1,15 @@ +FROM rust:latest as builder +WORKDIR /usr/src/rusted-fbt +COPY . . +RUN env RUSTFLAGS="-C target-cpu=westmere" cargo install --path . + +FROM ubuntu:23.10 +# RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/* +COPY --from=builder /usr/local/cargo/bin/rusted-fbt /usr/local/bin/rusted-fbt +RUN apt update -y +RUN apt install ca-certificates -y +RUN apt update -y +# This is the only dependancie missing as far as I can tell which is great +RUN apt install libssl-dev -y +RUN apt autoremove -y +CMD ["rusted-fbt"] \ No newline at end of file diff --git a/znver3.Dockerfile b/znver3.Dockerfile new file mode 100644 index 0000000..f5ac552 --- /dev/null +++ b/znver3.Dockerfile @@ -0,0 +1,14 @@ +FROM rust:latest as builder +WORKDIR /usr/src/rusted-fbt +COPY . . +RUN env RUSTFLAGS="-C target-cpu=znver3" cargo install --path . + +FROM debian:buster-slim +# RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/* +COPY --from=builder /usr/local/cargo/bin/rusted-fbt /usr/local/bin/rusted-fbt +run apt update -y +run apt install ca-certificates -y +run apt update -y +# This is the only dependancie missing as far as I can tell which is great +run apt install libssl-dev -y +CMD ["rusted-fbt"] \ No newline at end of file