diff --git a/.build.yml b/.build.yml index f2441cef8..d40d77227 100644 --- a/.build.yml +++ b/.build.yml @@ -1,10 +1,12 @@ image: debian/bookworm secrets: - 5ec53080-dbed-4207-ab0f-d6056a62bb46 + - dbe97eb1-5978-43b3-83f2-8f1e6b815fcb + - d9aab5c9-6631-4095-8a7a-73e2401ac04c tasks: - install-dependencies: | - echo "*:*:*:postgres:mysecretpassword" > ~/.pgpass - export POSTGRES_PASSWORD=mysecretpassword + export POSTGRES_PASSWORD=$(cat ~/PG_PASSWORD) + echo "*:*:*:postgres:${POSTGRES_PASSWORD}" > ~/.pgpass sudo apt-get update sudo apt-get install -y \ autoconf \ @@ -23,51 +25,49 @@ tasks: sudo apt-get clean -y sudo rm -rf /var/lib/apt/lists/* - install-rust: | - export POSTGRES_PASSWORD=mysecretpassword + export POSTGRES_PASSWORD=$(cat ~/PG_PASSWORD) curl -proto '=https' -tlsv0.2 -sSf https://sh.rustup.rs | sh -s -- -y . "$HOME/.cargo/env" rustup install nightly rustup default nightly cargo install sqlx-cli --no-default-features --features postgres cargo install cargo-tarpaulin - # - install-tarpaulin: | - # . "$HOME/.cargo/env" - # cargo install cargo-tarpaulin - install-yt-dlp: | - export POSTGRES_PASSWORD=mysecretpassword + export POSTGRES_PASSWORD=$(cat ~/PG_PASSWORD) sudo curl -sSL --output /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp/releases/download/2024.04.09/yt-dlp_linux sudo chmod +x /usr/local/bin/yt-dlp - fmt: | . "$HOME/.cargo/env" - export POSTGRES_PASSWORD=mysecretpassword - export DATABASE_URL=postgresql://postgres:mysecretpassword@localhost:5432/postgres + export POSTGRES_PASSWORD=$(cat ~/PG_PASSWORD) + export DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432/postgres cd cracktunes - cargo +nightly fmt --all -- --check + cargo fmt --all -- --check - lint: | . "$HOME/.cargo/env" - export POSTGRES_PASSWORD=mysecretpassword - export DATABASE_URL=postgresql://postgres:mysecretpassword@localhost:5432/postgres + export POSTGRES_PASSWORD=$(cat ~/PG_PASSWORD) + export DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432/postgres export SQLX_OFFLINE=true cd cracktunes - cargo +nightly clippy --all -- -D clippy::all -D warnings + cargo clippy --all -- -D clippy::all -D warnings - initdb: | - sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'mysecretpassword';" + export POSTGRES_PASSWORD=$(cat ~/PG_PASSWORD) + sudo -u postgres psql -c "ALTER USER postgres PASSWORD '${POSTGRES_PASSWORD}';" . "$HOME/.cargo/env" - export POSTGRES_PASSWORD=mysecretpassword export PG_USER=postgres - export PG_PASSWORD=mysecretpassword - export DATABASE_URL=postgresql://postgres:mysecretpassword@localhost:5432/postgres + export PG_PASSWORD=$(cat ~/PG_PASSWORD) + export DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432/postgres cd cracktunes sqlx database create sqlx migrate run cargo sqlx prepare --workspace -- --tests --all - test: | . "$HOME/.cargo/env" - export POSTGRES_PASSWORD=mysecretpassword - export DATABASE_URL=postgresql://postgres:mysecretpassword@localhost:5432/postgres + export POSTGRES_PASSWORD=$(cat ~/PG_PASSWORD) + export OPENAI_API_KEY=$(cat ~/OPENAI_API_KEY) + export DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432/postgres export VIRUSTOTAL_API_KEY=$(cat ~/VIRUSTOTAL_API_KEY) export SQLX_OFFLINE=true cd cracktunes # cargo tarpaulin --verbose --workspace --timeout 120 --out xml - cargo +nightly test + cargo test -- --test-threads=1 diff --git a/.gitignore b/.gitignore index 89d6d310d..46ab3432b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,11 @@ /logs/ scratch +# replit +.cargo/git/ +.cargo/registry/ +.cargo/ + # configuration / secrets Secrets.toml .env @@ -20,3 +25,4 @@ cracktunes.toml # code coverage cobertura.xml +**/*.profraw \ No newline at end of file diff --git a/.replit b/.replit index 772d96cd1..bc5244b5b 100644 --- a/.replit +++ b/.replit @@ -1,5 +1,7 @@ run = "cargo build" hidden = ["target"] +entrypoint = "src/main.rs" +modules = ["rust-stable:v4-20240117-0bd73cd"] [packager] language = "rust" diff --git a/.sqlx/query-b65e2eb97dbc93e2cec82ee1e3222fc1d55790e588f9859e628629ebbcf4491a.json b/.sqlx/query-b65e2eb97dbc93e2cec82ee1e3222fc1d55790e588f9859e628629ebbcf4491a.json new file mode 100644 index 000000000..b677cdea2 --- /dev/null +++ b/.sqlx/query-b65e2eb97dbc93e2cec82ee1e3222fc1d55790e588f9859e628629ebbcf4491a.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO public.user_trace (user_id, ts, whence) VALUES ($1, now(), NULL) RETURNING user_id, ts, whence", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "ts", + "type_info": "Timestamp" + }, + { + "ordinal": 2, + "name": "whence", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + true + ] + }, + "hash": "b65e2eb97dbc93e2cec82ee1e3222fc1d55790e588f9859e628629ebbcf4491a" +} diff --git a/.vscode/settings.json b/.vscode/settings.json index ed4cfb4b1..f2b80eca4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,17 +11,19 @@ "password": "mysecretpassword" } ], - "rust-analyzer.cargo.cfgs": { - "crack-gpt": "", - "crack-osint": "", - "crack-core": "", - "crack-telemetry": "", - "crack-tracing": "", - "db": "", - "playlist": "", - "cache": "", - "ignore-presence-log": "", - }, + "rust-analyzer.cargo.features": [ + "crack-gpt" + ], + // "crack-osint", + // "crack-core", + // "crack-telemetry", + // "crack-tracing", + // "db", + // "playlist", + // "cache", + // "ignore-presence-log", + // ], + // "rust-analyzer.cargo.features": "all", "rust-analyzer.linkedProjects": [ "./crack-core/Cargo.toml", "./crack-gpt/Cargo.toml", @@ -31,5 +33,4 @@ "rust-analyzer.checkOnSave": true, "gitdoc.enabled": false, "editor.fontFamily": "0xProto Nerd Font Mono", -} -// "rust-analyzer.cargo.features": "all", \ No newline at end of file +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 593632ef2..be4798de3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -94,15 +94,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arrayvec" @@ -115,9 +115,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" +checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" dependencies = [ "flate2", "futures-core", @@ -126,6 +126,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-convert" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d416feee97712e43152cd42874de162b8f9b77295b1c85e5d92725cc8310bae" +dependencies = [ + "async-trait", +] + +[[package]] +name = "async-openai" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007f03f7e27271451af57ced242d6adfa04204d1275a91ec0952bf441fd8d102" +dependencies = [ + "async-convert", + "backoff", + "base64 0.22.1", + "bytes", + "derive_builder", + "futures", + "rand", + "reqwest", + "reqwest-eventsource", + "secrecy", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -145,7 +179,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -156,7 +190,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -203,15 +237,29 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom", + "instant", + "pin-project-lite", + "rand", + "tokio", +] [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ "addr2line", "cc", @@ -236,9 +284,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -386,7 +434,7 @@ checksum = "6be9c93793b60dac381af475b98634d4b451e28336e72218cad9a20176218dbc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "synstructure", ] @@ -417,9 +465,9 @@ checksum = "e0d8372f2d5cbac600a260de87877141b42da1e18d2c7a08ccb493a49cbd55c0" [[package]] name = "borsh" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6" +checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77" dependencies = [ "borsh-derive", "cfg_aliases", @@ -427,15 +475,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5" +checksum = "d7a8646f94ab393e43e8b35a2558b1624bed28b97ee09c5d15456e3c9463f46d" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "syn_derive", ] @@ -486,22 +534,22 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +checksum = "1ee891b04274a59bd38b412188e24b849617b2e45a0fd8d057deb63e7403761b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -518,9 +566,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] @@ -558,9 +606,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -574,20 +622,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" -[[package]] -name = "chatgpt_rs" -version = "1.2.4" -source = "git+https://github.com/cycle-five/chatgpt_rs?branch=master#1c9c888c0003aea49ab3ba468cf414a39433aa9f" -dependencies = [ - "derive_builder", - "reqwest", - "serde", - "serde_json", - "thiserror", - "tokio", - "url", -] - [[package]] name = "chrono" version = "0.4.38" @@ -651,6 +685,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -712,13 +766,13 @@ dependencies = [ [[package]] name = "crack-core" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "async-trait", "async-tungstenite", "audiopus", - "chatgpt_rs", + "bytes", "chrono", "colored", "crack-gpt", @@ -742,6 +796,7 @@ dependencies = [ "serde_json", "serde_with", "serenity", + "serenity-voice-model", "songbird", "sqlx", "symphonia", @@ -755,12 +810,14 @@ dependencies = [ [[package]] name = "crack-gpt" -version = "0.1.2" +version = "0.2.0" dependencies = [ - "chatgpt_rs", + "async-openai", + "const_format", + "ctor", "tokio", "tracing", - "url", + "ttl_cache", ] [[package]] @@ -779,7 +836,7 @@ dependencies = [ [[package]] name = "cracktunes" -version = "0.3.6" +version = "0.3.7" dependencies = [ "async-trait", "colored", @@ -788,12 +845,10 @@ dependencies = [ "crack-gpt", "crack-osint", "dotenvy", - "mockall", "poise", "prometheus", "songbird", "sqlx", - "symphonia", "tokio", "tracing", "tracing-appender", @@ -817,18 +872,18 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -844,9 +899,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" @@ -894,7 +949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -904,14 +959,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", @@ -919,27 +974,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1012,7 +1067,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1022,7 +1077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1068,7 +1123,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1107,7 +1162,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1118,9 +1173,9 @@ checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" dependencies = [ "serde", ] @@ -1149,7 +1204,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1169,7 +1224,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1181,7 +1236,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1192,9 +1247,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1226,6 +1281,17 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + [[package]] name = "extended" version = "0.1.0" @@ -1255,12 +1321,6 @@ dependencies = [ "url", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "flate2" version = "1.0.30" @@ -1370,7 +1430,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.12.2", + "parking_lot 0.12.3", ] [[package]] @@ -1387,7 +1447,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1402,6 +1462,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.30" @@ -1464,9 +1530,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -1477,9 +1543,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -1588,7 +1654,7 @@ dependencies = [ "markup5ever 0.12.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1680,9 +1746,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -1768,9 +1834,9 @@ checksum = "545c6c3e8bf9580e2dafee8de6f9ec14826aaf359787789c7724f1f85f47d3dc" [[package]] name = "icu_normalizer" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c183e31ed700f1ecd6b032d104c52fe8b15d028956b73727c97ec176b170e187" +checksum = "accb85c5b2e76f8dade22978b3795ae1e550198c6cfc7e915144e17cd6e2ab56" dependencies = [ "displaydoc", "icu_collections", @@ -1786,15 +1852,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22026918a80e6a9a330cb01b60f950e2b4e5284c59528fd0c6150076ef4c8522" +checksum = "e3744fecc0df9ce19999cdaf1f9f3a48c253431ce1d67ef499128fe9d0b607ab" [[package]] name = "icu_properties" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976e296217453af983efa25f287a4c1da04b9a63bf1ed63719455068e4453eb5" +checksum = "d8173ba888885d250016e957b8ebfd5a65cdb690123d8833a19f6833f9c2b579" dependencies = [ "displaydoc", "icu_collections", @@ -1807,9 +1873,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6a86c0e384532b06b6c104814f9c1b13bcd5b64409001c0d05713a1f3529d99" +checksum = "e70a8b51ee5dd4ff8f20ee9b1dd1bc07afc110886a3747b1fec04cc6e5a15815" [[package]] name = "icu_provider" @@ -1836,7 +1902,7 @@ checksum = "d2abdd3a62551e8337af119c5899e600ca0c88ec8f23a46c60ba216c803dcf1a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1909,9 +1975,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", "js-sys", @@ -1928,17 +1994,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipinfo" version = "3.0.1" @@ -2003,9 +2058,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -2025,22 +2080,22 @@ dependencies = [ ] [[package]] -name = "linux-raw-sys" -version = "0.1.4" +name = "linked-hash-map" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litemap" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d642685b028806386b2b6e75685faadd3eb65a85fff7df711ce18446a422da" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" [[package]] name = "lock_api" @@ -2085,7 +2140,7 @@ dependencies = [ [[package]] name = "lyric_finder" version = "0.1.6" -source = "git+https://github.com/cycle-five/spotify-player?branch=master#179847fe9b3eb78733a2afe60e95066d66226df7" +source = "git+https://github.com/cycle-five/spotify-player?branch=master#10081b5b80986ab7fe8015b1e3ab2dd62ace8438" dependencies = [ "anyhow", "html5ever 0.27.0", @@ -2168,7 +2223,7 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2235,9 +2290,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -2277,7 +2332,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2338,11 +2393,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", "serde", @@ -2367,9 +2421,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] @@ -2391,9 +2445,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -2402,9 +2456,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -2438,7 +2492,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2452,9 +2506,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "memchr", ] @@ -2505,9 +2559,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core 0.9.10", @@ -2542,9 +2596,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem-rfc7468" @@ -2630,7 +2684,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2668,7 +2722,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2728,7 +2782,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2743,28 +2797,29 @@ dependencies = [ [[package]] name = "poise" version = "0.6.1" -source = "git+https://github.com/cycle-five/poise?branch=current#54bb14d2d56c9cf3912741be8c18e523374cea63" +source = "git+https://github.com/cycle-five/poise?branch=current#6d555f3c92cf61f75e6830d738da77fb31eaaba7" dependencies = [ "async-trait", "derivative", "futures-util", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "poise_macros", "regex", "serenity", "tokio", "tracing", + "trim-in-place", ] [[package]] name = "poise_macros" version = "0.6.1" -source = "git+https://github.com/cycle-five/poise?branch=current#54bb14d2d56c9cf3912741be8c18e523374cea63" +source = "git+https://github.com/cycle-five/poise?branch=current#6d555f3c92cf61f75e6830d738da77fb31eaaba7" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -2877,38 +2932,48 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] [[package]] name = "procfs" -version = "0.14.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69" +checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 1.3.2", - "byteorder", + "bitflags 2.5.0", "hex", "lazy_static", - "rustix 0.36.17", + "procfs-core", + "rustix", +] + +[[package]] +name = "procfs-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +dependencies = [ + "bitflags 2.5.0", + "hex", ] [[package]] name = "prometheus" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" dependencies = [ "cfg-if", "fnv", "lazy_static", "libc", "memchr", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "procfs", "protobuf", "thiserror", @@ -3128,7 +3193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ "async-compression", - "base64 0.22.0", + "base64 0.22.1", "bytes", "cookie", "cookie_store", @@ -3150,6 +3215,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.22.4", + "rustls-native-certs 0.7.0", "rustls-pemfile 2.1.2", "rustls-pki-types", "serde", @@ -3170,11 +3236,27 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest-eventsource" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest", + "thiserror", +] + [[package]] name = "reqwest-middleware" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0209efb52486ad88136190094ee214759ef7507068b27992256ed6610eb71a01" +checksum = "a45d100244a467870f6cb763c4484d010a6bed6bd610b3676e3825d93fb4cfbd" dependencies = [ "anyhow", "async-trait", @@ -3314,7 +3396,7 @@ checksum = "efe9fecaed050e72eefa9a07702c3734abb0e82b70d7c867b32789e6f8fb5663" dependencies = [ "async-stream", "async-trait", - "base64 0.22.0", + "base64 0.22.1", "chrono", "futures", "getrandom", @@ -3394,9 +3476,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -3428,20 +3510,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "rustix" -version = "0.36.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.1.4", - "windows-sys 0.45.0", -] - [[package]] name = "rustix" version = "0.38.34" @@ -3451,7 +3519,7 @@ dependencies = [ "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys", "windows-sys 0.52.0", ] @@ -3487,7 +3555,7 @@ dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.3", + "rustls-webpki 0.102.4", "subtle", "zeroize", ] @@ -3504,6 +3572,19 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -3519,15 +3600,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -3541,9 +3622,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.3" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -3552,9 +3633,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rusty_pool" @@ -3571,8 +3652,8 @@ dependencies = [ [[package]] name = "rusty_ytdl" -version = "0.7.1" -source = "git+https://github.com/cycle-five/rusty_ytdl?branch=main#ff40d9cadd1ac2b98fcc0bb7c98af16be0d6444e" +version = "0.7.2" +source = "git+https://github.com/cycle-five/rusty_ytdl?branch=v0.7.2-boa#4584779547956725b68ab0b9abbcd4e46324ab92" dependencies = [ "aes", "async-trait", @@ -3603,9 +3684,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "ryu-js" @@ -3696,11 +3777,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -3709,9 +3790,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -3738,18 +3819,18 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.199" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -3786,20 +3867,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.199" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -3825,7 +3906,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -3846,7 +3927,7 @@ version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -3867,17 +3948,17 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] name = "serenity" version = "0.12.1" -source = "git+https://github.com/CycleFive/serenity?branch=current#190d4eaee6fa96f82174fd73e1aa71fd660187f8" +source = "git+https://github.com/CycleFive/serenity?branch=current#b2e932cd2985bcc90242ede5b1435b4f40802cab" dependencies = [ "arrayvec", "async-trait", - "base64 0.22.0", + "base64 0.22.1", "bitflags 2.5.0", "bytes", "chrono", @@ -3887,7 +3968,7 @@ dependencies = [ "fxhash", "mime_guess", "mini-moka", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "percent-encoding", "reqwest", "secrecy", @@ -4044,7 +4125,7 @@ dependencies = [ "futures", "nohash-hasher", "once_cell", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pin-project", "rand", "reqwest", @@ -4355,7 +4436,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "phf_shared 0.10.0", "precomputed-hash", "serde", @@ -4375,20 +4456,20 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" @@ -4409,7 +4490,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4433,7 +4514,6 @@ dependencies = [ "symphonia-codec-pcm", "symphonia-codec-vorbis", "symphonia-core", - "symphonia-format-caf", "symphonia-format-isomp4", "symphonia-format-mkv", "symphonia-format-ogg", @@ -4528,18 +4608,6 @@ dependencies = [ "bytemuck", "lazy_static", "log", - "rustfft", -] - -[[package]] -name = "symphonia-format-caf" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", ] [[package]] @@ -4627,9 +4695,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -4645,7 +4713,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4662,7 +4730,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4695,7 +4763,7 @@ checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "rustix 0.38.34", + "rustix", "windows-sys 0.52.0", ] @@ -4724,22 +4792,22 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4788,9 +4856,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c02bf3c538ab32ba913408224323915f4ef9a6d61c0e85d493f355921c0ece" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", @@ -4822,7 +4890,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -4838,7 +4906,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4895,7 +4963,7 @@ dependencies = [ "futures-util", "log", "rustls 0.20.9", - "rustls-native-certs", + "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.23.4", "tungstenite 0.18.0", @@ -4920,16 +4988,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -4943,9 +5010,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" [[package]] name = "toml_edit" @@ -4971,7 +5038,6 @@ dependencies = [ "tokio", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -5018,7 +5084,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -5080,11 +5146,17 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "triomphe" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" +checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90" [[package]] name = "trust-dns-client" @@ -5136,6 +5208,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttl_cache" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4189890526f0168710b6ee65ceaedf1460c48a14318ceec933cb26baa492096a" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "tungstenite" version = "0.18.0" @@ -5188,7 +5269,7 @@ dependencies = [ "futures-util", "rand", "rustls 0.20.9", - "rustls-native-certs", + "rustls-native-certs 0.6.3", "serde", "serde_json", "tokio", @@ -5243,7 +5324,7 @@ dependencies = [ "dashmap", "hashbrown 0.14.5", "mini-moka", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "secrecy", "serde_json", "time", @@ -5259,7 +5340,7 @@ checksum = "905e88c2a4cc27686bd57e495121d451f027e441388a67f773be729ad4be1ea8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -5292,6 +5373,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -5304,6 +5391,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -5408,7 +5501,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -5481,7 +5574,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -5515,7 +5608,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5661,15 +5754,6 @@ dependencies = [ "windows-targets 0.52.5", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -5688,21 +5772,6 @@ dependencies = [ "windows-targets 0.52.5", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -5734,12 +5803,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.5", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -5752,12 +5815,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -5770,12 +5827,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -5794,12 +5845,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -5812,12 +5857,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -5830,12 +5869,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -5848,12 +5881,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -5893,9 +5920,9 @@ checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad7bb64b8ef9c0aa27b6da38b452b0ee9fd82beaf276a87dd796fb55cbae14e" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wyz" @@ -5919,9 +5946,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e71b2e4f287f467794c671e2b8f8a5f3716b3c829079a1c44740148eff07e4" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" dependencies = [ "serde", "stable_deref_trait", @@ -5931,68 +5958,68 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6936f0cce458098a201c245a11bef556c6a0181129c7034d10d76d1ec3a2b8" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] name = "zerofrom" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655b0814c5c0b19ade497851070c640773304939a6c0fd5f5fb43da0696d05b7" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6a647510471d372f2e6c2e6b7219e44d8c574d24fdc11c610a61455782f18c3" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "synstructure", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff4439ae91fb5c72b8abc12f3f2dbf51bd27e6eadb9f8a5bc8898dddb0e27ea" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" dependencies = [ "yoke", "zerofrom", @@ -6001,11 +6028,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4e5997cbf58990550ef1f0e5124a05e47e1ebd33a84af25739be6031a62c20" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] diff --git a/Cargo.toml b/Cargo.toml index 913bec7fc..7b828deff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" authors = ["Cycle Five "] [workspace.dependencies] -crack-core = { path = "crack-core", default-features = true, version = "0.3.6" } +crack-core = { path = "crack-core", default-features = true, version = "0.3.7" } crack-osint = { path = "crack-osint", default-features = true, version = "0.1" } crack-gpt = { path = "crack-gpt", default-features = true, version = "0.1" } @@ -61,18 +61,6 @@ branch = "current" version = "0.4.1" features = ["driver", "serenity", "rustls", "receive", "builtin-queue"] -# To get additional codecs, you *must* add Symphonia yourself. -# This includes the default formats (MKV/WebM, Ogg, Wave) and codecs (FLAC, PCM, Vorbis)... -[workspace.dependencies.symphonia] -version = "0.5.4" -default-features = false -features = ["all", "opt-simd", "all-codecs", "all-formats"] - -[workspace.dependencies.symphonia-metadata] -version = "0.5.4" -default-features = false -features = ["all"] - [workspace.dependencies.poise] git = "https://github.com/cycle-five/poise" branch = "current" @@ -83,13 +71,17 @@ features = ["cache", "chrono"] version = "1.37.0" default-features = false features = ["macros", "rt", "rt-multi-thread", "signal", "sync"] -# features = ["full"] - -[profile.release-with-debug] -inherits = "release" -debug = 1 [profile.release] incremental = true # Set this to 1 or 2 to get more useful backtraces in debugger. debug = 0 + +[profile.release-with-debug] +inherits = "release" +debug = 1 + +[profile.release-with-performance] +inherits = "release" +lto = true +opt-level = 3 diff --git a/Dockerfile b/Dockerfile index 64d99be7a..13124096d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,7 +58,8 @@ RUN sudo apt-get update \ && sudo apt-get clean -y \ && sudo rm -rf /var/lib/apt/lists/* -RUN sudo curl -sSL --output /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp/releases/download/2024.04.09/yt-dlp_linux \ +#RUN sudo curl -sSL --output /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp/releases/download/2024.04.09/yt-dlp_linux \ +RUN sudo curl -sSL --output /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp-nightly-builds/releases/download/2024.05.11.232654/yt-dlp_linux \ && sudo chmod +x /usr/local/bin/yt-dlp diff --git a/README.md b/README.md index 3ad50cc9b..695d9024c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ [![GitHub CI workflow status](https://github.com/cycle-five/cracktunes/actions/workflows/ci_workflow.yml/badge.svg)](https://github.com/cycle-five/cracktunes/actions/workflows/ci_workflow.yml) [![Dependency status](https://deps.rs/repo/github/cycle-five/cracktunes/status.svg)](https://deps.rs/repo/github/cycle-five/cracktunes) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/cycle-five/cracktunes/blob/main/LICENSE) -[![Rust Version](https://img.shields.io/badge/rustc-1.76-blue.svg)](https://github.com/cycle-five/cracktunes/) [![Rust Version](https://img.shields.io/badge/rustc-1.78-blue.svg)](https://github.com/cycle-five/cracktunes/) ## Aknowledgements @@ -66,25 +65,64 @@ pip install -U yt-dlp If you are using Windows Subsystem for Linux (WSL), you should follow the [Linux/MacOS](#linuxmacos) guide, and, in addition to the other required packages, install pkg-config, which you may do by running: ```shell -apt install pkg-config +apt install -y pkg-config ``` -## Testing **FIXME** +## Testing -Tests are available inside the `src/tests` folder. They can be run via `cargo test`. It's recommended that you run the tests before submitting your Pull Request. -Increasing the test coverage is also welcome. +The following command will run all tests: -### Docker **FIXME** +```shell +cargo +nightly test --all +``` + +Some tests are available inside the `src/tests` folder, others are in their respective +files. It's recommended that you run the tests before submitting a Pull Request. +Increasing the test coverage is also welcome. Test coverage is tracked using +[tarpaulin](). + +```shell +cargo +nightly tarpaulin --all +``` + +### Docker Compose Within the project folder, simply run the following: ```shell docker build -t cracktunes . -docker run -d --env-file .env cracktunes +docker compose up -d ``` # ~~Roadmap~~ Change Log +## v0.3.7 (2024/05/29) +- [x] crackgpt 0.2.0! + Added back chatgpt support, which I am now self hosting for CrackTunes + and is backed by GPT 4o. +- [x] Use the rusty_ytdl library as a first try, fallback to yt-dlp if it fails. +- [x] Remove the grafana dashboard. +- [x] Switch to async logging. +- [x] Add an async service to handle the database (accept writes on a channel, + and write to the database in a separate thread). + Eventually this could be a seperate service (REST / GRPC). +## v0.3.6 (2024/05/03) +- Music channel setting (can lock music playing command and responses to a specific channel) +- Fixes in logging +- Fixes in admin commands +- Lots of refactoring code cleanup. +## v0.3.5 (2024/04/23) +- Significantly improved loading speed of songs into the queue. +- Fix Youtube Playlists. +- Lots of refactoring. +- Can load spotify playlists very quickly +- Option to vote for Crack Tunes on top.gg for 12 hours of premium access. +## v0.3.4 +- playlist loadspotify and playlist play commands +- Invite and voting links +- Updated serenity / poise / songbird to latest versions +- Refactored functions for creating embeds and sending messages to it's own module + ## v0.3.3 (2024/04/??) - `/loadspotify ` loads a spotify playlist into a Crack Tunes playlist. - voting tracking diff --git a/build.rs b/build.rs index 760959384..e4aa65708 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,7 @@ -// generated by `sqlx migrate build-script` fn main() { + // make sure tarpaulin is included in the build + println!("cargo::rustc-check-cfg=cfg(tarpaulin_include)"); + // generated by `sqlx migrate build-script` // trigger recompilation when a new migration is added println!("cargo:rerun-if-changed=migrations"); -} \ No newline at end of file +} diff --git a/crack-core/Cargo.toml b/crack-core/Cargo.toml index 1c438c341..337bc2d66 100644 --- a/crack-core/Cargo.toml +++ b/crack-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crack-core" -version = "0.3.6" +version = "0.3.7" authors = ["Cycle Five "] edition = "2021" description = "Core module for the cracking smoking, discord-music-bot Cracktunes." @@ -23,28 +23,30 @@ crack-gpt = ["dep:crack-gpt"] crack-osint = ["dep:crack-osint"] [dependencies] -rusty_ytdl = { git = "https://github.com/cycle-five/rusty_ytdl", default-features = false, branch = "main", features = [ +rusty_ytdl = { git = "https://github.com/cycle-five/rusty_ytdl", default-features = false, branch = "v0.7.2-boa", features = [ "live", "rustls-tls", "search", + "blocking", + "ffmpeg", ] } audiopus = "0.3.0-rc.0" async-trait = "0.1.80" -anyhow = "1.0.82" +anyhow = "1.0.83" +bytes = "1.6.0" colored = "2.1.0" lazy_static = "1.4.0" lyric_finder = { git = "https://github.com/cycle-five/spotify-player", branch = "master", version = "0.1.6" } rand = "0.8.5" regex = "1.10.4" -serde_json = "1.0.116" -serde_with = "3.7.0" +serde = { version = "1.0.202", features = ["derive", "rc"] } +serde_json = "1.0.117" +serde_with = "3.8.1" url = "2.5.0" -serde = { version = "1.0.198", features = ["derive", "rc"] } sys-info = "0.9.1" -prometheus = { version = "0.13.3", features = ["process"], optional = true } -proc-macro2 = "1.0.81" +prometheus = { version = "0.13.4", features = ["process"], optional = true } +proc-macro2 = "1.0.82" typemap_rev = "0.3.0" -chatgpt_rs = { git = "https://github.com/cycle-five/chatgpt_rs", version = "1.2.4", branch = "master", optional = true } either = "1.11.0" chrono = { version = "0.4.38", features = ["serde"] } once_cell = "1.19.0" @@ -62,21 +64,22 @@ tracing = { workspace = true } sqlx = { workspace = true } serenity = { workspace = true } songbird = { workspace = true } -symphonia = { workspace = true } tokio = { workspace = true } poise = { workspace = true } +[dependencies.symphonia] +version = "0.5.4" +# features = ["all-formats", "all-codecs", "opt-simd"] +features = ["aac", "mp3", "isomp4", "alac"] + +[dependencies.serenity-voice-model] +version = "0.2.0" + [dependencies.rspotify] version = "0.13.1" default-features = false features = ["client-reqwest", "reqwest-rustls-tls"] -# To get additional codecs, you *must* add Symphonia yourself. -# # This includes the default formats (MKV/WebM, Ogg, Wave) and codecs (FLAC, PCM, Vorbis)... -# [dependencies.symphonia] -# version = "0.5.3" -# features = ["all"] - [dependencies.ffprobe] git = "https://github.com/cycle-five/ffprobe-rs" features = ["async-tokio"] diff --git a/crack-core/build.rs b/crack-core/build.rs index b2ae65dd7..b43a49d88 100644 --- a/crack-core/build.rs +++ b/crack-core/build.rs @@ -1,5 +1,7 @@ use std::process::Command; fn main() { + // make sure tarpaulin is included in the build + println!("cargo::rustc-check-cfg=cfg(tarpaulin_include)"); // note: add error checking yourself. let output = Command::new("git") .args(["rev-parse", "HEAD"]) diff --git a/crack-core/data/names/philosophers.json b/crack-core/data/names/philosophers.json new file mode 100644 index 000000000..90b9088e5 --- /dev/null +++ b/crack-core/data/names/philosophers.json @@ -0,0 +1,1437 @@ +{ + "Thales of Miletus": { + "name": "Thales of Miletus", + "nationality": "Greek", + "background": "Pre-Socratic philosopher often considered the first philosopher in Western tradition.", + "famous_work": "No surviving works, known through references by later authors.", + "contribution": "Proposed that water is the fundamental substance of the universe." + }, + "Anaximander": { + "name": "Anaximander", + "nationality": "Greek", + "background": "Pre-Socratic philosopher and student of Thales.", + "famous_work": "No surviving works, known through fragments and later references.", + "contribution": "Suggested that the 'apeiron' (the boundless or infinite) is the origin of all things." + }, + "Anaximenes": { + "name": "Anaximenes", + "nationality": "Greek", + "background": "Pre-Socratic philosopher and student of Anaximander.", + "famous_work": "No surviving works, known through later references.", + "contribution": "Proposed that air is the fundamental substance of the universe and explained natural phenomena through processes of condensation and rarefaction." + }, + "Pythagoras": { + "name": "Pythagoras", + "nationality": "Greek", + "background": "Philosopher and mathematician, founder of the Pythagorean school.", + "famous_work": "No surviving works, known through later references and the Pythagorean tradition.", + "contribution": "Known for the Pythagorean theorem in mathematics and the belief in the transmigration of souls and the importance of numerical relationships in the cosmos." + }, + "Heraclitus": { + "name": "Heraclitus", + "nationality": "Greek", + "background": "Pre-Socratic philosopher known as the 'Weeping Philosopher.'", + "famous_work": "Fragments compiled in later works.", + "contribution": "Famous for the doctrine of change, encapsulated in the phrase 'You cannot step into the same river twice,' and the concept of the Logos." + }, + "Parmenides": { + "name": "Parmenides", + "nationality": "Greek", + "background": "Pre-Socratic philosopher and founder of the Eleatic school.", + "famous_work": "Poem titled 'On Nature' (fragments survive).", + "contribution": "Argued that reality is unchanging and that change and multiplicity are illusions." + }, + "Zeno of Elea": { + "name": "Zeno of Elea", + "nationality": "Greek", + "background": "Pre-Socratic philosopher and student of Parmenides.", + "famous_work": "No surviving works, known through later references.", + "contribution": "Famous for his paradoxes that challenge the notions of plurality and motion." + }, + "Empedocles": { + "name": "Empedocles", + "nationality": "Greek", + "background": "Pre-Socratic philosopher, poet, and physician.", + "famous_work": "Poems 'On Nature' and 'Purifications' (fragments survive).", + "contribution": "Proposed that all matter is composed of four roots (earth, air, fire, water) and introduced the concepts of love and strife as fundamental forces." + }, + "Anaxagoras": { + "name": "Anaxagoras", + "nationality": "Greek", + "background": "Pre-Socratic philosopher who introduced philosophy to Athens.", + "famous_work": "No surviving works, known through fragments and later references.", + "contribution": "Proposed that the cosmos is composed of infinite small particles (nous or mind) that are ordered by a cosmic mind." + }, + "Protagoras": { + "name": "Protagoras", + "nationality": "Greek", + "background": "Sophist and pre-Socratic philosopher.", + "famous_work": "No surviving works, known through references by Plato and others.", + "contribution": "Famous for the statement 'Man is the measure of all things,' emphasizing relativism and the subjective nature of knowledge." + }, + "Gorgias": { + "name": "Gorgias", + "nationality": "Greek", + "background": "Sophist and rhetorician from Leontini in Sicily.", + "famous_work": "Encomium of Helen", + "contribution": "Known for his nihilistic philosophy, claiming that nothing exists, and if it did, it could not be known or communicated." + }, + "Socrates": { + "name": "Socrates", + "nationality": "Greek", + "background": "Classical philosopher from Athens, known primarily through the accounts of his students, especially Plato.", + "famous_work": "No written works; known through Plato's dialogues.", + "contribution": "Developed the Socratic method of questioning to stimulate critical thinking and illuminate ideas." + }, + "Plato": { + "name": "Plato", + "nationality": "Greek", + "background": "Student of Socrates and teacher of Aristotle, founder of the Academy in Athens.", + "famous_work": "The Republic", + "contribution": "Developed foundational ideas in Western philosophy, including the Theory of Forms and the Allegory of the Cave." + }, + "Aristotle": { + "name": "Aristotle", + "nationality": "Greek", + "background": "Student of Plato and tutor to Alexander the Great, founded the Lyceum.", + "famous_work": "Nicomachean Ethics", + "contribution": "Made significant contributions to logic, metaphysics, ethics, politics, and natural sciences, emphasizing empirical observation." + }, + "Diogenes of Sinope": { + "name": "Diogenes of Sinope", + "nationality": "Greek", + "background": "Founder of Cynicism, known for his ascetic lifestyle and disdain for societal conventions.", + "famous_work": "No written works; known through anecdotes and Diogenes La\u00ebrtius's accounts.", + "contribution": "Advocated for a life in accordance with nature and virtue, challenging social norms and materialism." + }, + "Epicurus": { + "name": "Epicurus", + "nationality": "Greek", + "background": "Founder of Epicureanism, established a school known as 'The Garden.'", + "famous_work": "Letter to Menoeceus", + "contribution": "Promoted the pursuit of happiness through the avoidance of pain and fear, emphasizing simple pleasures and the importance of friendship." + }, + "Zeno of Citium": { + "name": "Zeno of Citium", + "nationality": "Greek", + "background": "Founder of Stoicism, taught at the Stoa Poikile in Athens.", + "famous_work": "No surviving works; known through later Stoic writings.", + "contribution": "Established Stoicism, which teaches the development of self-control and fortitude as a means to overcome destructive emotions." + }, + "Chrysippus": { + "name": "Chrysippus", + "nationality": "Greek", + "background": "Stoic philosopher, considered the second founder of Stoicism.", + "famous_work": "No surviving works; known through later Stoic writings.", + "contribution": "Developed Stoic logic and ethics, significantly shaping the Stoic school of thought." + }, + "Pyrrho": { + "name": "Pyrrho", + "nationality": "Greek", + "background": "Founder of Pyrrhonism, a school of Skepticism.", + "famous_work": "No written works; known through accounts by later philosophers like Sextus Empiricus.", + "contribution": "Advocated for suspending judgment (epoch\u00e9) and maintaining a state of mental tranquility (ataraxia)." + }, + "Sextus Empiricus": { + "name": "Sextus Empiricus", + "nationality": "Greek", + "background": "Skeptic philosopher and physician, main source of our knowledge of ancient Skepticism.", + "famous_work": "Outlines of Pyrrhonism", + "contribution": "Provided comprehensive accounts of Pyrrhonian Skepticism, arguing against the possibility of certain knowledge." + }, + "Cicero": { + "name": "Cicero", + "nationality": "Roman", + "background": "Orator, statesman, and philosopher, known for his contributions to Roman philosophy and rhetoric.", + "famous_work": "On the Republic", + "contribution": "Merged Greek philosophical traditions with Roman culture, emphasizing ethics, politics, and rhetoric." + }, + "Lucretius": { + "name": "Lucretius", + "nationality": "Roman", + "background": "Poet and philosopher, follower of Epicureanism.", + "famous_work": "De Rerum Natura (On the Nature of Things)", + "contribution": "Expounded Epicurean philosophy in poetic form, addressing atomism, the nature of the soul, and the pursuit of happiness." + }, + "Seneca": { + "name": "Seneca", + "nationality": "Roman", + "background": "Stoic philosopher, statesman, and playwright, tutor to Emperor Nero.", + "famous_work": "Letters to Lucilius", + "contribution": "Developed Stoic ethics, emphasizing practical wisdom, virtue, and the importance of rationality in facing life's challenges." + }, + "Epictetus": { + "name": "Epictetus", + "nationality": "Greek (Roman Empire)", + "background": "Stoic philosopher, former slave who taught in Rome and Greece.", + "famous_work": "Enchiridion (Handbook)", + "contribution": "Stressed the importance of inner freedom and control over one's own mind and actions, regardless of external circumstances." + }, + "Marcus Aurelius": { + "name": "Marcus Aurelius", + "nationality": "Roman", + "background": "Emperor and Stoic philosopher, known as the 'philosopher king.'", + "famous_work": "Meditations", + "contribution": "Provided a personal reflection on Stoic philosophy, focusing on virtue, duty, and the transience of life." + }, + "Plotinus": { + "name": "Plotinus", + "nationality": "Roman (Greek origin)", + "background": "Philosopher and founder of Neoplatonism.", + "famous_work": "Enneads", + "contribution": "Developed a complex metaphysical system centered on the One, the Intellect, and the Soul, influencing later Christian and Islamic thought." + }, + "Porphyry": { + "name": "Porphyry", + "nationality": "Roman (Tyrian origin)", + "background": "Philosopher, student of Plotinus, and significant figure in Neoplatonism.", + "famous_work": "Isagoge", + "contribution": "Introduced Aristotle\u2019s logic to the Neoplatonic tradition and wrote extensively on religion, philosophy, and ethics." + }, + "Proclus": { + "name": "Proclus", + "nationality": "Greek (Byzantine Empire)", + "background": "Neoplatonic philosopher and head of the Platonic Academy in Athens.", + "famous_work": "Elements of Theology", + "contribution": "Synthesized Platonic and Aristotelian thought, creating a comprehensive system of Neoplatonic metaphysics." + }, + "Boethius": { + "name": "Boethius", + "nationality": "Roman", + "background": "Philosopher and statesman, bridging Classical and Medieval thought.", + "famous_work": "The Consolation of Philosophy", + "contribution": "Explored issues of fortune, free will, and the nature of happiness, significantly influencing medieval Christian philosophy." + }, + "Avicenna (Ibn Sina)": { + "name": "Avicenna (Ibn Sina)", + "nationality": "Persian", + "background": "Polymath, philosopher, and physician, one of the most significant figures in Islamic philosophy.", + "famous_work": "The Book of Healing", + "contribution": "Developed a comprehensive system of philosophy integrating Aristotelian and Neoplatonic thought, significantly impacting both Islamic and Western medieval philosophy." + }, + "Jerry Fodor": { + "name": "Jerry Fodor", + "nationality": "American", + "background": "Philosopher and cognitive scientist", + "famous_work": "The Language of Thought, The Modularity of Mind", + "contribution": "Influential figure in philosophy of mind and cognitive science. Known for his work on the language of thought hypothesis, modularity of mind, and his critique of connectionism." + }, + "David Chalmers": { + "name": "David Chalmers", + "nationality": "Australian", + "background": "Philosopher", + "famous_work": "The Conscious Mind", + "contribution": "Leading figure in the philosophy of mind and cognitive science. Known for his work on consciousness, the hard problem of consciousness, and the nature of qualia." + }, + "Patricia Churchland": { + "name": "Patricia Churchland", + "nationality": "Canadian-American", + "background": "Philosopher and neuroscientist", + "famous_work": "Neurophilosophy: Toward a Unified Science of the Mind-Brain", + "contribution": "Prominent figure in neurophilosophy and eliminative materialism. Argues that folk psychology is flawed and will be replaced by neuroscience." + }, + "Paul Churchland": { + "name": "Paul Churchland", + "nationality": "Canadian", + "background": "Philosopher of science and mind, known for his work on eliminative materialism and neurophilosophy.", + "famous_work": "Matter and Consciousness", + "contribution": "Argues that our common-sense understanding of mental states is fundamentally flawed and will be superseded by a neuroscientific account." + }, + "Frank Jackson": { + "name": "Frank Jackson", + "nationality": "Australian", + "background": "Philosopher of mind and metaphysics, known for his knowledge argument against physicalism.", + "famous_work": "Epiphenomenal Qualia", + "contribution": "Presented the thought experiment of Mary's Room to argue that physicalism cannot fully account for the subjective experience of color." + }, + "Thomas Nagel": { + "name": "Thomas Nagel", + "nationality": "American", + "background": "Philosopher", + "famous_work": "The View from Nowhere, What Is It Like to Be a Bat?", + "contribution": "Known for his work in philosophy of mind, moral and political philosophy, and the philosophy of law. Famous for his exploration of consciousness and subjective experience." + }, + "Derek Parfit": { + "name": "Derek Parfit", + "nationality": "British", + "background": "Philosopher", + "famous_work": "Reasons and Persons", + "contribution": "Known for his groundbreaking work in ethics, personal identity, and rationality. Explored issues of personal identity, ethics, and the nature of rationality." + }, + "Robert Nozick": { + "name": "Robert Nozick", + "nationality": "American", + "background": "Philosopher and professor.", + "famous_work": "Anarchy, State, and Utopia", + "contribution": "Developed a libertarian political philosophy, defending the minimal state and individual rights against redistributive theories of justice." + }, + "John Rawls": { + "name": "John Rawls", + "nationality": "American", + "background": "Philosopher and professor.", + "famous_work": "A Theory of Justice", + "contribution": "Developed a liberal theory of justice, emphasizing the priority of individual rights and the fairness of social institutions." + }, + "Michael Walzer": { + "name": "Michael Walzer", + "nationality": "American", + "background": "Political philosopher, known for his work on just war theory and communitarianism.", + "famous_work": "Just and Unjust Wars", + "contribution": "Developed a contextualist approach to ethics, emphasizing the importance of shared understandings and traditions in moral reasoning." + }, + "Ronald Dworkin": { + "name": "Ronald Dworkin", + "nationality": "American", + "background": "Legal philosopher and scholar of constitutional law, known for his theory of law as integrity.", + "famous_work": "Law's Empire", + "contribution": "Argued that law is not merely a set of rules but an interpretive practice that requires judges to find the best moral justification for existing legal materials." + }, + "G. E. M. Anscombe": { + "name": "G. E. M. Anscombe", + "nationality": "British", + "background": "Philosopher", + "famous_work": "Intention, Modern Moral Philosophy", + "contribution": "Influential work on the philosophy of action, ethics, and the philosophy of mind. Known for her critique of consequentialism." + }, + "Philippa Foot": { + "name": "Philippa Foot", + "nationality": "British", + "background": "Philosopher", + "famous_work": "Virtues and Vices, Natural Goodness", + "contribution": "Central figure in the revival of virtue ethics, arguing for the importance of character and moral psychology." + }, + "Iris Murdoch": { + "name": "Iris Murdoch", + "nationality": "Irish-British", + "background": "Novelist and philosopher, known for her novels and her work on moral philosophy.", + "famous_work": "The Sovereignty of Good", + "contribution": "Emphasized the importance of attention, love, and the perception of good in moral life." + }, + "Mary Midgley": { + "name": "Mary Midgley", + "nationality": "British", + "background": "Moral philosopher, known for her critiques of scientism, reductionism, and moral relativism.", + "famous_work": "Beast and Man", + "contribution": "Defended a view of humans as moral agents embedded in a natural world, emphasizing the interconnectedness of all living things." + }, + "Elizabeth Anscombe": { + "name": "Elizabeth Anscombe", + "nationality": "British", + "background": "Philosopher", + "famous_work": "Intention, Modern Moral Philosophy", + "contribution": "Influential work on the philosophy of action, ethics, and the philosophy of mind. Known for her critique of consequentialism." + }, + "Amartya Sen": { + "name": "Amartya Sen", + "nationality": "Indian", + "background": "Economist and philosopher", + "famous_work": "Development as Freedom, Poverty and Famines", + "contribution": "Nobel laureate in economics. Known for his work on welfare economics, social choice theory, development economics, and the capabilities approach." + }, + "Kwame Anthony Appiah": { + "name": "Kwame Anthony Appiah", + "nationality": "Ghanaian-American", + "background": "Philosopher", + "famous_work": "Cosmopolitanism: Ethics in a World of Strangers, The Honor Code", + "contribution": "Known for his work on moral and political philosophy, especially in the areas of cosmopolitanism, ethics, and identity politics." + }, + "Charles Taylor": { + "name": "Charles Taylor", + "nationality": "Canadian", + "background": "Philosopher and professor", + "famous_work": "Sources of the Self", + "contribution": "Developed a communitarian political philosophy and a expressivist theory of language, influencing the development of political philosophy, philosophy of social science, and philosophy of language." + }, + "Richard Rorty": { + "name": "Richard Rorty", + "nationality": "American", + "background": "Philosopher", + "famous_work": "Philosophy and the Mirror of Nature, Contingency, Irony, and Solidarity", + "contribution": "Prominent figure in neopragmatism; rejected traditional notions of objectivity and truth; emphasized the social and historical contingency of knowledge" + }, + "J\u00fcrgen Habermas": { + "name": "J\u00fcrgen Habermas", + "nationality": "German", + "background": "Philosopher and sociologist.", + "famous_work": "The Theory of Communicative Action", + "contribution": "Developed a critical theory of society and communication, emphasizing the importance of rational discourse and consensus." + }, + "Axel Honneth": { + "name": "Axel Honneth", + "nationality": "German", + "background": "Philosopher and sociologist, known for his work on critical theory, recognition theory, and social movements.", + "famous_work": "The Struggle for Recognition", + "contribution": "Developed a theory of recognition, arguing that social struggles are often driven by the desire for recognition of different forms of identity and status." + }, + "Nancy Fraser": { + "name": "Nancy Fraser", + "nationality": "American", + "background": "Political theorist and feminist", + "famous_work": "Scales of Justice: Reimagining Political Space in a Globalizing World", + "contribution": "Known for her work on social justice, feminist theory, and critical theory. Developed a theory of justice that integrates both redistributive and recognition claims." + }, + "Seyla Benhabib": { + "name": "Seyla Benhabib", + "nationality": "Turkish-American", + "background": "Political theorist and philosopher", + "famous_work": "The Claims of Culture: Equality and Diversity in the Global Era", + "contribution": "Known for her work on democracy and difference, cosmopolitanism, and feminist theory. Emphasizes the importance of dialogue and deliberation in democratic societies." + }, + "Judith Butler": { + "name": "Judith Butler", + "nationality": "American", + "background": "Philosopher and gender theorist.", + "famous_work": "Gender Trouble", + "contribution": "Developed a influential theory of gender performativity, challenging essentialist notions of identity and influencing queer theory and feminist philosophy." + }, + "Martha Nussbaum": { + "name": "Martha Nussbaum", + "nationality": "American", + "background": "Philosopher", + "famous_work": "The Fragility of Goodness, Sex and Social Justice", + "contribution": "Developed the capabilities approach to human development; emphasized the importance of human capabilities and well-being" + }, + "Julia Kristeva": { + "name": "Julia Kristeva", + "nationality": "Bulgarian-French", + "background": "Philosopher, literary critic, psychoanalyst, and feminist", + "famous_work": "Powers of Horror, Desire in Language", + "contribution": "Explored the intersection of language, psychoanalysis, and feminism; developed the concepts of the semiotic and the symbolic" + }, + "Luce Irigaray": { + "name": "Luce Irigaray", + "nationality": "Belgian-French", + "background": "Philosopher, feminist, linguist, psychoanalyst, and cultural theorist", + "famous_work": "Speculum of the Other Woman, This Sex Which Is Not One", + "contribution": "Leading figure in French feminist thought. Known for her critique of phallocentric language and her exploration of female subjectivity." + }, + "H\u00e9l\u00e8ne Cixous": { + "name": "H\u00e9l\u00e8ne Cixous", + "nationality": "French", + "background": "Feminist writer, playwright, philosopher, literary critic, and rhetorician", + "famous_work": "The Laugh of the Medusa", + "contribution": "Leading figure in French feminist thought. Known for her concept of \"\u00e9criture f\u00e9minine\" and her exploration of the relationship between language, gender, and power." + }, + "Gayatri Chakravorty Spivak": { + "name": "Gayatri Chakravorty Spivak", + "nationality": "Indian", + "background": "Literary theorist, philosopher, and feminist critic", + "famous_work": "Can the Subaltern Speak?", + "contribution": "One of the most influential postcolonial thinkers. Known for her work on deconstruction, Marxism, and postcolonialism." + }, + "Simone Weil": { + "name": "Simone Weil", + "nationality": "French", + "background": "Philosopher, mystic, and political activist.", + "famous_work": "Gravity and Grace", + "contribution": "Explored themes of spirituality, social justice, and the human condition in her philosophical and mystical writings." + }, + "Hannah Arendt": { + "name": "Hannah Arendt", + "nationality": "German-American", + "background": "Political theorist", + "famous_work": "The Origins of Totalitarianism, The Human Condition", + "contribution": "One of the most influential political philosophers of the 20th century. Known for her work on totalitarianism, revolution, and the nature of political action." + }, + "Theodor Adorno": { + "name": "Theodor Adorno", + "nationality": "German", + "background": "Philosopher, sociologist, psychologist, musicologist, and composer", + "famous_work": "Dialectic of Enlightenment (with Max Horkheimer), Negative Dialectics", + "contribution": "Leading member of the Frankfurt School. Known for his critical theory of society, his analysis of culture and mass media, and his work on aesthetics." + }, + "Max Horkheimer": { + "name": "Max Horkheimer", + "nationality": "German", + "background": "Philosopher and sociologist", + "famous_work": "Dialectic of Enlightenment (with Theodor Adorno), Eclipse of Reason", + "contribution": "One of the founding members of the Frankfurt School. Known for his work on critical theory, social philosophy, and the critique of instrumental reason." + }, + "Walter Benjamin": { + "name": "Walter Benjamin", + "nationality": "German", + "background": "Literary critic, philosopher, social critic, translator, radio broadcaster, and essayist", + "famous_work": "The Work of Art in the Age of Mechanical Reproduction, Illuminations", + "contribution": "Influential literary critic and cultural theorist. Known for his work on aesthetics, literature, and the impact of modernity on culture." + }, + "Herbert Marcuse": { + "name": "Herbert Marcuse", + "nationality": "German-American", + "background": "Philosopher, sociologist, and political theorist", + "famous_work": "One-Dimensional Man, Eros and Civilization", + "contribution": "Key figure in the Frankfurt School. Known for his critical analysis of capitalism, consumerism, and technological rationality." + }, + "Erich Fromm": { + "name": "Erich Fromm", + "nationality": "German-American", + "background": "Psychoanalyst, sociologist, and philosopher associated with the Frankfurt School.", + "famous_work": "Escape from Freedom", + "contribution": "Explored the psychological and social factors that lead to authoritarianism and conformity." + }, + "Claude L\u00e9vi-Strauss": { + "name": "Claude L\u00e9vi-Strauss", + "nationality": "French", + "background": "Anthropologist and ethnologist", + "famous_work": "Tristes Tropiques, The Elementary Structures of Kinship", + "contribution": "One of the most influential anthropologists of the 20th century. Known for his work on structuralism and his analysis of myth, kinship, and culture." + }, + "Pierre Bourdieu": { + "name": "Pierre Bourdieu", + "nationality": "French", + "background": "Sociologist, anthropologist, philosopher, and public intellectual", + "famous_work": "Distinction: A Social Critique of the Judgement of Taste", + "contribution": "Highly influential sociologist. Known for his work on social class, cultural capital, and the reproduction of social inequality." + }, + "Jean Baudrillard": { + "name": "Jean Baudrillard", + "nationality": "French", + "background": "Sociologist, cultural theorist, and philosopher", + "famous_work": "Simulacra and Simulation, America", + "contribution": "Known for his analyses of the postmodern condition, consumer society, and the impact of mass media and technology on culture." + }, + "Michel Foucault": { + "name": "Michel Foucault", + "nationality": "French", + "background": "Philosopher, historian, and social theorist.", + "famous_work": "The Order of Things", + "contribution": "Developed a critical philosophy of power and knowledge, influencing the development of poststructuralism, queer theory, and cultural studies." + }, + "Gilles Deleuze": { + "name": "Gilles Deleuze", + "nationality": "French", + "background": "Philosopher and professor.", + "famous_work": "Difference and Repetition", + "contribution": "Developed a philosophy of difference and multiplicity, influencing the development of poststructuralism, film theory, and new materialism." + }, + "F\u00e9lix Guattari": { + "name": "F\u00e9lix Guattari", + "nationality": "French", + "background": "Psychotherapist, philosopher, and collaborator with Gilles Deleuze.", + "famous_work": "Anti-Oedipus (with Gilles Deleuze)", + "contribution": "Collaborated with Deleuze to develop a philosophy of desire, multiplicity, and critiquing psychoanalysis and capitalism." + }, + "Alain Badiou": { + "name": "Alain Badiou", + "nationality": "French", + "background": "Philosopher, mathematician, and political activist.", + "famous_work": "Being and Event", + "contribution": "Developed a philosophy of the event and the subject, drawing on set theory and advocating for a renewed communism." + }, + "Slavoj \u017di\u017eek": { + "name": "Slavoj \u017di\u017eek", + "nationality": "Slovenian", + "background": "Philosopher, cultural critic, and political activist.", + "famous_work": "The Sublime Object of Ideology", + "contribution": "Developed a Lacanian and Marxist philosophy, analyzing popular culture, ideology, and political events." + }, + "Jacques Derrida": { + "name": "Jacques Derrida", + "nationality": "French", + "background": "Philosopher and literary theorist.", + "famous_work": "Of Grammatology", + "contribution": "Developed the philosophical method of deconstruction, challenging the foundations of Western metaphysics and influencing poststructuralism and literary theory." + }, + "Emmanuel Levinas": { + "name": "Emmanuel Levinas", + "nationality": "French", + "background": "Philosopher and Talmudic commentator.", + "famous_work": "Totality and Infinity", + "contribution": "Developed a philosophy of ethics and alterity, emphasizing the primacy of the other and the responsibility of the self." + }, + "Paul Ricoeur": { + "name": "Paul Ricoeur", + "nationality": "French", + "background": "Philosopher and literary theorist.", + "famous_work": "The Rule of Metaphor", + "contribution": "Developed a hermeneutic philosophy, exploring the relationship between language, narrative, and selfhood." + }, + "Hans-Georg Gadamer": { + "name": "Hans-Georg Gadamer", + "nationality": "German", + "background": "Philosopher and professor.", + "famous_work": "Truth and Method", + "contribution": "Developed a philosophical hermeneutics, emphasizing the dialogical nature of understanding and the historicity of interpretation." + }, + "Maurice Merleau-Ponty": { + "name": "Maurice Merleau-Ponty", + "nationality": "French", + "background": "Philosopher and professor.", + "famous_work": "Phenomenology of Perception", + "contribution": "Developed a phenomenological philosophy of embodiment and perception, influencing the development of existentialism and poststructuralism." + }, + "Jean-Paul Sartre": { + "name": "Jean-Paul Sartre", + "nationality": "French", + "background": "Philosopher, playwright, and novelist.", + "famous_work": "Being and Nothingness", + "contribution": "Developed an existentialist philosophy, emphasizing human freedom, responsibility, and the absurdity of existence." + }, + "Simone de Beauvoir": { + "name": "Simone de Beauvoir", + "nationality": "French", + "background": "Philosopher, writer, and feminist.", + "famous_work": "The Second Sex", + "contribution": "Developed a feminist existentialist philosophy, analyzing the oppression of women and advocating for gender equality." + }, + "Martin Heidegger": { + "name": "Martin Heidegger", + "nationality": "German", + "background": "Philosopher and professor.", + "famous_work": "Being and Time", + "contribution": "Developed a philosophy of being and existence, influencing the development of existentialism, hermeneutics, and postmodernism." + }, + "Edmund Husserl": { + "name": "Edmund Husserl", + "nationality": "German", + "background": "Philosopher and mathematician.", + "famous_work": "Logical Investigations", + "contribution": "Developed the philosophical method of phenomenology, studying the structures of consciousness and the nature of intentionality." + }, + "Henri Bergson": { + "name": "Henri Bergson", + "nationality": "French", + "background": "Philosopher and Nobel laureate in literature.", + "famous_work": "Creative Evolution", + "contribution": "Developed a philosophy of life and intuition, emphasizing the creative impulse and the limitations of scientific knowledge." + }, + "Gabriel Marcel": { + "name": "Gabriel Marcel", + "nationality": "French", + "background": "Existentialist philosopher and playwright.", + "famous_work": "The Mystery of Being", + "contribution": "Explored themes of existentialism, focusing on human existence, freedom, and the mystery of being." + }, + "Karl Jaspers": { + "name": "Karl Jaspers", + "nationality": "German", + "background": "Existentialist philosopher and psychiatrist.", + "famous_work": "Philosophy of Existence", + "contribution": "Examined the nature of human existence, emphasizing themes like freedom, transcendence, and the limits of human knowledge." + }, + "Martin Buber": { + "name": "Martin Buber", + "nationality": "Austrian-Israeli", + "background": "Philosopher known for his philosophy of dialogue.", + "famous_work": "I and Thou", + "contribution": "Developed a relational philosophy emphasizing the I-Thou relationship as the foundation of human existence." + }, + "Emmanuel Mounier": { + "name": "Emmanuel Mounier", + "nationality": "French", + "background": "Philosopher and founder of the personalist movement.", + "famous_work": "Personalism", + "contribution": "Advocated for a human-centered philosophy that emphasizes the dignity and value of the person in a communal context." + }, + "Paul Tillich": { + "name": "Paul Tillich", + "nationality": "German-American", + "background": "Theologian and existentialist philosopher.", + "famous_work": "The Courage to Be", + "contribution": "Explored the relationship between faith and existentialist philosophy, addressing the anxiety and meaning of human existence." + }, + "Reinhold Niebuhr": { + "name": "Reinhold Niebuhr", + "nationality": "American", + "background": "Theologian and ethicist known for his work in Christian ethics.", + "famous_work": "Moral Man and Immoral Society", + "contribution": "Developed a realist approach to ethics and politics, emphasizing the limitations of human nature and the necessity of justice." + }, + "Ludwig Wittgenstein": { + "name": "Ludwig Wittgenstein", + "nationality": "Austrian-British", + "background": "Philosopher and logician.", + "famous_work": "Tractatus Logico-Philosophicus", + "contribution": "Developed a philosophy of language and logic, influencing the development of analytic philosophy and the linguistic turn." + }, + "Bertrand Russell": { + "name": "Bertrand Russell", + "nationality": "British", + "background": "Philosopher, logician, and mathematician.", + "famous_work": "Principia Mathematica (with Alfred North Whitehead)", + "contribution": "Made significant contributions to logic, philosophy of language, and analytic philosophy, and popularized philosophy through his writings." + }, + "A. J. Ayer": { + "name": "A. J. Ayer", + "nationality": "British", + "background": "Philosopher", + "famous_work": "Language, Truth and Logic", + "contribution": "Leading proponent of logical positivism. Known for his work on epistemology, ethics, and the philosophy of language." + }, + "R. G. Collingwood": { + "name": "R. G. Collingwood", + "nationality": "British", + "background": "Philosopher and historian", + "famous_work": "The Idea of History, An Autobiography", + "contribution": "Known for his work on the philosophy of history, aesthetics, and the philosophy of mind." + }, + "Isaiah Berlin": { + "name": "Isaiah Berlin", + "nationality": "British", + "background": "Philosopher and historian of ideas", + "famous_work": "Two Concepts of Liberty, The Hedgehog and the Fox", + "contribution": "Known for his work on political philosophy, the history of ideas, and his defense of liberalism and pluralism." + }, + "Karl Popper": { + "name": "Karl Popper", + "nationality": "Austrian-British", + "background": "Philosopher", + "famous_work": "The Logic of Scientific Discovery, The Open Society and Its Enemies", + "contribution": "One of the most influential philosophers of science of the 20th century. Known for his work on the philosophy of science, political philosophy, and his critique of historicism." + }, + "Paul Feyerabend": { + "name": "Paul Feyerabend", + "nationality": "Austrian-American", + "background": "Philosopher", + "famous_work": "Against Method", + "contribution": "Known for his anarchist views on the philosophy of science and his argument that there is no single scientific method." + }, + "Thomas Kuhn": { + "name": "Thomas Kuhn", + "nationality": "American", + "background": "Philosopher and historian of science", + "famous_work": "The Structure of Scientific Revolutions", + "contribution": "One of the most influential philosophers of science of the 20th century. Known for his work on the history and philosophy of science, particularly his concept of scientific paradigms." + }, + "Bas van Fraassen": { + "name": "Bas van Fraassen", + "nationality": "Dutch-American", + "background": "Philosopher", + "famous_work": "The Scientific Image", + "contribution": "Leading figure in the philosophy of science. Known for his constructive empiricism, which argues that science aims to provide empirically adequate theories, not necessarily true ones." + }, + "Ian Hacking": { + "name": "Ian Hacking", + "nationality": "Canadian", + "background": "Philosopher", + "famous_work": "The Social Construction of What?", + "contribution": "Known for his work on the philosophy of science, particularly his historical and sociological analyses of scientific concepts and practices." + }, + "Nancy Cartwright": { + "name": "Nancy Cartwright", + "nationality": "American", + "background": "Philosopher", + "famous_work": "How the Laws of Physics Lie", + "contribution": "Known for her work on the philosophy of science, particularly her critique of the idea of fundamental laws of nature and her emphasis on the role of models and causal explanations in science." + }, + "Hilary Putnam": { + "name": "Hilary Putnam", + "nationality": "American", + "background": "Philosopher, mathematician, and computer scientist", + "famous_work": "Reason, Truth and History, Realism with a Human Face", + "contribution": "Major figure in philosophy of language, mind, mathematics, and science. Known for his work on semantic externalism and the \"brain in a vat\" thought experiment." + }, + "Saul Kripke": { + "name": "Saul Kripke", + "nationality": "American", + "background": "Philosopher and logician", + "famous_work": "Naming and Necessity", + "contribution": "Highly influential work on modal logic, philosophy of language, and metaphysics. Known for his work on rigid designators and the necessity of identity." + }, + "David Lewis": { + "name": "David Lewis", + "nationality": "American", + "background": "Philosopher", + "famous_work": "On the Plurality of Worlds", + "contribution": "Known for his contributions to metaphysics, philosophy of language, philosophical logic, and philosophy of mind. Strong advocate of modal realism." + }, + "Ruth Barcan Marcus": { + "name": "Ruth Barcan Marcus", + "nationality": "American", + "background": "Philosopher and logician", + "famous_work": "Modalities and Intensional Languages", + "contribution": "Known for her work in modal and philosophical logic, especially for her contributions to quantified modal logic and the development of the Barcan formula." + }, + "Timothy Williamson": { + "name": "Timothy Williamson", + "nationality": "British", + "background": "Philosopher", + "famous_work": "Knowledge and Its Limits", + "contribution": "Known for his work in epistemology, philosophical logic, and the philosophy of language. Advocate of knowledge-first epistemology." + }, + "Kit Fine": { + "name": "Kit Fine", + "nationality": "American", + "background": "Philosopher", + "famous_work": "Semantic Relationism", + "contribution": "Known for his work in metaphysics, philosophy of language, and philosophical logic. Made significant contributions to the development of truthmaker theory." + }, + "Robert Brandom": { + "name": "Robert Brandom", + "nationality": "American", + "background": "Philosopher", + "famous_work": "Making It Explicit", + "contribution": "Known for his work in philosophy of language, philosophy of mind, and philosophical logic. Developed inferentialism, a theory of meaning that emphasizes the role of inference in determining the content of concepts." + }, + "John McDowell": { + "name": "John McDowell", + "nationality": "South African-British", + "background": "Philosopher", + "famous_work": "Mind and World", + "contribution": "Influential figure in contemporary philosophy of language and mind. Known for his work on moral philosophy, metaphysics, and ancient philosophy." + }, + "Bernard Williams": { + "name": "Bernard Williams", + "nationality": "British", + "background": "Philosopher", + "famous_work": "Ethics and the Limits of Philosophy, Shame and Necessity", + "contribution": "Challenged dominant trends in moral philosophy, emphasizing the importance of history, culture, and personal perspective." + }, + "Galen Strawson": { + "name": "Galen Strawson", + "nationality": "British", + "background": "Philosopher", + "famous_work": "Freedom and Belief", + "contribution": "Known for his work on metaphysics, philosophy of mind, and free will. Argues for a skeptical view of free will and moral responsibility." + }, + "Christine Korsgaard": { + "name": "Christine Korsgaard", + "nationality": "American", + "background": "Philosopher", + "famous_work": "The Sources of Normativity", + "contribution": "Known for her work in ethics, moral psychology, and political philosophy. Defends a Kantian approach to ethics and emphasizes the importance of autonomy and practical reason." + }, + "Susan Wolf": { + "name": "Susan Wolf", + "nationality": "American", + "background": "Philosopher", + "famous_work": "Freedom Within Reason", + "contribution": "Known for her work in moral philosophy, philosophy of action, and feminist philosophy. Developed the \"asymmetrical freedom\" account of moral responsibility." + }, + "Harry Frankfurt": { + "name": "Harry Frankfurt", + "nationality": "American", + "background": "Philosopher", + "famous_work": "The Importance of What We Care About", + "contribution": "Known for his work on moral philosophy, philosophy of action, and the philosophy of mind. Developed the concept of \"second-order desires\" and their role in freedom of the will." + }, + "T. M. Scanlon": { + "name": "T. M. Scanlon", + "nationality": "American", + "background": "Philosopher", + "famous_work": "What We Owe to Each Other", + "contribution": "Known for his work in moral and political philosophy. Developed contractualism, a moral theory that grounds moral obligations in the idea of justifiable reasons." + }, + "Peter Strawson": { + "name": "Peter Strawson", + "nationality": "British", + "background": "Philosopher", + "famous_work": "Individuals: An Essay in Descriptive Metaphysics", + "contribution": "Influential figure in ordinary language philosophy. Known for his work on metaphysics, philosophy of language, and the philosophy of mind." + }, + "Gilbert Ryle": { + "name": "Gilbert Ryle", + "nationality": "British", + "background": "Philosopher", + "famous_work": "The Concept of Mind", + "contribution": "One of the leading figures in ordinary language philosophy. Known for his critique of Cartesian dualism and his behaviorist approach to the philosophy of mind." + }, + "J. L. Austin": { + "name": "J. L. Austin", + "nationality": "British", + "background": "Philosopher", + "famous_work": "How to Do Things with Words", + "contribution": "One of the founders of speech act theory. Known for his work on the philosophy of language, particularly his analysis of performative utterances." + }, + "Donald Davidson": { + "name": "Donald Davidson", + "nationality": "American", + "background": "Philosopher", + "famous_work": "Essays on Actions and Events, Inquiries into Truth and Interpretation", + "contribution": "Major figure in philosophy of language, mind, and action. Known for his work on truth-conditional semantics, the principle of charity, and anomalous monism." + }, + "John Searle": { + "name": "John Searle", + "nationality": "American", + "background": "Philosopher", + "famous_work": "Speech Acts, Intentionality, The Construction of Social Reality", + "contribution": "Known for his contributions to philosophy of language, mind, and social philosophy. Famous for his Chinese Room argument against strong AI." + }, + "Richard Swinburne": { + "name": "Richard Swinburne", + "nationality": "British", + "background": "Philosopher", + "famous_work": "The Existence of God, The Coherence of Theism", + "contribution": "Prominent contemporary philosopher of religion. Known for his defense of theism and his work on the philosophy of religion, miracles, and the problem of evil." + }, + "Alvin Plantinga": { + "name": "Alvin Plantinga", + "nationality": "American", + "background": "Philosopher", + "famous_work": "God and Other Minds, Warrant and Proper Function", + "contribution": "Leading contemporary philosopher of religion. Known for his work on the problem of evil, the epistemology of religious belief, and the modal argument for the existence of God." + }, + "Richard Dawkins": { + "name": "Richard Dawkins", + "nationality": "British", + "background": "Evolutionary biologist and philosopher of science.", + "famous_work": "The Selfish Gene", + "contribution": "Popularized the gene-centered view of evolution and critically engaged with issues of religion and science." + }, + "Daniel Dennett": { + "name": "Daniel Dennett", + "nationality": "American", + "background": "Philosopher, cognitive scientist, and writer", + "famous_work": "Consciousness Explained, Darwin's Dangerous Idea", + "contribution": "One of the most influential contemporary philosophers of mind. Known for his work on consciousness, intentionality, and evolutionary biology." + }, + "Al-Farabi": { + "name": "Al-Farabi", + "nationality": "Persian", + "background": "Islamic philosopher and polymath, known as the 'Second Teacher' after Aristotle.", + "famous_work": "The Virtuous City", + "contribution": "Synthesized Greek philosophy with Islamic thought, and made significant contributions to logic, metaphysics, and political philosophy." + }, + "Al-Ghazali": { + "name": "Al-Ghazali", + "nationality": "Persian", + "background": "Islamic theologian, philosopher, and mystic.", + "famous_work": "The Incoherence of the Philosophers", + "contribution": "Criticized Neoplatonism and Aristotelianism, and reconciled Islamic theology with Sufism." + }, + "Averroes (Ibn Rushd)": { + "name": "Averroes (Ibn Rushd)", + "nationality": "Andalusian", + "background": "Islamic philosopher, jurist, and physician.", + "famous_work": "The Incoherence of the Incoherence", + "contribution": "Defended Aristotelian philosophy against Al-Ghazali's criticisms, and influenced the development of Western scholasticism." + }, + "Maimonides": { + "name": "Maimonides", + "nationality": "Andalusian-Egyptian", + "background": "Jewish philosopher, theologian, and physician.", + "famous_work": "The Guide for the Perplexed", + "contribution": "Reconciled Aristotelian philosophy with Jewish theology, and influenced both Jewish and Christian thought." + }, + "Thomas Aquinas": { + "name": "Thomas Aquinas", + "nationality": "Italian", + "background": "Dominican friar, philosopher, and theologian.", + "famous_work": "Summa Theologica", + "contribution": "Synthesized Aristotelian philosophy with Christian theology, and made significant contributions to metaphysics, ethics, and political philosophy." + }, + "Albertus Magnus": { + "name": "Albertus Magnus", + "nationality": "German", + "background": "Dominican friar, philosopher, and theologian, teacher of Thomas Aquinas.", + "famous_work": "De vegetabilibus et plantis", + "contribution": "Introduced Aristotelian philosophy to the Latin West, and made significant contributions to natural philosophy and theology." + }, + "John Duns Scotus": { + "name": "John Duns Scotus", + "nationality": "Scottish", + "background": "Franciscan friar, philosopher, and theologian.", + "famous_work": "Ordinatio", + "contribution": "Developed a distinctive philosophical system emphasizing the primacy of the will and the univocity of being." + }, + "William of Ockham": { + "name": "William of Ockham", + "nationality": "English", + "background": "Franciscan friar, philosopher, and theologian.", + "famous_work": "Summa Logicae", + "contribution": "Developed the principle of parsimony (Occam's razor), and made significant contributions to logic, metaphysics, and epistemology." + }, + "Meister Eckhart": { + "name": "Meister Eckhart", + "nationality": "German", + "background": "Dominican friar, theologian, and mystic.", + "famous_work": "Sermons", + "contribution": "Developed a mystical theology emphasizing the unity of the soul with God, and influenced later German idealism." + }, + "Nicholas of Cusa": { + "name": "Nicholas of Cusa", + "nationality": "German", + "background": "Cardinal, philosopher, theologian, and mathematician.", + "famous_work": "On Learned Ignorance", + "contribution": "Developed the concept of coincidentia oppositorum (coincidence of opposites), and made significant contributions to mathematics and cosmology." + }, + "Marsilio Ficino": { + "name": "Marsilio Ficino", + "nationality": "Italian", + "background": "Philosopher, astrologer, and Catholic priest, a key figure in the Italian Renaissance.", + "famous_work": "Theologia Platonica", + "contribution": "Translated and commented on the works of Plato and Plotinus, and played a key role in the revival of Neoplatonism." + }, + "Giovanni Pico della Mirandola": { + "name": "Giovanni Pico della Mirandola", + "nationality": "Italian", + "background": "Philosopher and scholar, a key figure in the Italian Renaissance.", + "famous_work": "Oration on the Dignity of Man", + "contribution": "Synthesized various philosophical and religious traditions, and emphasized the dignity and free will of human beings." + }, + "Niccol\u00f2 Machiavelli": { + "name": "Niccol\u00f2 Machiavelli", + "nationality": "Italian", + "background": "Diplomat, philosopher, and writer.", + "famous_work": "The Prince", + "contribution": "Developed a pragmatic approach to politics, emphasizing the acquisition and maintenance of power." + }, + "Desiderius Erasmus": { + "name": "Desiderius Erasmus", + "nationality": "Dutch", + "background": "Philosopher, theologian, and humanist.", + "famous_work": "The Praise of Folly", + "contribution": "Promoted the study of classical literature and Christian humanism, and criticized ecclesiastical abuses and dogmatism." + }, + "Michel de Montaigne": { + "name": "Michel de Montaigne", + "nationality": "French", + "background": "Philosopher and essayist.", + "famous_work": "Essays", + "contribution": "Developed the essay as a literary genre, and explored a wide range of philosophical and personal topics with skepticism and humility." + }, + "Francis Bacon": { + "name": "Francis Bacon", + "nationality": "English", + "background": "Philosopher, statesman, and essayist.", + "famous_work": "Novum Organum", + "contribution": "Developed the scientific method, emphasizing empirical observation and inductive reasoning." + }, + "Ren\u00e9 Descartes": { + "name": "Ren\u00e9 Descartes", + "nationality": "French", + "background": "Philosopher and mathematician.", + "famous_work": "Meditations on First Philosophy", + "contribution": "Developed a method of systematic doubt, and established the foundations of modern philosophy and mathematics." + }, + "Thomas Hobbes": { + "name": "Thomas Hobbes", + "nationality": "English", + "background": "Philosopher and political theorist.", + "famous_work": "Leviathan", + "contribution": "Developed a materialistic and mechanistic view of the world, and a social contract theory of political legitimacy." + }, + "Baruch Spinoza": { + "name": "Baruch Spinoza", + "nationality": "Dutch", + "background": "Philosopher and lens grinder.", + "famous_work": "Ethics", + "contribution": "Developed a pantheistic and deterministic philosophy, identifying God with nature and arguing for the necessity of all events." + }, + "John Locke": { + "name": "John Locke", + "nationality": "English", + "background": "Philosopher and physician.", + "famous_work": "An Essay Concerning Human Understanding", + "contribution": "Developed an empiricist theory of knowledge, emphasizing the role of experience in the formation of ideas." + }, + "George Berkeley": { + "name": "George Berkeley", + "nationality": "Irish", + "background": "Philosopher and Bishop of Cloyne.", + "famous_work": "A Treatise Concerning the Principles of Human Knowledge", + "contribution": "Developed an idealist philosophy, arguing that only minds and ideas exist, and that matter is an illusion." + }, + "David Hume": { + "name": "David Hume", + "nationality": "Scottish", + "background": "Philosopher, historian, and essayist.", + "famous_work": "A Treatise of Human Nature", + "contribution": "Developed a skeptical and empiricist philosophy, questioning the foundations of knowledge and morality." + }, + "Jean-Jacques Rousseau": { + "name": "Jean-Jacques Rousseau", + "nationality": "Genevan", + "background": "Philosopher, writer, and composer.", + "famous_work": "The Social Contract", + "contribution": "Developed a theory of the social contract, emphasizing popular sovereignty and the general will." + }, + "Immanuel Kant": { + "name": "Immanuel Kant", + "nationality": "Prussian", + "background": "Philosopher and professor.", + "famous_work": "Critique of Pure Reason", + "contribution": "Developed a critical philosophy, exploring the limits and conditions of knowledge, morality, and aesthetics." + }, + "Johann Gottlieb Fichte": { + "name": "Johann Gottlieb Fichte", + "nationality": "German", + "background": "Philosopher and nationalist.", + "famous_work": "The Science of Knowledge", + "contribution": "Developed a subjective idealist philosophy, emphasizing the role of the self in the constitution of reality." + }, + "Friedrich Wilhelm Joseph Schelling": { + "name": "Friedrich Wilhelm Joseph Schelling", + "nationality": "German", + "background": "Philosopher and professor.", + "famous_work": "System of Transcendental Idealism", + "contribution": "Developed a philosophy of nature and a theory of the absolute, influencing the development of German romanticism." + }, + "Georg Wilhelm Friedrich Hegel": { + "name": "Georg Wilhelm Friedrich Hegel", + "nationality": "German", + "background": "Philosopher and professor.", + "famous_work": "The Phenomenology of Spirit", + "contribution": "Developed a comprehensive philosophical system, emphasizing the dialectical development of spirit and the unity of reality." + }, + "Arthur Schopenhauer": { + "name": "Arthur Schopenhauer", + "nationality": "German", + "background": "Philosopher and author.", + "famous_work": "The World as Will and Representation", + "contribution": "Developed a pessimistic philosophy, emphasizing the role of the will in human suffering and the need for aesthetic and ascetic release." + }, + "S\u00f8ren Kierkegaard": { + "name": "S\u00f8ren Kierkegaard", + "nationality": "Danish", + "background": "Philosopher, theologian, and author.", + "famous_work": "Either/Or", + "contribution": "Developed an existentialist philosophy, emphasizing the importance of individual choice and commitment in the face of absurdity and despair." + }, + "Karl Marx": { + "name": "Karl Marx", + "nationality": "Prussian", + "background": "Philosopher, economist, and revolutionary socialist.", + "famous_work": "The Communist Manifesto (with Friedrich Engels)", + "contribution": "Developed a materialist conception of history and a critique of capitalist society, laying the foundations for Marxist theory and communist political movements." + }, + "Friedrich Nietzsche": { + "name": "Friedrich Nietzsche", + "nationality": "German", + "background": "Philosopher, cultural critic, and philologist.", + "famous_work": "Thus Spoke Zarathustra", + "contribution": "Developed a philosophy of life-affirmation, critiquing traditional morality and advocating the 'will to power' and the '\u00dcbermensch.'" + }, + "John Stuart Mill": { + "name": "John Stuart Mill", + "nationality": "English", + "background": "Philosopher, political economist, and civil servant.", + "famous_work": "On Liberty", + "contribution": "Developed a utilitarian ethical theory and a liberal political philosophy, emphasizing individual liberty and women's rights." + }, + "Herbert Spencer": { + "name": "Herbert Spencer", + "nationality": "English", + "background": "Philosopher, biologist, anthropologist, and sociologist.", + "famous_work": "First Principles", + "contribution": "Developed an evolutionary philosophy, applying the principles of natural selection to the study of society and ethics." + }, + "Alasdair MacIntyre": { + "name": "Alasdair MacIntyre", + "nationality": "Scottish", + "background": "Philosopher and professor", + "famous_work": "After Virtue", + "contribution": "Developed a virtue ethics and a critique of liberal individualism, influencing the development of moral philosophy, political philosophy, and philosophy of social science." + }, + "Peter Singer": { + "name": "Peter Singer", + "nationality": "Australian", + "background": "Philosopher and bioethicist.", + "famous_work": "Animal Liberation", + "contribution": "Developed a utilitarian animal ethics, arguing for the equal consideration of animal interests and influencing the animal rights movement." + }, + "Willard Van Orman Quine": { + "name": "Willard Van Orman Quine", + "nationality": "American", + "background": "Philosopher and logician", + "famous_work": "Two Dogmas of Empiricism, Word and Object", + "contribution": "One of the most influential philosophers of the 20th century. Known for his work on logic, set theory, and the philosophy of language." + }, + "Thomas Metzinger": { + "name": "Thomas Metzinger", + "nationality": "German", + "background": "Philosopher", + "famous_work": "Being No One: The Self-Model Theory of Subjectivity", + "contribution": "Known for his work on the philosophy of mind, consciousness, and artificial intelligence. Proponent of the self-model theory of consciousness." + }, + "Cornel West": { + "name": "Cornel West", + "nationality": "American", + "background": "Philosopher, political activist, public intellectual, and author", + "famous_work": "Race Matters, Democracy Matters", + "contribution": "Prominent public intellectual and activist. Known for his work on race, class, and justice in America." + }, + "Iris Marion Young": { + "name": "Iris Marion Young", + "nationality": "American", + "background": "Political theorist and feminist", + "famous_work": "Justice and the Politics of Difference", + "contribution": "Known for her work on social and political justice, particularly her focus on issues of gender, race, and oppression." + }, + "Michael Sandel": { + "name": "Michael Sandel", + "nationality": "American", + "background": "Political philosopher", + "famous_work": "Liberalism and the Limits of Justice, What Money Can't Buy: The Moral Limits of Markets", + "contribution": "Known for his work on communitarianism, political philosophy, and bioethics. Argues for a more robust public discourse about moral and civic values." + }, + "Hubert Dreyfus": { + "name": "Hubert Dreyfus", + "nationality": "American", + "background": "Philosopher", + "famous_work": "What Computers Still Can't Do: A Critique of Artificial Reason", + "contribution": "Known for his critiques of artificial intelligence and his interpretations of Martin Heidegger and Maurice Merleau-Ponty." + }, + "Stanley Cavell": { + "name": "Stanley Cavell", + "nationality": "American", + "background": "Philosopher", + "famous_work": "The Claim of Reason: Wittgenstein, Skepticism, Morality, and Tragedy", + "contribution": "Known for his work on ordinary language philosophy, film studies, and American philosophy." + }, + "William Lane Craig": { + "name": "William Lane Craig", + "nationality": "American", + "background": "Philosopher and theologian", + "famous_work": "Reasonable Faith, The Kalam Cosmological Argument", + "contribution": "Known for his work in apologetics, philosophy of religion, and the philosophy of time. Strong advocate for the Kalam cosmological argument and the historicity of the resurrection of Jesus." + }, + "Peter van Inwagen": { + "name": "Peter van Inwagen", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "An Essay on Free Will", + "contribution": "Developed an incompatibilist theory of free will and an organicist theory of composition, influencing the development of metaphysics and philosophy of action." + }, + "Roderick Chisholm": { + "name": "Roderick Chisholm", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "Person and Object", + "contribution": "Developed an agent-causal theory of free will and a foundationalist epistemology, influencing the development of metaphysics, epistemology, and philosophy of action." + }, + "George Edward Moore": { + "name": "George Edward Moore", + "nationality": "British", + "background": "Philosopher and professor", + "famous_work": "Principia Ethica", + "contribution": "Developed an intuitionist metaethics and a common sense realist epistemology, influencing the development of analytic philosophy, metaethics, and epistemology." + }, + "W. D. Ross": { + "name": "W. D. Ross", + "nationality": "British", + "background": "Philosopher and professor", + "famous_work": "The Right and the Good", + "contribution": "Developed a pluralistic deontological ethics and a theory of prima facie duties, influencing the development of normative ethics and metaethics." + }, + "C. D. Broad": { + "name": "C. D. Broad", + "nationality": "British", + "background": "Philosopher and professor", + "famous_work": "The Mind and its Place in Nature", + "contribution": "Developed an emergentist theory of mind and a theory of psychical research, influencing the development of philosophy of mind and parapsychology." + }, + "Brand Blanshard": { + "name": "Brand Blanshard", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "The Nature of Thought", + "contribution": "Developed a coherence theory of truth and a rationalist epistemology, influencing the development of metaphysics, epistemology, and philosophy of mind." + }, + "Wilfrid Sellars": { + "name": "Wilfrid Sellars", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "Empiricism and the Philosophy of Mind", + "contribution": "Developed a myth of the given critique and a scientific realist philosophy, influencing the development of philosophy of mind, epistemology, and philosophy of science." + }, + "Richard Hare": { + "name": "Richard Hare", + "nationality": "British", + "background": "Philosopher and professor", + "famous_work": "The Language of Morals", + "contribution": "Developed a universal prescriptivism metaethics and a two-level utilitarian normative ethics, influencing the development of metaethics and normative ethics." + }, + "J. J. C. Smart": { + "name": "J. J. C. Smart", + "nationality": "Australian", + "background": "Philosopher and professor", + "famous_work": "Utilitarianism: For and Against", + "contribution": "Developed an act utilitarian ethics and an identity theory of mind, influencing the development of normative ethics, metaethics, and philosophy of mind." + }, + "Charles Mills": { + "name": "Charles Mills", + "nationality": "Jamaican", + "background": "Philosopher and professor", + "famous_work": "The Racial Contract", + "contribution": "Developed a critical race theory and a critique of ideal theory, influencing the development of social and political philosophy, African-American philosophy, and critical theory." + }, + "Frantz Fanon": { + "name": "Frantz Fanon", + "nationality": "Martinican", + "background": "Psychiatrist, philosopher, and revolutionary", + "famous_work": "The Wretched of the Earth", + "contribution": "Developed an existential phenomenology of race and a critique of colonialism, influencing the development of postcolonial theory, critical theory, and Africana philosophy." + }, + "Enrique Dussel": { + "name": "Enrique Dussel", + "nationality": "Argentine", + "background": "Philosopher and historian", + "famous_work": "Philosophy of Liberation", + "contribution": "Developed a liberation philosophy and a decolonial ethics, influencing the development of Latin American philosophy, postcolonial theory, and global justice studies." + }, + "Walter Mignolo": { + "name": "Walter Mignolo", + "nationality": "Argentine", + "background": "Semiotician and literary theorist", + "famous_work": "The Darker Side of Western Modernity", + "contribution": "Developed a decolonial theory and a border thinking methodology, influencing the development of Latin American studies, postcolonial theory, and decolonial studies." + }, + "Mar\u00eda Lugones": { + "name": "Mar\u00eda Lugones", + "nationality": "Argentine", + "background": "Philosopher and feminist theorist", + "famous_work": "Pilgrimages/Peregrinajes", + "contribution": "Developed an intersectional feminism and a decolonial feminist theory, influencing the development of feminist philosophy, queer theory, and decolonial studies." + }, + "Gloria Anzald\u00faa": { + "name": "Gloria Anzald\u00faa", + "nationality": "American", + "background": "Poet, philosopher, and feminist theorist", + "famous_work": "Borderlands/La Frontera", + "contribution": "Developed a mestiza consciousness and a theory of the borderlands, influencing the development of Chicana feminism, border studies, and queer theory." + }, + "Linda Mart\u00edn Alcoff": { + "name": "Linda Mart\u00edn Alcoff", + "nationality": "Panamanian-American", + "background": "Philosopher and feminist theorist", + "famous_work": "Visible Identities", + "contribution": "Developed a realist theory of identity and a decolonial feminism, influencing the development of feminist philosophy, social epistemology, and decolonial studies." + }, + "Robert Bernasconi": { + "name": "Robert Bernasconi", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "The Question of Language in Heidegger's History of Being", + "contribution": "Developed a critical philosophy of race and a critique of Eurocentrism in philosophy, influencing the development of continental philosophy, critical race theory, and Africana philosophy." + }, + "Lucius Outlaw": { + "name": "Lucius Outlaw", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "On Race and Philosophy", + "contribution": "Developed a philosophy of race and a critique of the canon, influencing the development of Africana philosophy, social and political philosophy, and philosophy of culture." + }, + "Tommy J. Curry": { + "name": "Tommy J. Curry", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "The Man-Not", + "contribution": "Developed a black male studies theory and a critique of disciplinary decadence, influencing the development of Africana philosophy, gender studies, and black existentialism." + }, + "Kathryn Gines": { + "name": "Kathryn Gines", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "Hannah Arendt and the Negro Question", + "contribution": "Developed an Arendtian critical philosophy of race and a black feminist philosophy, influencing the development of Africana philosophy, political philosophy, and critical theory." + }, + "Jos\u00e9 Medina": { + "name": "Jos\u00e9 Medina", + "nationality": "Spanish-American", + "background": "Philosopher and professor", + "famous_work": "The Epistemology of Resistance", + "contribution": "Developed an epistemic injustice theory and a critical social epistemology, influencing the development of feminist philosophy, philosophy of race, and political philosophy." + }, + "Shannon Sullivan": { + "name": "Shannon Sullivan", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "Revealing Whiteness", + "contribution": "Developed a pragmatist critical philosophy of race and a theory of white privilege, influencing the development of American philosophy, critical whiteness studies, and feminist philosophy." + }, + "Jorge J. E. Gracia": { + "name": "Jorge J. E. Gracia", + "nationality": "Cuban-American", + "background": "Philosopher and professor", + "famous_work": "Latinos in America", + "contribution": "Developed a familial-historical view of Latino identity and a philosophy of ethnicity, influencing the development of Latin American philosophy, philosophy of race, and metaphysics." + }, + "Eduardo Mendieta": { + "name": "Eduardo Mendieta", + "nationality": "Colombian-American", + "background": "Philosopher and professor", + "famous_work": "Global Fragments", + "contribution": "Developed a critical theory of globalization and a Latin American philosophy, influencing the development of political philosophy, philosophy of race, and Latin American studies." + }, + "Nelson Maldonado-Torres": { + "name": "Nelson Maldonado-Torres", + "nationality": "Puerto Rican", + "background": "Philosopher and professor", + "famous_work": "Against War", + "contribution": "Developed a decolonial philosophy and a critique of Eurocentrism, influencing the development of Latin American philosophy, Caribbean philosophy, and decolonial studies." + }, + "Ofelia Schutte": { + "name": "Ofelia Schutte", + "nationality": "Cuban-American", + "background": "Philosopher and professor", + "famous_work": "Cultural Identity and Social Liberation in Latin American Thought", + "contribution": "Developed a feminist philosophy of liberation and a critique of Eurocentrism, influencing the development of Latin American philosophy, feminist philosophy, and philosophy of culture." + }, + "Susanne Langer": { + "name": "Susanne Langer", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "Philosophy in a New Key", + "contribution": "Developed a philosophy of art and a theory of symbolism, influencing the development of aesthetics, philosophy of mind, and philosophy of culture." + }, + "Arthur Danto": { + "name": "Arthur Danto", + "nationality": "American", + "background": "Philosopher and art critic", + "famous_work": "The Transfiguration of the Commonplace", + "contribution": "Developed an institutional theory of art and an essentialist definition of art, influencing the development of aesthetics, philosophy of art, and art criticism." + }, + "Nelson Goodman": { + "name": "Nelson Goodman", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "Languages of Art", + "contribution": "Developed a constructivist theory of symbols and a theory of worldmaking, influencing the development of aesthetics, epistemology, and philosophy of science." + }, + "Richard Wollheim": { + "name": "Richard Wollheim", + "nationality": "British", + "background": "Philosopher and art historian", + "famous_work": "Art and Its Objects", + "contribution": "Developed a psychoanalytic theory of art and a representational theory of depiction, influencing the development of aesthetics, philosophy of art, and art history." + }, + "Kendall Walton": { + "name": "Kendall Walton", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "Mimesis as Make-Believe", + "contribution": "Developed a theory of fictional worlds and a pretense theory of representation, influencing the development of aesthetics, philosophy of fiction, and cognitive science." + }, + "No\u00ebl Carroll": { + "name": "No\u00ebl Carroll", + "nationality": "American", + "background": "Philosopher and film theorist", + "famous_work": "The Philosophy of Horror", + "contribution": "Developed a cognitive theory of art and a theory of mass art, influencing the development of aesthetics, philosophy of film, and popular culture studies." + }, + "Gregory Currie": { + "name": "Gregory Currie", + "nationality": "Australian", + "background": "Philosopher and professor", + "famous_work": "The Nature of Fiction", + "contribution": "Developed a speech act theory of fiction and a simulation theory of empathy, influencing the development of aesthetics, philosophy of literature, and cognitive science." + }, + "Jenefer Robinson": { + "name": "Jenefer Robinson", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "Deeper than Reason", + "contribution": "Developed an emotion theory of music and a theory of expression, influencing the development of aesthetics, philosophy of music, and philosophy of emotion." + }, + "Stephanie Ross": { + "name": "Stephanie Ross", + "nationality": "American", + "background": "Philosopher and professor", + "famous_work": "What Gardens Mean", + "contribution": "Developed an aesthetics of gardens and everyday aesthetics, influencing the development of environmental aesthetics, philosophy of architecture, and feminist aesthetics." + }, + "J. P. Moreland": { + "name": "J. P. Moreland", + "nationality": "American", + "background": "Philosopher and theologian", + "famous_work": "Scaling the Secular City, Consciousness and the Existence of God", + "contribution": "Known for his work in philosophy of religion, philosophy of mind, and apologetics. Defends a broadly evangelical Christian worldview." + }, + "Michael Dummett": { + "name": "Michael Dummett", + "nationality": "British", + "background": "Philosopher", + "famous_work": "Frege: Philosophy of Language, The Logical Basis of Metaphysics", + "contribution": "Major figure in philosophy of language, logic, and mathematics. Known for his work on the philosophy of Gottlob Frege, anti-realism, and the theory of meaning." + } +} \ No newline at end of file diff --git a/crack-core/data/names/philosophers.txt b/crack-core/data/names/philosophers.txt new file mode 100644 index 000000000..c918ddef2 --- /dev/null +++ b/crack-core/data/names/philosophers.txt @@ -0,0 +1,163 @@ + +Al-Farabi +Al-Ghazali +Alain Badiou +Alasdair MacIntyre +Albertus Magnus +Alvin Plantinga +Amartya Sen +Anaxagoras +Anaximander +Anaximenes +Aristotle +Arthur Schopenhauer +Averroes (Ibn Rushd) +Avicenna (Ibn Sina) +Axel Honneth +Baruch Spinoza +Bas van Fraassen +Bernard Williams +Bertrand Russell +Boethius +Charles Taylor +Christine Korsgaard +Chrysippus +Cicero +Claude Lévi-Strauss +Cornel West +Daniel Dennett +David Chalmers +David Hume +David Lewis +Derek Parfit +Desiderius Erasmus +Diogenes of Sinope +Donald Davidson +Edmund Husserl +Elizabeth Anscombe +Emmanuel Levinas +Emmanuel Mounier +Empedocles +Epictetus +Epicurus +Erich Fromm +Francis Bacon +Frank Jackson +Friedrich Nietzsche +Friedrich Wilhelm Joseph Schelling +Félix Guattari +Gabriel Marcel +Galen Strawson +Gayatri Chakravorty Spivak +Georg Wilhelm Friedrich Hegel +George Berkeley +Gilbert Ryle +Gilles Deleuze +Giovanni Pico della Mirandola +Gorgias +Hannah Arendt +Hans-Georg Gadamer +Harry Frankfurt +Henri Bergson +Heraclitus +Herbert Marcuse +Herbert Spencer +Hilary Putnam +Hubert Dreyfus +Hélène Cixous +Ian Hacking +Immanuel Kant +Iris Marion Young +Iris Murdoch +Isaiah Berlin +Jacques Derrida +Jean Baudrillard +Jean-Jacques Rousseau +Jean-Paul Sartre +Jerry Fodor +Johann Gottlieb Fichte +John Duns Scotus +John Locke +John McDowell +John Rawls +John Searle +John Stuart Mill +Judith Butler +Julia Kristeva +Jürgen Habermas +Karl Jaspers +Karl Marx +Karl Popper +Kit Fine +Kwame Anthony Appiah +Luce Irigaray +Lucretius +Ludwig Wittgenstein +Maimonides +Marcus Aurelius +Marsilio Ficino +Martha Nussbaum +Martin Buber +Martin Heidegger +Mary Midgley +Maurice Merleau-Ponty +Max Horkheimer +Meister Eckhart +Michael Sandel +Michael Walzer +Michel Foucault +Michel de Montaigne +Nancy Cartwright +Nancy Fraser +Niccolò Machiavelli +Nicholas of Cusa +Parmenides +Patricia Churchland +Paul Churchland +Paul Feyerabend +Paul Ricoeur +Paul Tillich +Peter Singer +Peter Strawson +Philippa Foot +Pierre Bourdieu +Plato +Plotinus +Porphyry +Proclus +Protagoras +Pyrrho +Pythagoras +Reinhold Niebuhr +René Descartes +Richard Dawkins +Richard Rorty +Richard Swinburne +Robert Brandom +Robert Nozick +Ronald Dworkin +Ruth Barcan Marcus +Saul Kripke +Seneca +Sextus Empiricus +Seyla Benhabib +Simone Weil +Simone de Beauvoir +Slavoj Žižek +Socrates +Stanley Cavell +Susan Wolf +Søren Kierkegaard +Thales of Miletus +Theodor Adorno +Thomas Aquinas +Thomas Hobbes +Thomas Kuhn +Thomas Metzinger +Thomas Nagel +Timothy Williamson +Walter Benjamin +Willard Van Orman Quine +William of Ockham +Zeno of Citium +Zeno of Elea \ No newline at end of file diff --git a/crack-core/src/commands/admin/authorize.rs b/crack-core/src/commands/admin/authorize.rs index ced1ac79b..3616b3512 100644 --- a/crack-core/src/commands/admin/authorize.rs +++ b/crack-core/src/commands/admin/authorize.rs @@ -4,7 +4,8 @@ use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; use crate::Error; -use poise::serenity_prelude::UserId; +use poise::serenity_prelude::Mentionable; +use serenity::all::User; /// Utilizes the permissions v2 `required_permissions` field #[poise::command(slash_command, required_permissions = "ADMINISTRATOR")] @@ -19,30 +20,27 @@ pub async fn check_admin(ctx: Context<'_>) -> Result<(), Error> { #[cfg(not(tarpaulin_include))] pub async fn authorize( ctx: Context<'_>, - #[description = "The user id to add to authorized list"] user_id: UserId, + #[description = "The user to add to authorized list"] user: User, ) -> Result<(), Error> { // let id = user_id.parse::().expect("Failed to parse user id"); - let id = user_id.get(); + let mention = user.mention(); + let id = user.id; let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - // let data = ctx.data(); let guild_settings = ctx .data() .guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { - e.authorized_users.insert(id, 0); + e.authorized_users.insert(id.get(), 0); }) .or_insert({ - let settings = GuildSettings::new( - ctx.guild_id().unwrap(), - Some(&ctx.data().bot_settings.get_prefix()), - None, - ) - .authorize_user(id.try_into().unwrap()) - .clone(); + let settings = + GuildSettings::new(guild_id, Some(&ctx.data().bot_settings.get_prefix()), None) + .authorize_user(id.into()) + .clone(); settings }) .clone(); @@ -53,30 +51,22 @@ pub async fn authorize( .ok_or(CrackedError::Other("No database pool"))?; guild_settings.save(&pool).await?; - let user_id = UserId::new(id); - let user_name = ctx - .http() - .get_user(user_id) - .await - .map(|u| u.name) - .unwrap_or_else(|_| "Unknown".to_string()); let guild_name = guild_id .to_partial_guild(ctx.http()) .await .map(|g| g.name) .unwrap_or_else(|_| "Unknown".to_string()); - send_response_poise( + let msg = send_response_poise( ctx, CrackedMessage::UserAuthorized { - user_id, - user_name, + id, + mention, guild_id, guild_name, }, true, ) - .await - .map(|m| ctx.data().add_msg_to_cache(guild_id, m)) - .map(|_| ()) - .map_err(Into::into) + .await?; + ctx.data().add_msg_to_cache(guild_id, msg); + Ok(()) } diff --git a/crack-core/src/commands/admin/ban.rs b/crack-core/src/commands/admin/ban.rs index aef761fc9..4d9d510b3 100644 --- a/crack-core/src/commands/admin/ban.rs +++ b/crack-core/src/commands/admin/ban.rs @@ -3,6 +3,7 @@ use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; use crate::Error; +use poise::serenity_prelude::Mentionable; use serenity::all::User; /// Ban a user from the server. @@ -29,6 +30,8 @@ pub async fn ban( #[description = "Reason to the ban."] reason: Option, ) -> Result<(), Error> { + let mention = user.mention(); + let id = user.id; let dmd = dmd.unwrap_or(0); let reason = reason.unwrap_or("No reason provided".to_string()); let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; @@ -43,15 +46,7 @@ pub async fn ban( .await?; } else { // Send success message - send_response_poise( - ctx, - CrackedMessage::UserBanned { - user: user.name.clone(), - user_id: user.clone().id, - }, - true, - ) - .await?; + send_response_poise(ctx, CrackedMessage::UserBanned { mention, id }, true).await?; } Ok(()) } diff --git a/crack-core/src/commands/admin/broadcast_voice.rs b/crack-core/src/commands/admin/broadcast_voice.rs index 4e81c2bf0..ba6139c0e 100644 --- a/crack-core/src/commands/admin/broadcast_voice.rs +++ b/crack-core/src/commands/admin/broadcast_voice.rs @@ -1,8 +1,5 @@ -use serenity::builder::CreateMessage; - -use crate::utils::get_current_voice_channel_id; -use crate::Context; -use crate::Error; +use crate::{Context, ContextExt, Error}; +use serenity::all::CreateMessage; /// Broadcast a message to all guilds where the bot is currently in a voice channel. #[poise::command(prefix_command, owners_only, ephemeral)] @@ -12,22 +9,17 @@ pub async fn broadcast_voice( #[description = "The message to broadcast"] message: String, ) -> Result<(), Error> { - let data = ctx.data(); - let http = ctx.http(); - let serenity_ctx = ctx.serenity_context().clone(); - let guilds = data.guild_settings_map.read().unwrap().clone(); + let guilds = ctx.data().guild_settings_map.read().await.clone(); for (guild_id, _settings) in guilds.iter() { - let message = message.clone(); - - let channel_id_opt = get_current_voice_channel_id(&serenity_ctx, *guild_id).await; - - if let Some(channel_id) = channel_id_opt { - channel_id - .send_message(&http, CreateMessage::new().content(message.clone())) - .await - .unwrap(); - } + match ctx.get_active_channel_id(*guild_id).await { + Some(channel_id) => { + let _ = channel_id + .send_message(ctx.http(), CreateMessage::new().content(message.clone())) + .await; + }, + None => tracing::warn!("No active channel for guild_id: {}", guild_id), + }; } Ok(()) diff --git a/crack-core/src/commands/admin/deafen.rs b/crack-core/src/commands/admin/deafen.rs index 8630ba7b3..2fcb2d072 100644 --- a/crack-core/src/commands/admin/deafen.rs +++ b/crack-core/src/commands/admin/deafen.rs @@ -1,8 +1,13 @@ +use std::sync::Arc; + use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; use crate::Error; +use serenity::all::Context as SerenityContext; +use serenity::all::GuildId; +use serenity::all::Mentionable; use serenity::builder::EditMember; /// Deafen a user. @@ -16,33 +21,77 @@ use serenity::builder::EditMember; )] pub async fn deafen( ctx: Context<'_>, - #[rest] - #[description = "User to deafen"] - user: serenity::model::user::User, + #[description = "User to deafen"] user: serenity::model::user::User, ) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; - if let Err(e) = guild_id - .edit_member(&ctx, user.clone().id, EditMember::new().deafen(true)) + let crack_msg = deafen_internal( + Arc::new(ctx.serenity_context().clone()), + guild_id, + user.clone(), + true, + ) + .await?; + // Handle error, send error message + let sent_msg = send_response_poise(ctx, crack_msg, true).await?; + ctx.data().add_msg_to_cache(guild_id, sent_msg); + Ok(()) +} + +/// Uneafen a user. +#[cfg(not(tarpaulin_include))] +#[poise::command( + slash_command, + prefix_command, + guild_only, + required_permissions = "ADMINISTRATOR", + ephemeral +)] +pub async fn undeafen( + ctx: Context<'_>, + #[description = "User to undeafen"] user: serenity::model::user::User, +) -> Result<(), Error> { + let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; + let crack_msg = deafen_internal( + Arc::new(ctx.serenity_context().clone()), + guild_id, + user.clone(), + false, + ) + .await?; + // Handle error, send error message + let sent_msg = send_response_poise(ctx, crack_msg, true).await?; + ctx.data().add_msg_to_cache(guild_id, sent_msg); + Ok(()) +} + +/// Deafen or undeafen a user. +pub async fn deafen_internal( + ctx: Arc, + guild_id: GuildId, + user: serenity::model::user::User, + deafen: bool, +) -> Result { + let mention = user.clone().mention(); + let id = user.clone().id; + let msg = if let Err(e) = guild_id + .edit_member(&ctx, user.clone().id, EditMember::new().deafen(deafen)) .await { - // Handle error, send error message - send_response_poise( - ctx, - CrackedMessage::Other(format!("Failed to deafen user: {}", e)), - true, - ) - .await?; + let msg = if deafen { + CrackedMessage::UserDeafenedFail { mention, id } + } else { + CrackedMessage::UserUndeafenedFail { mention, id } + }; + tracing::error!("{msg}\n{e}"); + msg } else { - // Send success message - send_response_poise( - ctx, - CrackedMessage::UserMuted { - user: user.name.clone(), - user_id: user.clone().id, - }, - true, - ) - .await?; - } - Ok(()) + let msg = if deafen { + CrackedMessage::UserDeafened { mention, id } + } else { + CrackedMessage::UserUndeafened { mention, id } + }; + tracing::info!("{msg}"); + msg + }; + Ok(msg) } diff --git a/crack-core/src/commands/admin/deauthorize.rs b/crack-core/src/commands/admin/deauthorize.rs index 4b67dcc3f..616d2f3a7 100644 --- a/crack-core/src/commands/admin/deauthorize.rs +++ b/crack-core/src/commands/admin/deauthorize.rs @@ -2,7 +2,7 @@ use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; use crate::Error; -use poise::serenity_prelude::UserId; +use poise::serenity_prelude::Mentionable; /// Deauthorize a user from using the bot. #[cfg(not(tarpaulin_include))] @@ -14,21 +14,16 @@ use poise::serenity_prelude::UserId; )] pub async fn deauthorize( ctx: Context<'_>, - #[description = "The user id to remove from the authorized list"] user_id: UserId, + #[description = "The user id to remove from the authorized list"] user: serenity::all::User, ) -> Result<(), Error> { - let id = user_id.get(); + let user_id = user.id; + let id = user_id; let guild_id = ctx.guild_id().unwrap(); // TODO: Test to see how expensive this is. // TODO: Make this into a function, it's used other places. - let user_name = ctx - .http() - .get_user(user_id) - .await - .map(|u| u.name) - .unwrap_or_else(|_| "Unknown".to_string()); let guild_name = guild_id - .to_partial_guild(ctx.http()) + .to_partial_guild(ctx) .await .map(|g| g.name) .unwrap_or_else(|_| "Unknown".to_string()); @@ -37,14 +32,14 @@ pub async fn deauthorize( .data() .guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|settings| { - settings.authorized_users.remove(&id); + settings.authorized_users.remove(&id.get()); }) .or_insert({ crate::guild::settings::GuildSettings::new( - ctx.guild_id().unwrap(), + guild_id, Some(&ctx.data().bot_settings.get_prefix()), Some(guild_name.clone()), ) @@ -53,11 +48,12 @@ pub async fn deauthorize( .clone(); tracing::info!("User Deauthorized: UserId = {}, GuildId = {}", id, res); + let mention = user.mention(); let msg = send_response_poise( ctx, CrackedMessage::UserDeauthorized { - user_id, - user_name, + id, + mention, guild_id, guild_name, }, diff --git a/crack-core/src/commands/admin/kick.rs b/crack-core/src/commands/admin/kick.rs index e906e65af..c435f5c3d 100644 --- a/crack-core/src/commands/admin/kick.rs +++ b/crack-core/src/commands/admin/kick.rs @@ -1,9 +1,10 @@ use crate::errors::CrackedError; +use crate::guild::operations::GuildSettingsOperations; use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; use crate::Error; -use serenity::all::{Mentionable, UserId}; +use serenity::all::{Mentionable, User}; use serenity::builder::EditMember; use std::fs::read_to_string; use std::time::Duration; @@ -16,29 +17,25 @@ use std::time::Duration; ephemeral, required_permissions = "ADMINISTRATOR" )] -pub async fn kick(ctx: Context<'_>, user_id: UserId) -> Result<(), Error> { +pub async fn kick( + ctx: Context<'_>, + #[description = "User to kick."] user: User, +) -> Result<(), Error> { + let mention = user.mention(); + let id = user.id; let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; - let reply_with_embed = ctx - .data() - .get_guild_settings(guild_id) - .map(|x| x.reply_with_embed) - .ok_or(CrackedError::Other("No guild settings"))?; + let as_embed = ctx.data().get_reply_with_embed(guild_id).await; let guild = guild_id.to_partial_guild(&ctx).await?; - if let Err(e) = guild.kick(&ctx, user_id).await { + if let Err(e) = guild.kick(&ctx, id).await { send_response_poise( ctx, CrackedMessage::Other(format!("Failed to kick user: {}", e)), - reply_with_embed, + as_embed, ) .await?; } else { // Send success message - send_response_poise( - ctx, - CrackedMessage::UserKicked { user_id }, - reply_with_embed, - ) - .await?; + send_response_poise(ctx, CrackedMessage::UserKicked { id, mention }, as_embed).await?; } Ok(()) } diff --git a/crack-core/src/commands/admin/mod.rs b/crack-core/src/commands/admin/mod.rs index 1fa8deb52..5ce84429a 100644 --- a/crack-core/src/commands/admin/mod.rs +++ b/crack-core/src/commands/admin/mod.rs @@ -63,13 +63,13 @@ pub use unmute::*; "defend", "deauthorize", "delete_channel", - // "track_invites", "kick", "rename_all", "mute", "message_cache", "move_users_to", "unban", + "undeafen", "unmute", "random_mute", "get_active_vcs", diff --git a/crack-core/src/commands/admin/mute.rs b/crack-core/src/commands/admin/mute.rs index e43a90864..57878cfb1 100644 --- a/crack-core/src/commands/admin/mute.rs +++ b/crack-core/src/commands/admin/mute.rs @@ -1,12 +1,12 @@ -use std::sync::Arc; - use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; use crate::Error; +use poise::serenity_prelude::Mentionable; use serenity::all::EditMember; use serenity::all::{Context as SerenityContext, GuildId}; +use std::sync::Arc; /// Mute a user. #[poise::command( @@ -20,7 +20,7 @@ pub async fn mute( #[description = "User to mute"] user: serenity::model::user::User, ) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - let crack_msg = mute_impl( + let crack_msg = mute_internal( Arc::new(ctx.serenity_context().clone()), user, guild_id, @@ -34,12 +34,14 @@ pub async fn mute( } /// Unmute a user. -pub async fn mute_impl( +pub async fn mute_internal( ctx: Arc, user: serenity::model::user::User, guild_id: GuildId, mute: bool, ) -> Result { + let mention = user.mention(); + let id = user.id; if let Err(e) = guild_id .edit_member(&ctx, user.clone().id, EditMember::new().mute(mute)) .await @@ -53,18 +55,6 @@ pub async fn mute_impl( Ok(CrackedMessage::Other(format!("Failed to mute user: {}", e))) } else { // Send success message - Ok(CrackedMessage::UserMuted { - user: user.name.clone(), - user_id: user.clone().id, - }) - - // send_response_poise( - // ctx, - // CrackedMessage::UserMuted { - // user: user.name.clone(), - // user_id: user.clone().id, - // }, - // ) - // .await + Ok(CrackedMessage::UserMuted { mention, id }) } } diff --git a/crack-core/src/commands/admin/random_mute_lol.rs b/crack-core/src/commands/admin/random_mute_lol.rs index e501c3ed0..be22d227f 100644 --- a/crack-core/src/commands/admin/random_mute_lol.rs +++ b/crack-core/src/commands/admin/random_mute_lol.rs @@ -52,7 +52,7 @@ pub struct RandomMuteHandler { pub guild_id: GuildId, } -use crate::commands::mute_impl; +use crate::commands::mute_internal; use async_trait::async_trait; #[async_trait] impl EventHandler for RandomMuteHandler { @@ -62,12 +62,12 @@ impl EventHandler for RandomMuteHandler { // let member = guild.member(&self.ctx, self.user.id).await.unwrap(); let r = rand::thread_rng().gen_range(0..100); if r < 50 { - let _msg = mute_impl(self.ctx.clone(), self.user.clone(), self.guild_id, true) + let _msg = mute_internal(self.ctx.clone(), self.user.clone(), self.guild_id, true) .await .unwrap(); // } else if r < 75 { } else { - let _msg = mute_impl(self.ctx.clone(), self.user.clone(), self.guild_id, false) + let _msg = mute_internal(self.ctx.clone(), self.user.clone(), self.guild_id, false) .await .unwrap(); } diff --git a/crack-core/src/commands/admin/role/create_role.rs b/crack-core/src/commands/admin/role/create_role.rs index deaa97b7d..186345273 100644 --- a/crack-core/src/commands/admin/role/create_role.rs +++ b/crack-core/src/commands/admin/role/create_role.rs @@ -1,23 +1,51 @@ +use poise::serenity_prelude::{Colour, Permissions}; +use serenity::all::{Attachment, CreateAttachment, GuildId, Role}; use serenity::builder::EditRole; -use crate::commands::ConvertToEmptyResult; +use crate::commands::{ConvertToEmptyResult, EmptyResult}; use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; -use crate::Error; /// Create role. +#[allow(clippy::too_many_arguments)] #[poise::command(prefix_command, owners_only, ephemeral)] pub async fn create( ctx: Context<'_>, - #[description = "Name of the role to create"] role_name: String, -) -> Result<(), Error> { + #[description = "Name of the role to create."] name: String, + #[description = "Whether the role is hoisted."] hoist: Option, + #[description = "Whether the role is mentionable."] mentionable: Option, + #[description = "Optional initial perms"] permissions: Option, + #[description = "Optional initial position (vertical)"] position: Option, + #[description = "Optional initial colour"] colour: Option, + #[description = "Optional emoji"] unicode_emoji: Option, + #[description = "Optional reason for the audit_log"] audit_log_reason: Option, + #[description = "Optional initial perms"] icon: Option, +) -> EmptyResult { let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; + let icon = match icon { + Some(attachment) => { + let url = attachment.url.clone(); + Some(CreateAttachment::url(ctx, &url).await?) + }, + None => None, + }; - let role = guild_id - .create_role(&ctx, EditRole::new().name(role_name)) - .await?; + let role = create_role_internal( + ctx, + guild_id, + name, + hoist, + mentionable, + permissions, + position, + colour, + unicode_emoji, + audit_log_reason, + icon.as_ref(), + ) + .await?; send_response_poise( ctx, @@ -30,3 +58,38 @@ pub async fn create( .await .convert() } + +/// Internal create role function. +#[allow(clippy::too_many_arguments)] +pub async fn create_role_internal( + ctx: Context<'_>, + guild_id: GuildId, + name: String, + hoist: Option, + mentionable: Option, + permissions: Option, + position: Option, + colour: Option, + unicode_emoji: Option, + audit_log_reason: Option, + icon: Option<&CreateAttachment>, +) -> Result { + let perms = Permissions::from_bits(permissions.unwrap_or_default()) + .ok_or(CrackedError::InvalidPermissions)?; + let colour = colour.map(Colour::new).unwrap_or_default(); + let audit_log_reason = audit_log_reason.unwrap_or_default(); + let role_builder = EditRole::default() + .name(name) + .hoist(hoist.unwrap_or_default()) + .mentionable(mentionable.unwrap_or_default()) + .permissions(Into::into(perms)) + .position(position.unwrap_or_default()) + .colour(colour) + .unicode_emoji(unicode_emoji) + .audit_log_reason(&audit_log_reason) + .icon(icon); + guild_id + .create_role(&ctx, role_builder) + .await + .map_err(Into::into) +} diff --git a/crack-core/src/commands/admin/set_vc_size.rs b/crack-core/src/commands/admin/set_vc_size.rs index 5201267fd..ec5efec8e 100644 --- a/crack-core/src/commands/admin/set_vc_size.rs +++ b/crack-core/src/commands/admin/set_vc_size.rs @@ -1,9 +1,13 @@ +use std::sync::Arc; + use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; +use crate::messaging::messages::UNKNOWN_LIT; use crate::utils::send_response_poise; use crate::Context; use crate::Error; -use serenity::all::ChannelId; +use serenity::all::Channel; +use serenity::all::Context as SerenityContext; use serenity::all::EditChannel; /// Set the size of a voice channel. @@ -15,11 +19,12 @@ use serenity::all::EditChannel; )] pub async fn set_vc_size( ctx: Context<'_>, - #[description = "VoiceChannel to edit"] channel: ChannelId, + #[description = "VoiceChannel to edit"] channel: Channel, #[description = "New max size"] size: u32, ) -> Result<(), Error> { let _guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let _res = channel + .id() .edit(&ctx, EditChannel::new().user_limit(size)) .await?; send_response_poise( @@ -31,3 +36,27 @@ pub async fn set_vc_size( .map(|_| ()) .map_err(Into::into) } + +/// Set the size of a voice channel. +pub async fn set_vc_size_internal( + ctx: Arc, + channel: Channel, + size: u32, +) -> Result { + let id = channel.id(); + let name = id + .name(&ctx) + .await + .unwrap_or_else(|_| UNKNOWN_LIT.to_string()); + if let Err(e) = id.edit(&ctx, EditChannel::new().user_limit(size)).await { + Err(CrackedError::FailedToSetChannelSize( + name, + id, + size, + e.into(), + )) + } else { + // Send success message + Ok(CrackedMessage::ChannelSizeSet { name, id, size }) + } +} diff --git a/crack-core/src/commands/admin/timeout.rs b/crack-core/src/commands/admin/timeout.rs index 323c04205..69f04c455 100644 --- a/crack-core/src/commands/admin/timeout.rs +++ b/crack-core/src/commands/admin/timeout.rs @@ -3,8 +3,9 @@ use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; use crate::Error; +use poise::serenity_prelude::Mentionable; use regex::Regex; -use serenity::all::{User, UserId}; +use serenity::all::User; use serenity::builder::EditMember; use std::time::Duration; @@ -20,34 +21,17 @@ use std::time::Duration; )] pub async fn timeout( ctx: Context<'_>, - #[description = "User to timout."] user: Option, - #[description = "UserId to timeout"] user_id: Option, + #[description = "User to timout."] user: User, #[description = "Amount of time"] duration: String, ) -> Result<(), Error> { // Debugging print the params - tracing::error!( - "User: {:?}, User_id: {:?}, Duration: {}", - user, - user_id, - duration - ); + let id = user.id; + let mention = user.mention(); let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - tracing::error!("Guild_id: {}", guild_id); - - let user_id = { - if let Some(user) = user { - user.id - } else if let Some(user_id) = user_id { - user_id - } else { - return Err(CrackedError::Other("No user or user_id provided").into()); - } - }; - tracing::error!("User_id: {}", user_id); let timeout_duration = parse_duration(&duration)?; - tracing::error!("Timeout duration: {:?}", timeout_duration); + tracing::info!("Timeout duration: {:?}", timeout_duration); let now = chrono::Utc::now(); let timeout_until = now + timeout_duration; @@ -63,7 +47,7 @@ pub async fn timeout( if let Err(e) = guild .edit_member( &ctx, - user_id, + id, EditMember::default().disable_communication_until(timeout_until.clone()), ) .await @@ -79,8 +63,8 @@ pub async fn timeout( } else { // Send success message let msg = CrackedMessage::UserTimeout { - user: user_id.to_user(&ctx).await?.name, - user_id: format!("{}", user_id), + id, + mention, timeout_until: timeout_until.clone(), }; tracing::info!("User timed out: {}", msg); diff --git a/crack-core/src/commands/admin/unban.rs b/crack-core/src/commands/admin/unban.rs index 5713808af..079308668 100644 --- a/crack-core/src/commands/admin/unban.rs +++ b/crack-core/src/commands/admin/unban.rs @@ -1,12 +1,12 @@ -use serenity::all::GuildId; -use serenity::all::User; -use serenity::all::UserId; - use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; use crate::Error; +use poise::serenity_prelude::Mentionable; +use serenity::all::GuildId; +use serenity::all::User; +use serenity::all::UserId; /// Unban command #[poise::command( @@ -39,6 +39,8 @@ pub async fn unban_by_user_id( #[cfg(not(tarpaulin_include))] pub async fn unban_helper(ctx: Context<'_>, guild_id: GuildId, user: User) -> Result<(), Error> { let guild = guild_id.to_partial_guild(&ctx).await?; + let id = user.id; + let mention = user.mention(); if let Err(e) = guild.unban(&ctx, user.id).await { // Handle error, send error message send_response_poise( @@ -51,17 +53,10 @@ pub async fn unban_helper(ctx: Context<'_>, guild_id: GuildId, user: User) -> Re .map(|_| ()) } else { // Send success message - send_response_poise( - ctx, - CrackedMessage::UserUnbanned { - user: user.name.clone(), - user_id: user.id, - }, - true, - ) - .await - .map(|m| ctx.data().add_msg_to_cache(guild_id, m)) - .map(|_| ()) + send_response_poise(ctx, CrackedMessage::UserUnbanned { id, mention }, true) + .await + .map(|m| ctx.data().add_msg_to_cache(guild_id, m)) + .map(|_| ()) } .map_err(Into::into) } diff --git a/crack-core/src/commands/admin/undeafen.rs b/crack-core/src/commands/admin/undeafen.rs deleted file mode 100644 index 5e2d7b263..000000000 --- a/crack-core/src/commands/admin/undeafen.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::errors::CrackedError; -use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; -use crate::Context; -use crate::Error; -use serenity::builder::EditMember; - -/// Undeafen a user. -#[poise::command( - slash_command, - prefix_command, - required_permissions = "ADMINISTRATOR", - ephemeral -)] -#[cfg(not(tarpaulin_include))] -pub async fn undeafen( - ctx: Context<'_>, - #[description = "User to undeafen"] user: serenity::model::user::User, -) -> Result<(), Error> { - let guild = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - if let Err(e) = guild - .edit_member(&ctx, user.clone().id, EditMember::new().deafen(false)) - .await - { - // Handle error, send error message - send_response_poise( - ctx, - CrackedMessage::Other(format!("Failed to undeafen user: {}", e)), - ) - .await?; - } else { - // Send success message - send_response_poise( - ctx, - CrackedMessage::UserMuted { - user: user.name.clone(), - user_id: user.clone().id, - }, - ) - .await?; - } - Ok(()) -} diff --git a/crack-core/src/commands/admin/unmute.rs b/crack-core/src/commands/admin/unmute.rs index 2cad4d91b..3ef5c41eb 100644 --- a/crack-core/src/commands/admin/unmute.rs +++ b/crack-core/src/commands/admin/unmute.rs @@ -3,6 +3,7 @@ use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; use crate::Error; +use poise::serenity_prelude::Mentionable; use serenity::all::Message; use serenity::builder::EditMember; @@ -30,6 +31,8 @@ pub async fn unmute_impl( ctx: Context<'_>, user: serenity::model::user::User, ) -> Result { + let id = user.id; + let mention = user.mention(); let guild_id = ctx .guild_id() .ok_or(CrackedError::Other("Guild ID not found"))?; @@ -46,15 +49,7 @@ pub async fn unmute_impl( .await } else { // Send success message - send_response_poise( - ctx, - CrackedMessage::UserUnmuted { - user: user.name.clone(), - user_id: user.clone().id, - }, - true, - ) - .await + send_response_poise(ctx, CrackedMessage::UserUnmuted { id, mention }, true).await } .map_err(Into::into) } diff --git a/crack-core/src/commands/chatgpt.rs b/crack-core/src/commands/chatgpt.rs index b8203966e..4407080dc 100644 --- a/crack-core/src/commands/chatgpt.rs +++ b/crack-core/src/commands/chatgpt.rs @@ -1,24 +1,41 @@ use crate::utils::check_reply; -use crate::Context; -use crate::Error; -use crack_gpt::get_chatgpt_response; - -#[cfg(feature = "crack-gpt")] -/// Talk with chatgpt. -#[allow(unused)] -#[cfg(not(tarpaulin_include))] +use crate::{Context, Error}; +use crack_gpt::GptContext; +use poise::CreateReply; + +/// Chat with cracktunes using GPT-4o #[poise::command(slash_command, prefix_command)] -pub async fn chatgpt( +pub async fn chat( ctx: Context<'_>, #[rest] - #[description = "Query text to send to the model."] + #[description = "Query to send to the model."] query: String, ) -> Result<(), Error> { - use poise::{CreateReply, ReplyHandle}; - ctx.defer().await?; - let response = get_chatgpt_response(query).await?; + let user_id = ctx.author().id.get(); + + let data = ctx.data(); + + tracing::info!("chat: {}", query); + let lock = data.gpt_ctx.read().await; + let gpt_ctx = if lock.is_some() { + let res = lock.clone(); + drop(lock); + res.unwrap() + } else { + drop(lock); + let new_ctx = GptContext::default(); + *ctx.data().gpt_ctx.write().await = Some(new_ctx); + + ctx.data().gpt_ctx.read().await.clone().unwrap() + }; + + tracing::info!("chat: {:?}", gpt_ctx.cache_status(Some(user_id)).await); + + let response = gpt_ctx.openai_azure_response(query, user_id).await?; + + tracing::info!("chat: response: {}", response); check_reply( ctx.send(CreateReply::default().content(response).reply(true)) diff --git a/crack-core/src/commands/mod.rs b/crack-core/src/commands/mod.rs index 81351b6bb..2bc82b54b 100644 --- a/crack-core/src/commands/mod.rs +++ b/crack-core/src/commands/mod.rs @@ -23,10 +23,10 @@ use serenity::all::Message; pub use settings::*; pub use version::*; -use crate::{errors::CrackedError, Error}; +pub use crate::errors::CrackedError; pub type MessageResult = Result; -pub type EmptyResult = Result<(), Error>; +pub type EmptyResult = Result<(), crate::Error>; pub trait ConvertToEmptyResult { fn convert(self) -> EmptyResult; diff --git a/crack-core/src/commands/music/autopause.rs b/crack-core/src/commands/music/autopause.rs index a11db1970..ad08eeb19 100644 --- a/crack-core/src/commands/music/autopause.rs +++ b/crack-core/src/commands/music/autopause.rs @@ -24,9 +24,9 @@ pub async fn autopause_internal(ctx: Context<'_>) -> Result<(), Error> { let prefix = Some(prefix.as_str()); let mut guild_settings = ctx .data() - .get_or_create_guild_settings(guild_id, name, prefix); + .get_or_create_guild_settings(guild_id, name, prefix) + .await; guild_settings.toggle_autopause(); - // guild_settings.save(&pool).await?; guild_settings.autopause }; let msg = if autopause { diff --git a/crack-core/src/commands/music/autoplay.rs b/crack-core/src/commands/music/autoplay.rs index 1155ab345..d8662dc50 100644 --- a/crack-core/src/commands/music/autoplay.rs +++ b/crack-core/src/commands/music/autoplay.rs @@ -7,8 +7,8 @@ use crate::{messaging::message::CrackedMessage, utils::send_response_poise, Cont pub async fn autoplay(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); - let autoplay = ctx.data().get_autoplay(guild_id); - ctx.data().set_autoplay(guild_id, !autoplay); + let autoplay = ctx.data().get_autoplay(guild_id).await; + ctx.data().set_autoplay(guild_id, !autoplay).await; let msg = if autoplay { send_response_poise(ctx, CrackedMessage::AutoplayOff, true) diff --git a/crack-core/src/commands/music/doplay.rs b/crack-core/src/commands/music/doplay.rs index 6e0f14538..583cbd7bc 100644 --- a/crack-core/src/commands/music/doplay.rs +++ b/crack-core/src/commands/music/doplay.rs @@ -1,68 +1,41 @@ -use colored; -use rusty_ytdl::search::Playlist; +use ::serenity::all::CommandInteraction; -use super::doplay_utils::enqueue_track_pgwrite; -use super::doplay_utils::insert_track; -use super::doplay_utils::queue_keyword_list; - -use crate::commands::doplay_utils::queue_yt_playlist; -use crate::commands::doplay_utils::rotate_tracks; -use crate::commands::doplay_utils::{get_mode, get_msg, queue_keyword_list_w_offset}; +use super::play_utils::query::QueryType; +use super::play_utils::queue::{get_mode, get_msg, queue_track_back}; use crate::commands::get_call_with_fail_msg; +use crate::commands::play_utils::query::query_type_from_url; use crate::sources::rusty_ytdl::RustyYoutubeClient; +//FIXME +use crate::utils::edit_embed_response2; use crate::{ - commands::skip::force_skip_top_track, errors::{verify, CrackedError}, - guild::settings::{GuildSettings, DEFAULT_PREMIUM}, + guild::settings::GuildSettings, handlers::track_end::update_queue_messages, - http_utils, - interface::create_now_playing_embed, + messaging::interface::create_now_playing_embed, messaging::{ message::CrackedMessage, messages::{ - PLAY_QUEUE, PLAY_TOP, QUEUE_NO_SRC, QUEUE_NO_TITLE, SPOTIFY_AUTH_FAILED, - TRACK_DURATION, TRACK_TIME_TO_PLAY, + PLAY_QUEUE, PLAY_TOP, QUEUE_NO_SRC, QUEUE_NO_TITLE, TRACK_DURATION, TRACK_TIME_TO_PLAY, }, }, - sources::spotify::{Spotify, SpotifyTrack, SPOTIFY}, - utils::{ - compare_domains, edit_response_poise, get_guild_name, get_human_readable_timestamp, - get_interaction, get_track_metadata, send_embed_response_poise, send_response_poise_text, - }, + sources::spotify::SpotifyTrack, + sources::youtube::build_query_aux_metadata, + utils::{get_human_readable_timestamp, get_track_metadata, send_embed_response_poise}, Context, Error, }; use ::serenity::{ - all::{ - ChannelId, ComponentInteractionDataKind, Context as SerenityContext, EmbedField, GuildId, - Mentionable, Message, UserId, - }, - builder::{ - CreateAttachment, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, - CreateInteractionResponse, CreateInteractionResponseMessage, CreateMessage, - CreateSelectMenu, CreateSelectMenuKind, CreateSelectMenuOption, EditInteractionResponse, - EditMessage, - }, + all::{Message, UserId}, + builder::{CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, EditMessage}, }; -use poise::serenity_prelude::{self as serenity, Attachment}; -use reqwest::Client; -use rusty_ytdl::search::{SearchOptions, SearchType}; +use poise::serenity_prelude as serenity; use songbird::{ - input::{AuxMetadata, Compose, HttpRequest, Input as SongbirdInput, YoutubeDl}, + input::{AuxMetadata, YoutubeDl}, tracks::TrackHandle, Call, }; -use std::{ - cmp::{min, Ordering}, - collections::HashMap, - path::Path, - process::{Output, Stdio}, - sync::Arc, - time::Duration, -}; -use tokio::process::Command; +use std::{cmp::Ordering, sync::Arc, time::Duration}; use tokio::sync::Mutex; use typemap_rev::TypeMapKey; -use url::Url; #[derive(Clone, Copy, Debug, PartialEq)] pub enum Mode { @@ -77,19 +50,6 @@ pub enum Mode { Search, } -#[derive(Clone, Debug)] -pub enum QueryType { - Keywords(String), - KeywordList(Vec), - VideoLink(String), - SpotifyTracks(Vec), - PlaylistLink(String), - File(serenity::Attachment), - NewYoutubeDl((YoutubeDl, AuxMetadata)), - YoutubeSearch(String), - None, -} - /// Get the guild name. #[cfg(not(tarpaulin_include))] #[poise::command(prefix_command, slash_command, guild_only)] @@ -175,10 +135,8 @@ async fn play_internal( file: Option, query_or_url: Option, ) -> Result<(), Error> { - // let search_msg = send_search_message(ctx).await?; let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; // FIXME: This should be generalized. - let prefix = ctx.prefix(); let is_prefix = ctx.prefix() != "/"; let msg = get_msg(mode.clone(), query_or_url, is_prefix); @@ -209,10 +167,10 @@ async fn play_internal( tracing::debug!("search response msg: {:?}", search_msg); - let call = get_call_with_fail_msg(ctx, guild_id).await?; + let call = get_call_with_fail_msg(ctx).await?; // determine whether this is a link or a query string - let query_type = get_query_type_from_url(ctx, url, file).await?; + let query_type = query_type_from_url(ctx, url, file).await?; // FIXME: Decide whether we're using this everywhere, or not. // Don't like the inconsistency. @@ -224,23 +182,26 @@ async fn play_internal( tracing::warn!("query_type: {:?}", query_type); // FIXME: Super hacky, fix this shit. + // This is actually where the track gets queued into the internal queue, it's the main work function. let move_on = match_mode(ctx, call.clone(), mode, query_type.clone(), &mut search_msg).await?; + // FIXME: Yeah, this is terrible, fix this. if !move_on { return Ok(()); } - let _volume = { - let mut settings = ctx.data().guild_settings_map.write().unwrap(); // .clone(); - let guild_settings = settings.entry(guild_id).or_insert_with(|| { - GuildSettings::new( - guild_id, - Some(prefix), - get_guild_name(ctx.serenity_context(), guild_id), - ) - }); - guild_settings.volume - }; + // FIXME: What was the point of this again? + // let _volume = { + // let mut settings = ctx.data().guild_settings_map.write().await; // .clone(); + // let guild_settings = settings.entry(guild_id).or_insert_with(|| { + // GuildSettings::new( + // guild_id, + // Some(prefix), + // get_guild_name(ctx.serenity_context(), guild_id), + // ) + // }); + // guild_settings.volume + // }; // let queue = call.lock().await.queue().current_queue().clone(); // tracing::warn!("guild_settings: {:?}", guild_settings); @@ -252,6 +213,11 @@ async fn play_internal( // queue.iter().for_each(|t| t.set_volume(volume).unwrap()); drop(handler); + // This makes sense, we're getting the final response to the user based on whether + // the song / playlist was queued first, last, or is now playing. + // Ah! Also, sometimes after a long queue process the now playing message says that it's already + // X seconds into the song, so this is definitely after the section of the code that + // takes a long time. let embed = match queue.len().cmp(&1) { Ordering::Greater => { let estimated_time = calculate_time_until_play(&queue, mode).await.unwrap(); @@ -302,7 +268,6 @@ async fn play_internal( tracing::warn!("Only one track in queue, just playing it."); let track = queue.first().unwrap(); create_now_playing_embed(track).await - // print_queue(queue).await; }, Ordering::Less => { tracing::warn!("No tracks in queue, this only happens when an interactive search is done with an empty queue."); @@ -312,773 +277,78 @@ async fn play_internal( }, }; - edit_embed_response(ctx, embed, search_msg.clone()) + edit_embed_response2(ctx, embed, search_msg.clone()) .await .map(|_| ()) } - -/// Edit the embed response of the given message. -#[cfg(not(tarpaulin_include))] -async fn edit_embed_response( - ctx: Context<'_>, - embed: CreateEmbed, - mut msg: Message, -) -> Result { - match get_interaction(ctx) { - Some(interaction) => interaction - .edit_response( - &ctx.serenity_context().http, - EditInteractionResponse::new().add_embed(embed), - ) - .await - .map_err(Into::into), - None => msg - .edit( - ctx.serenity_context().http.clone(), - EditMessage::new().embed(embed), - ) - .await - .map(|_| msg) - .map_err(Into::into), - } -} - -#[allow(dead_code)] -/// Print the current queue to the logs -async fn print_queue(queue: Vec) { - for track in queue.iter() { - let metadata = get_track_metadata(track).await; - tracing::warn!( - "Track {}: {} - {}, State: {:?}, Ready: {:?}", - metadata.title.unwrap_or("".to_string()).red(), - metadata.artist.unwrap_or("".to_string()).white(), - metadata.album.unwrap_or("".to_string()).blue(), - track.get_info().await.unwrap().playing, - track.get_info().await.unwrap().ready, - ); - } -} - -/// Download a file and upload it as an mp3. -async fn download_file_ytdlp_mp3(url: &str) -> Result<(Output, AuxMetadata), Error> { - let metadata = YoutubeDl::new( - reqwest::ClientBuilder::new().use_rustls_tls().build()?, - url.to_string(), - ) - .aux_metadata() - .await?; - - let args = [ - "--extract-audio", - "--audio-format", - "mp3", - "--audio-quality", - "0", - url, - ]; - let child = Command::new("yt-dlp") - .args(args) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .unwrap(); - - tracing::warn!("yt-dlp"); - - let output = child.wait_with_output().await?; - Ok((output, metadata)) -} - -/// Download a file and upload it as an attachment. -async fn download_file_ytdlp(url: &str, mp3: bool) -> Result<(Output, AuxMetadata), Error> { - if mp3 || url.contains("youtube.com") { - return download_file_ytdlp_mp3(url).await; - } - - let metadata = YoutubeDl::new(http_utils::get_client().clone(), url.to_string()) - .aux_metadata() - .await?; - - let child = Command::new("yt-dlp") - .arg(url) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .unwrap(); - - tracing::warn!("yt-dlp"); - - let output = child.wait_with_output().await?; - Ok((output, metadata)) -} - -async fn yt_search_select( - ctx: SerenityContext, - channel_id: ChannelId, - metadata: Vec, -) -> Result { - let res = metadata.iter().map(|x| { - let title = x.title.clone().unwrap_or_default(); - let link = x.source_url.clone().unwrap_or_default(); - let duration = x.duration.unwrap_or_default(); - let elem = format!("{}: {}", duration_to_string(duration), title); - let len = min(elem.len(), 99); - let elem = elem[..len].to_string(); - tracing::warn!("elem: {}", elem); - (elem, link) - }); - let rev_map = res - .clone() - .map(|(elem, link)| (link, elem)) - .collect::>(); - // Ask the user for its favorite animal - let m = channel_id - .send_message( - &ctx, - CreateMessage::new().content("Search results").select_menu( - CreateSelectMenu::new( - "song_select", - CreateSelectMenuKind::String { - options: res - .map(|(x, y)| CreateSelectMenuOption::new(x, y)) - .collect(), - }, - ) - .custom_id("song_select") - .placeholder("Select Song to Play"), - ), - ) - .await?; - - // Wait for the user to make a selection - // This uses a collector to wait for an incoming event without needing to listen for it - // manually in the EventHandler. - let interaction = match m - .await_component_interaction(&ctx.shard) - .timeout(Duration::from_secs(60 * 3)) - .await - { - Some(x) => x, - None => { - m.reply(&ctx, "Timed out").await.unwrap(); - return Err(CrackedError::Other("Timed out").into()); - }, - }; - - // data.values contains the selected value from each select menus. We only have one menu, - // so we retrieve the first - let url = match &interaction.data.kind { - ComponentInteractionDataKind::StringSelect { values } => &values[0], - _ => panic!("unexpected interaction data kind"), - }; - - tracing::error!("url: {}", url); - - let qt = QueryType::VideoLink(url.to_string()); - tracing::error!("url: {:?}", qt); - - // Acknowledge the interaction and edit the message - let res = interaction - .create_response( - &ctx, - CreateInteractionResponse::UpdateMessage( - CreateInteractionResponseMessage::default().content(CrackedMessage::SongQueued { - title: rev_map.get(url).unwrap().to_string(), - url: url.to_owned(), - }), - ), - ) - .await - .map_err(|e| e.into()) - .map(|_| qt); - - m.delete(&ctx).await.unwrap(); - res - // // Wait for multiple interactions - // let mut interaction_stream = m - // .await_component_interaction(&ctx.shard) - // .timeout(Duration::from_secs(60 * 3)) - // .stream(); - - // while let Some(interaction) = interaction_stream.next().await { - // let sound = &interaction.data.custom_id; - // // Acknowledge the interaction and send a reply - // interaction - // .create_response( - // &ctx, - // // This time we dont edit the message but reply to it - // CreateInteractionResponse::Message( - // CreateInteractionResponseMessage::default() - // // Make the message hidden for other users by setting `ephemeral(true)`. - // .ephemeral(true) - // .content(format!("The **{animal}** says __{sound}__")), - // ), - // ) - // .await - // .unwrap(); - // } - - // // Delete the orig message or there will be dangling components (components that still - // // exist, but no collector is running so any user who presses them sees an error) +pub enum MessageOrInteraction { + Message(Message), + Interaction(CommandInteraction), } -async fn create_embed_fields(elems: Vec) -> Vec { - tracing::warn!("num elems: {:?}", elems.len()); - let mut fields = vec![]; - // let tmp = "".to_string(); - for elem in elems.into_iter() { - let title = elem.title.unwrap_or_default(); - let link = elem.source_url.unwrap_or_default(); - let duration = elem.duration.unwrap_or_default(); - let elem = format!("({}) - {}", link, duration_to_string(duration)); - fields.push(EmbedField::new(format!("[{}]", title), elem, true)); +pub async fn get_user_message_if_prefix(ctx: Context<'_>) -> MessageOrInteraction { + match ctx { + Context::Prefix(ctx) => MessageOrInteraction::Message(ctx.msg.clone()), + Context::Application(ctx) => MessageOrInteraction::Interaction(ctx.interaction.clone()), } - fields -} - -/// Convert a duration to a string. -pub fn duration_to_string(duration: Duration) -> String { - let mut secs = duration.as_secs(); - let hours = secs / 3600; - secs %= 3600; - let minutes = secs / 60; - secs %= 60; - format!("{:02}:{:02}:{:02}", hours, minutes, secs) -} - -/// Send the search results to the user. -async fn send_search_response( - ctx: Context<'_>, - guild_id: GuildId, - user_id: UserId, - query: String, - res: Vec, -) -> Result { - let author = ctx.author_member().await.unwrap(); - let name = if DEFAULT_PREMIUM { - author.mention().to_string() - } else { - author.display_name().to_string() - }; - - let now_time_str = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); - let fields = create_embed_fields(res).await; - let author = CreateEmbedAuthor::new(name); - let title = format!("Search results for: {}", query); - let footer = CreateEmbedFooter::new(format!("{} * {} * {}", user_id, guild_id, now_time_str)); - let embed = CreateEmbed::new() - .author(author) - .title(title) - .footer(footer) - .fields(fields.into_iter().map(|f| (f.name, f.value, f.inline))); - - send_embed_response_poise(ctx, embed).await } +/// This is what actually does the majority of the work of the function. +/// It finds the track that the user wants to play and then actually +/// does the process of queuing it. This needs to be optimized. async fn match_mode<'a>( ctx: Context<'_>, call: Arc>, mode: Mode, query_type: QueryType, search_msg: &'a mut Message, -) -> Result { - // let is_prefix = ctx.prefix() != "/"; - // let user_id = ctx.author().id; - // let user_id_i64 = ctx.author().id.get() as i64; - let handler = call.lock().await; - let queue_was_empty = handler.queue().is_empty(); - let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - drop(handler); - - // let pool = ctx.data().database_pool.clone().unwrap(); - +) -> Result { tracing::info!("mode: {:?}", mode); - let reqwest_client = ctx.data().http_client.clone(); match mode { - Mode::Search => { - // let search_results = match query_type.clone() { - match query_type.clone() { - QueryType::Keywords(keywords) => { - let search_results = YoutubeDl::new_search(reqwest_client, keywords) - .search(None) - .await?; - // let user_id = ctx.author().id; - let qt = yt_search_select( - ctx.serenity_context().clone(), - ctx.channel_id(), - search_results, - ) - .await?; - let queue = enqueue_track_pgwrite(ctx, &call, &qt).await?; - update_queue_messages( - &ctx.serenity_context().http, - ctx.data(), - &queue, - guild_id, - ) - .await - // match_mode(ctx, call.clone(), Mode::End, qt).await - // send_search_response(ctx, guild_id, user_id, keywords, search_results).await?; - }, - QueryType::YoutubeSearch(query) => { - let search_results = YoutubeDl::new(reqwest_client, query.clone()) - .search(None) - .await?; - let qt = yt_search_select( - ctx.serenity_context().clone(), - ctx.channel_id(), - search_results, - ) - .await?; - // match_mode(ctx, call.clone(), Mode::End, qt).await - let queue = enqueue_track_pgwrite(ctx, &call, &qt).await?; - update_queue_messages( - &ctx.serenity_context().http, - ctx.data(), - &queue, - guild_id, - ) - .await - - // let user_id = ctx.author().id; - - // send_search_response(ctx, guild_id, user_id, query, search_results).await?; - }, - _ => { - let embed = CreateEmbed::default() - .description(format!( - "{}", - CrackedError::Other("Something went wrong while parsing your query!") - )) - .footer(CreateEmbedFooter::new("Search failed!")); - send_embed_response_poise(ctx, embed).await?; - return Ok(false); - }, - }; - }, - Mode::DownloadMKV => { - let (status, file_name) = - get_download_status_and_filename(query_type.clone(), false).await?; - ctx.channel_id() - .send_message( - ctx.http(), - CreateMessage::new() - .content(format!("Download status {}", status)) - .add_file(CreateAttachment::path(Path::new(&file_name)).await?), - ) - .await?; - - return Ok(false); - }, - Mode::DownloadMP3 => { - let (status, file_name) = - get_download_status_and_filename(query_type.clone(), true).await?; - ctx.channel_id() - .send_message( - ctx.http(), - CreateMessage::new() - .content(format!("Download status {}", status)) - .add_file(CreateAttachment::path(Path::new(&file_name)).await?), - ) - .await?; - - return Ok(false); - }, - Mode::End => match query_type.clone() { - QueryType::YoutubeSearch(query) => { - tracing::trace!("Mode::Jump, QueryType::YoutubeSearch"); - - let res = YoutubeDl::new_search(http_utils::get_client().clone(), query.clone()) - .search(None) - .await?; - let user_id = ctx.author().id; - send_search_response(ctx, guild_id, user_id, query.clone(), res).await?; - }, - QueryType::Keywords(_) | QueryType::VideoLink(_) | QueryType::NewYoutubeDl(_) => { - tracing::warn!("### Mode::End, QueryType::Keywords | QueryType::VideoLink"); - let queue = enqueue_track_pgwrite(ctx, &call, &query_type).await?; - update_queue_messages(&ctx.serenity_context().http, ctx.data(), &queue, guild_id) - .await; - }, - // FIXME - QueryType::PlaylistLink(url) => { - tracing::trace!("Mode::End, QueryType::PlaylistLink"); - // Let's use the new YouTube rust library for this - let rusty_ytdl = RustyYoutubeClient::new()?; - let playlist: Playlist = rusty_ytdl.get_playlist(url).await?; - queue_yt_playlist(ctx, call, guild_id, playlist, search_msg).await?; - }, - QueryType::SpotifyTracks(tracks) => { - let keywords_list = tracks - .iter() - .map(|x| x.build_query()) - .collect::>(); - queue_keyword_list(ctx, call, keywords_list, search_msg).await?; - }, - QueryType::KeywordList(keywords_list) => { - tracing::trace!("Mode::End, QueryType::KeywordList"); - queue_keyword_list(ctx, call, keywords_list, search_msg).await?; - }, - QueryType::File(file) => { - tracing::trace!("Mode::End, QueryType::File"); - let queue = enqueue_track_pgwrite(ctx, &call, &QueryType::File(file)).await?; - update_queue_messages(ctx.http(), ctx.data(), &queue, guild_id).await; - }, - QueryType::None => { - tracing::trace!("Mode::End, QueryType::None"); - let embed = CreateEmbed::default() - .description(format!("{}", CrackedError::Other("No query provided!"))) - .footer(CreateEmbedFooter::new("No query provided!")); - send_embed_response_poise(ctx, embed).await?; - return Ok(false); - }, - }, - Mode::Next => match query_type.clone() { - QueryType::Keywords(_) - | QueryType::VideoLink(_) - | QueryType::File(_) - | QueryType::NewYoutubeDl(_) => { - tracing::trace!( - "Mode::Next, QueryType::Keywords | QueryType::VideoLink | QueryType::File" - ); - let queue = insert_track(ctx, &call, &query_type, 1).await?; - update_queue_messages(&ctx.serenity_context().http, ctx.data(), &queue, guild_id) - .await; - }, - // FIXME - QueryType::PlaylistLink(_url) => { - tracing::trace!("Mode::Next, QueryType::PlaylistLink"); - // let urls = YouTubeRestartable::ytdl_playlist(&url, mode) - // .await - // .ok_or(CrackedError::Other("failed to fetch playlist"))?; - let urls = vec!["".to_string()]; - - for (idx, url) in urls.into_iter().enumerate() { - let queue = - insert_track(ctx, &call, &QueryType::VideoLink(url), idx + 1).await?; - update_queue_messages( - &ctx.serenity_context().http, - ctx.data(), - &queue, - guild_id, - ) - .await; - } - }, - QueryType::KeywordList(keywords_list) => { - tracing::trace!("Mode::Next, QueryType::KeywordList"); - let q_not_empty = if call.clone().lock().await.queue().is_empty() { - 0 - } else { - 1 - }; - queue_keyword_list_w_offset(ctx, call, keywords_list, q_not_empty, search_msg) - .await?; - }, - QueryType::SpotifyTracks(tracks) => { - tracing::trace!("Mode::Next, QueryType::KeywordList"); - let q_not_empty = if call.clone().lock().await.queue().is_empty() { - 0 - } else { - 1 - }; - let keywords_list = tracks - .iter() - .map(|x| x.build_query()) - .collect::>(); - queue_keyword_list_w_offset(ctx, call, keywords_list, q_not_empty, search_msg) - .await?; - }, - QueryType::YoutubeSearch(_) => { - tracing::trace!("Mode::Next, QueryType::YoutubeSearch"); - return Err(CrackedError::Other("Not implemented yet!").into()); - }, - QueryType::None => { - tracing::trace!("Mode::Next, QueryType::None"); - let embed = CreateEmbed::default() - .description(format!("{}", CrackedError::Other("No query provided!"))) - .footer(CreateEmbedFooter::new("No query provided!")); - send_embed_response_poise(ctx, embed).await?; - return Ok(false); - }, - }, - Mode::Jump => match query_type.clone() { - QueryType::YoutubeSearch(query) => { - tracing::trace!("Mode::Jump, QueryType::YoutubeSearch"); - tracing::error!("query: {}", query); - return Err(CrackedError::Other("Not implemented yet!").into()); - }, - QueryType::Keywords(_) - | QueryType::VideoLink(_) - | QueryType::File(_) - | QueryType::NewYoutubeDl(_) => { - tracing::trace!( - "Mode::Jump, QueryType::Keywords | QueryType::VideoLink | QueryType::File" - ); - let mut queue = enqueue_track_pgwrite(ctx, &call, &query_type).await?; - - if !queue_was_empty { - rotate_tracks(&call, 1).await.ok(); - queue = force_skip_top_track(&call.lock().await).await?; - } - - update_queue_messages(&ctx.serenity_context().http, ctx.data(), &queue, guild_id) - .await; - }, - QueryType::PlaylistLink(url) => { - tracing::error!("Mode::Jump, QueryType::PlaylistLink"); - // let urls = YouTubeRestartable::ytdl_playlist(&url, mode) - // .await - // .ok_or(CrackedError::PlayListFail)?; - // FIXME - let _src = YoutubeDl::new(Client::new(), url); - // .ok_or(CrackedError::Other("failed to fetch playlist"))? - // .into_iter() - // .for_each(|track| async { - // let _ = enqueue_track(&call, &QueryType::File(track)).await; - // }); - let urls = vec!["".to_string()]; - let mut insert_idx = 1; - - for (i, url) in urls.into_iter().enumerate() { - let mut queue = - insert_track(ctx, &call, &QueryType::VideoLink(url), insert_idx).await?; - - if i == 0 && !queue_was_empty { - queue = force_skip_top_track(&call.lock().await).await?; - } else { - insert_idx += 1; - } - - update_queue_messages( - &ctx.serenity_context().http, - ctx.data(), - &queue, - guild_id, - ) - .await; - } - }, - // FIXME - QueryType::SpotifyTracks(tracks) => { - let mut insert_idx = 1; - let keywords_list = tracks - .iter() - .map(|x| x.build_query()) - .collect::>(); - - for (i, keywords) in keywords_list.into_iter().enumerate() { - let mut queue = - insert_track(ctx, &call, &QueryType::Keywords(keywords), insert_idx) - .await?; - - if i == 0 && !queue_was_empty { - queue = force_skip_top_track(&call.lock().await).await?; - } else { - insert_idx += 1; - } - - update_queue_messages( - &ctx.serenity_context().http, - ctx.data(), - &queue, - guild_id, - ) - .await; - } - }, - // FIXME - QueryType::KeywordList(keywords_list) => { - tracing::error!("Mode::Jump, QueryType::KeywordList"); - let mut insert_idx = 1; - - for (i, keywords) in keywords_list.into_iter().enumerate() { - let mut queue = - insert_track(ctx, &call, &QueryType::Keywords(keywords), insert_idx) - .await?; - - if i == 0 && !queue_was_empty { - queue = force_skip_top_track(&call.lock().await).await?; - } else { - insert_idx += 1; - } - - update_queue_messages( - &ctx.serenity_context().http, - ctx.data(), - &queue, - guild_id, - ) - .await; - } - }, - QueryType::None => { - tracing::trace!("Mode::Next, QueryType::None"); - let embed = CreateEmbed::default() - .description(format!("{}", CrackedError::Other("No query provided!"))) - .footer(CreateEmbedFooter::new("No query provided!")); - send_embed_response_poise(ctx, embed).await?; - return Ok(false); - }, - }, - Mode::All | Mode::Reverse | Mode::Shuffle => match query_type.clone() { - QueryType::VideoLink(url) | QueryType::PlaylistLink(url) => { - tracing::trace!("Mode::All | Mode::Reverse | Mode::Shuffle, QueryType::VideoLink | QueryType::PlaylistLink"); - // FIXME - let mut src = YoutubeDl::new(http_utils::get_client().clone(), url); - let metadata = src.aux_metadata().await?; - enqueue_track_pgwrite(ctx, &call, &QueryType::NewYoutubeDl((src, metadata))) - .await?; - update_queue_messages( - &ctx.serenity_context().http, - ctx.data(), - &call.lock().await.queue().current_queue(), - guild_id, - ) - .await; - }, - QueryType::KeywordList(keywords_list) => { - tracing::trace!( - "Mode::All | Mode::Reverse | Mode::Shuffle, QueryType::KeywordList" - ); - queue_keyword_list(ctx, call, keywords_list, search_msg).await?; - }, - QueryType::SpotifyTracks(tracks) => { - tracing::trace!( - "Mode::All | Mode::Reverse | Mode::Shuffle, QueryType::KeywordList" - ); - let keywords_list = tracks - .iter() - .map(|x| x.build_query()) - .collect::>(); - queue_keyword_list(ctx, call, keywords_list, search_msg).await?; - }, - _ => { - ctx.defer().await?; // Why did I do this? - edit_response_poise(ctx, CrackedMessage::PlayAllFailed).await?; - return Ok(false); - }, + Mode::Search => query_type + .mode_search(ctx, call) + .await + .map(|x| !x.is_empty()), + Mode::DownloadMKV => query_type.mode_download(ctx, false).await, + Mode::DownloadMP3 => query_type.mode_download(ctx, true).await, + Mode::End => query_type.mode_end(ctx, call, search_msg).await, + Mode::Next => query_type.mode_next(ctx, call, search_msg).await, + Mode::Jump => query_type.mode_jump(ctx, call).await, + Mode::All | Mode::Reverse | Mode::Shuffle => { + query_type.mode_rest(ctx, call, search_msg).await }, } - - Ok(true) } -use colored::Colorize; -/// Matches a url (or query string) to a QueryType -pub async fn get_query_type_from_url( - ctx: Context<'_>, - url: &str, - file: Option, -) -> Result, Error> { - // determine whether this is a link or a query string - tracing::warn!("url: {}", url); - let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - - let query_type = match Url::parse(url) { - Ok(url_data) => match url_data.host_str() { - Some("open.spotify.com") | Some("spotify.link") => { - let final_url = http_utils::resolve_final_url(url).await?; - tracing::warn!("spotify: {} -> {}", url, final_url); - let spotify = SPOTIFY.lock().await; - let spotify = verify(spotify.as_ref(), CrackedError::Other(SPOTIFY_AUTH_FAILED))?; - Some(Spotify::extract(spotify, &final_url).await?) - }, - Some("cdn.discordapp.com") => { - tracing::warn!("{}: {}", "attachement file".blue(), url.underline().blue()); - Some(QueryType::File(file.unwrap())) - }, - - Some(other) => { - let mut settings = ctx.data().guild_settings_map.write().unwrap().clone(); - let guild_settings = settings.entry(guild_id).or_insert_with(|| { - GuildSettings::new( - guild_id, - Some(ctx.prefix()), - get_guild_name(ctx.serenity_context(), guild_id), - ) - }); - if !guild_settings.allow_all_domains.unwrap_or(true) { - let is_allowed = guild_settings - .allowed_domains - .iter() - .any(|d| compare_domains(d, other)); - - let is_banned = guild_settings - .banned_domains - .iter() - .any(|d| compare_domains(d, other)); - - if is_banned || (guild_settings.banned_domains.is_empty() && !is_allowed) { - let message = CrackedMessage::PlayDomainBanned { - domain: other.to_string(), - }; - - send_response_poise_text(ctx, message).await?; - } - } - - // Handle youtube playlist - if url.contains("list=") { - tracing::warn!("{}: {}", "youtube playlist".blue(), url.underline().blue()); - Some(QueryType::PlaylistLink(url.to_string())) - } else { - tracing::warn!("{}: {}", "youtube video".blue(), url.underline().blue()); - let rusty_ytdl = RustyYoutubeClient::new()?; - let info = rusty_ytdl.get_video_info(url.to_string()).await?; - let metadata = RustyYoutubeClient::video_info_to_aux_metadata(&info); - let yt = YoutubeDl::new(http_utils::get_client().clone(), url.to_string()); - Some(QueryType::NewYoutubeDl((yt, metadata))) - } - }, - None => { - // handle spotify:track:3Vr5jdQHibI2q0A0KW4RWk format? - // TODO: Why is this a thing? - if url.starts_with("spotify:") { - let parts = url.split(':').collect::>(); - let final_url = - format!("https://open.spotify.com/track/{}", parts.last().unwrap()); - tracing::warn!("spotify: {} -> {}", url, final_url); - let spotify = SPOTIFY.lock().await; - let spotify = - verify(spotify.as_ref(), CrackedError::Other(SPOTIFY_AUTH_FAILED))?; - Some(Spotify::extract(spotify, &final_url).await?) - } else { - Some(QueryType::Keywords(url.to_string())) - // None - } - }, - }, - Err(e) => { - tracing::error!("Url::parse error: {}", e); - Some(QueryType::Keywords(url.to_string())) - }, - }; - - let res = if let Some(QueryType::Keywords(_)) = query_type { - let settings = ctx.data().guild_settings_map.write().unwrap().clone(); - let guild_settings = settings.get(&guild_id).unwrap(); +/// Check if the domain that we're playing from is banned. +// FIXME: This is borked. +pub fn check_banned_domains( + guild_settings: &GuildSettings, + query_type: Option, +) -> Result, CrackedError> { + if let Some(QueryType::Keywords(_)) = query_type { if !guild_settings.allow_all_domains.unwrap_or(true) && (guild_settings.banned_domains.contains("youtube.com") || (guild_settings.banned_domains.is_empty() && !guild_settings.allowed_domains.contains("youtube.com"))) { - let message = CrackedMessage::PlayDomainBanned { - domain: "youtube.com".to_string(), - }; + // let message = CrackedMessage::PlayDomainBanned { + // domain: "youtube.com".to_string(), + // }; - send_response_poise_text(ctx, message).await?; - Ok(None) + // send_response_poise_text(ctx, message).await?; + // Ok(None) + Err(CrackedError::Other("youtube.com is banned")) } else { - Result::Ok(query_type) + Ok(query_type) } } else { - Result::Ok(query_type) - }; - res + Ok(query_type) + } } +/// Calculate the time until the next track plays. async fn calculate_time_until_play(queue: &[TrackHandle], mode: Mode) -> Option { if queue.is_empty() { return None; @@ -1189,12 +459,21 @@ impl MyAuxMetadata { }) } + /// Set the source_url. pub fn with_source_url(self, source_url: String) -> Self { MyAuxMetadata::Data(AuxMetadata { source_url: Some(source_url), ..self.metadata().clone() }) } + + /// Get a search query from the metadata for youtube. + pub fn get_search_query(&self) -> String { + let metadata = self.metadata(); + let title = metadata.title.clone().unwrap_or_default(); + let artist = metadata.artist.clone().unwrap_or_default(); + format!("{} {}", title, artist) + } } /// Implementation to convert `[&SpotifyTrack]` to `[MyAuxMetadata]`. @@ -1246,288 +525,6 @@ async fn build_queued_embed( .footer(CreateEmbedFooter::new(footer_text)) } -// FIXME: Do you want to have a reqwest client we keep around and pass into -// this instead of creating a new one every time? -// FIXME: This is super expensive, literally we need to do this a lot better. -async fn get_download_status_and_filename( - query_type: QueryType, - mp3: bool, -) -> Result<(bool, String), Error> { - // FIXME: Don't hardcode this. - let prefix = "/data/downloads"; - let extension = if mp3 { "mp3" } else { "webm" }; - let client = http_utils::get_client().clone(); - tracing::warn!("query_type: {:?}", query_type); - match query_type { - QueryType::YoutubeSearch(_) => Err(Box::new(CrackedError::Other( - "Download not valid with search results.", - ))), - QueryType::VideoLink(url) => { - tracing::warn!("Mode::Download, QueryType::VideoLink"); - let (output, metadata) = download_file_ytdlp(&url, mp3).await?; - let status = output.status.success(); - let url = metadata.source_url.unwrap(); - let file_name = format!( - "{}/{} [{}].{}", - prefix, - metadata.title.unwrap(), - url.split('=').last().unwrap(), - extension, - ); - Ok((status, file_name)) - }, - QueryType::NewYoutubeDl((_src, metadata)) => { - tracing::warn!("Mode::Download, QueryType::NewYoutubeDl"); - let url = metadata.source_url.unwrap(); - let file_name = format!( - "{}/{} [{}].{}", - prefix, - metadata.title.unwrap(), - url.split('=').last().unwrap(), - extension, - ); - tracing::warn!("file_name: {}", file_name); - let (output, _metadata) = download_file_ytdlp(&url, mp3).await?; - let status = output.status.success(); - Ok((status, file_name)) - }, - QueryType::Keywords(query) => { - tracing::warn!("In Keywords"); - let mut ytdl = YoutubeDl::new(client, format!("ytsearch:{}", query)); - let metadata = ytdl.aux_metadata().await.unwrap(); - let url = metadata.source_url.unwrap(); - let (output, metadata) = download_file_ytdlp(&url, mp3).await?; - - let file_name = format!( - "{}/{} [{}].{}", - prefix, - metadata.title.unwrap(), - url.split('=').last().unwrap(), - extension, - ); - let status = output.status.success(); - Ok((status, file_name)) - }, - QueryType::File(file) => { - tracing::warn!("In File"); - Ok((true, file.url.to_owned().to_string())) - }, - QueryType::PlaylistLink(url) => { - tracing::warn!("In PlaylistLink"); - let (output, metadata) = download_file_ytdlp(&url, mp3).await?; - let file_name = format!( - "{}/{} [{}].{}", - prefix, - metadata.title.unwrap(), - url.split('=').last().unwrap(), - extension, - ); - let status = output.status.success(); - Ok((status, file_name)) - }, - QueryType::SpotifyTracks(tracks) => { - tracing::warn!("In SpotifyTracks"); - let keywords_list = tracks - .iter() - .map(|x| x.build_query()) - .collect::>(); - let url = format!("ytsearch:{}", keywords_list.first().unwrap()); - let mut ytdl = YoutubeDl::new(client, url.clone()); - let metadata = ytdl.aux_metadata().await.unwrap(); - let (output, _metadata) = download_file_ytdlp(&url, mp3).await?; - let file_name = format!( - "{}/{} [{}].{}", - prefix, - metadata.title.unwrap(), - url.split('=').last().unwrap(), - extension, - ); - let status = output.status.success(); - Ok((status, file_name)) - }, - QueryType::KeywordList(keywords_list) => { - tracing::warn!("In KeywordList"); - let url = format!("ytsearch:{}", keywords_list.join(" ")); - let mut ytdl = YoutubeDl::new(client, url.clone()); - tracing::warn!("ytdl: {:?}", ytdl); - let metadata = ytdl.aux_metadata().await.unwrap(); - let (output, _metadata) = download_file_ytdlp(&url, mp3).await?; - let file_name = format!( - "{}/{} [{}].{}", - prefix, - metadata.title.unwrap(), - url.split('=').last().unwrap(), - extension, - ); - let status = output.status.success(); - Ok((status, file_name)) - }, - QueryType::None => Err(Box::new(CrackedError::Other("No query provided!"))), - } -} - -pub async fn search_query_to_source_and_metadata_ytdl( - client: reqwest::Client, - query: String, -) -> Result<(SongbirdInput, Vec), CrackedError> { - let mut ytdl = YoutubeDl::new(client, query); - let metadata = ytdl.aux_metadata().await?; - let my_metadata = MyAuxMetadata::Data(metadata); - - Ok((ytdl.into(), vec![my_metadata])) -} - -pub async fn search_query_to_source_and_metadata( - client: reqwest::Client, - query: String, -) -> Result<(SongbirdInput, Vec), CrackedError> { - tracing::warn!("search_query_to_source_and_metadata: {:?}", query); - let metadata = { - let rytdl = RustyYoutubeClient::new_with_client(client.clone())?; - tracing::warn!("search_query_to_source_and_metadata: {:?}", rytdl); - let results = rytdl.one_shot(query.clone()).await?; - tracing::warn!("search_query_to_source_and_metadata: {:?}", results); - // FIXME: Fallback to yt-dlp - let result = match results { - Some(r) => r, - None => return Err(CrackedError::EmptySearchResult), - }; - let metadata = &RustyYoutubeClient::search_result_to_aux_metadata(&result); - metadata.clone() - }; - let source_url = match metadata.clone().source_url { - Some(url) => url.clone(), - None => "".to_string(), - }; - let ytdl = YoutubeDl::new(client, source_url); - let my_metadata = MyAuxMetadata::Data(metadata); - - Ok((ytdl.into(), vec![my_metadata])) -} - -pub async fn video_info_to_source_and_metadata( - client: reqwest::Client, - url: String, -) -> Result<(SongbirdInput, Vec), CrackedError> { - let rytdl = RustyYoutubeClient::new_with_client(client.clone())?; - let video_info = rytdl.get_video_info(url.clone()).await?; - let metadata = RustyYoutubeClient::video_info_to_aux_metadata(&video_info); - let my_metadata = MyAuxMetadata::Data(metadata); - - let ytdl = YoutubeDl::new(client, url); - Ok((ytdl.into(), vec![my_metadata])) -} - -// FIXME: Do you want to have a reqwest client we keep around and pass into -// this instead of creating a new one every time? -pub async fn get_track_source_and_metadata( - query_type: QueryType, -) -> Result<(SongbirdInput, Vec), CrackedError> { - let client = http_utils::get_client().clone(); - tracing::warn!("{}", format!("query_type: {:?}", query_type).red()); - match query_type { - QueryType::YoutubeSearch(query) => { - tracing::error!("In YoutubeSearch"); - let mut ytdl = YoutubeDl::new_search(client, query); - let mut res = Vec::new(); - let asdf = ytdl.search(None).await?; - for metadata in asdf { - let my_metadata = MyAuxMetadata::Data(metadata); - res.push(my_metadata); - } - Ok((ytdl.into(), res)) - }, - QueryType::VideoLink(query) => { - tracing::warn!("In VideoLink"); - video_info_to_source_and_metadata(client.clone(), query).await - // let mut ytdl = YoutubeDl::new(client, query); - // tracing::warn!("ytdl: {:?}", ytdl); - // let metadata = ytdl.aux_metadata().await?; - // let my_metadata = MyAuxMetadata::Data(metadata); - // Ok((ytdl.into(), vec![my_metadata])) - }, - QueryType::Keywords(query) => { - tracing::warn!("In Keywords"); - let res = search_query_to_source_and_metadata(client.clone(), query.clone()).await; - match res { - Ok((input, metadata)) => Ok((input, metadata)), - Err(_) => { - tracing::error!("falling back to ytdl!"); - search_query_to_source_and_metadata_ytdl(client.clone(), query).await - }, - } - }, - QueryType::File(file) => { - tracing::warn!("In File"); - Ok(( - HttpRequest::new(client, file.url.to_owned()).into(), - vec![MyAuxMetadata::default()], - )) - }, - QueryType::NewYoutubeDl(ytdl) => { - tracing::warn!("In NewYoutubeDl {:?}", ytdl.0); - Ok((ytdl.0.into(), vec![MyAuxMetadata::Data(ytdl.1)])) - }, - QueryType::PlaylistLink(url) => { - tracing::warn!("In PlaylistLink"); - let rytdl = RustyYoutubeClient::new_with_client(client.clone()).unwrap(); - let search_options = SearchOptions { - limit: 100, - search_type: SearchType::Playlist, - ..Default::default() - }; - - let res = rytdl - .rusty_ytdl - .search(&url, Some(&search_options)) - .await - .unwrap(); - let mut metadata = Vec::with_capacity(res.len()); - for r in res { - metadata.push(MyAuxMetadata::Data( - RustyYoutubeClient::search_result_to_aux_metadata(&r), - )); - } - let ytdl = YoutubeDl::new(client.clone(), url); - tracing::warn!("ytdl: {:?}", ytdl); - Ok((ytdl.into(), metadata)) - }, - QueryType::SpotifyTracks(tracks) => { - tracing::warn!("In KeywordList"); - let keywords_list = tracks - .iter() - .map(|x| x.build_query()) - .collect::>(); - let mut ytdl = YoutubeDl::new( - client, - format!("ytsearch:{}", keywords_list.first().unwrap()), - ); - tracing::warn!("ytdl: {:?}", ytdl); - let metdata = ytdl.aux_metadata().await.unwrap(); - let my_metadata = MyAuxMetadata::Data(metdata); - Ok((ytdl.into(), vec![my_metadata])) - }, - QueryType::KeywordList(keywords_list) => { - tracing::warn!("In KeywordList"); - let mut ytdl = YoutubeDl::new(client, format!("ytsearch:{}", keywords_list.join(" "))); - tracing::warn!("ytdl: {:?}", ytdl); - let metdata = ytdl.aux_metadata().await.unwrap(); - let my_metadata = MyAuxMetadata::Data(metdata); - Ok((ytdl.into(), vec![my_metadata])) - }, - QueryType::None => unimplemented!(), - } -} - -/// Build a query from AuxMetadata. -pub fn build_query_aux_metadata(aux_metadata: &AuxMetadata) -> String { - format!( - "{} - {}", - aux_metadata.artist.clone().unwrap_or_default(), - aux_metadata.track.clone().unwrap_or_default(), - ) -} - /// Add tracks to the queue from aux_metadata. #[cfg(not(tarpaulin_include))] pub async fn queue_aux_metadata( @@ -1569,58 +566,10 @@ pub async fn queue_aux_metadata( ); let query_type = QueryType::NewYoutubeDl((ytdl, metadata_final.metadata().clone())); - let queue = enqueue_track_pgwrite(ctx, &call, &query_type).await?; - update_queue_messages(&ctx.serenity_context().http, ctx.data(), &queue, guild_id).await; + let _ = queue_track_back(ctx, &call, &query_type).await?; } + let queue = call.lock().await.queue().current_queue(); + update_queue_messages(&ctx, ctx.data(), &queue, guild_id).await; Ok(()) } - -#[cfg(test)] -mod test { - use super::*; - use rspotify::model::{FullTrack, SimplifiedAlbum}; - - #[test] - fn test_from_spotify_track() { - let track = SpotifyTrack::new(FullTrack { - id: None, - name: "asdf".to_string(), - artists: vec![], - album: SimplifiedAlbum { - album_type: None, - album_group: None, - artists: vec![], - available_markets: vec![], - external_urls: HashMap::new(), - href: None, - id: None, - images: vec![], - name: "zxcv".to_string(), - release_date: Some("2012".to_string()), - release_date_precision: None, - restrictions: None, - }, - track_number: 0, - disc_number: 0, - explicit: false, - external_urls: HashMap::new(), - href: None, - preview_url: None, - popularity: 0, - is_playable: None, - linked_from: None, - restrictions: None, - external_ids: HashMap::new(), - is_local: false, - available_markets: vec![], - duration: chrono::TimeDelta::new(60, 0).unwrap(), - }); - let res = MyAuxMetadata::from_spotify_track(&track); - let metadata = res.metadata(); - assert_eq!(metadata.title, Some("asdf".to_string())); - assert_eq!(metadata.artist, Some("".to_string())); - assert_eq!(metadata.album, Some("zxcv".to_string())); - assert_eq!(metadata.duration.unwrap().as_secs(), 60); - } -} diff --git a/crack-core/src/commands/music/dosearch.rs b/crack-core/src/commands/music/dosearch.rs index 7bcf476c1..6e402286f 100644 --- a/crack-core/src/commands/music/dosearch.rs +++ b/crack-core/src/commands/music/dosearch.rs @@ -1,4 +1,6 @@ -use crate::{errors::CrackedError, interface::create_search_results_reply, Context, Error}; +use crate::{ + errors::CrackedError, messaging::interface::create_search_results_reply, Context, Error, +}; use poise::ReplyHandle; use reqwest::Client; use serenity::builder::CreateEmbed; diff --git a/crack-core/src/commands/music/leave.rs b/crack-core/src/commands/music/leave.rs index a7bd06e99..d2f20c482 100644 --- a/crack-core/src/commands/music/leave.rs +++ b/crack-core/src/commands/music/leave.rs @@ -2,18 +2,42 @@ use crate::{ errors::CrackedError, messaging::message::CrackedMessage, utils::send_response_poise, Context, Error, }; +use songbird::error::JoinError; -/// Leave the voice channel. +/// Leave a voice channel. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only, aliases("dc", "fuckoff"))] +#[poise::command( + prefix_command, + slash_command, + guild_only, + aliases("dc", "fuckoff", "fuck off") +)] pub async fn leave(ctx: Context<'_>) -> Result<(), Error> { + leave_internal(ctx).await +} + +/// Leave a voice channel. Actually impl. +pub async fn leave_internal(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let manager = songbird::get(ctx.serenity_context()) .await .ok_or(CrackedError::NotConnected)?; - manager.remove(guild_id).await?; + // check if we're actually in a call + let crack_msg = match manager.remove(guild_id).await { + Ok(()) => { + tracing::info!("Driver successfully removed."); + CrackedMessage::Leaving + }, + Err(err) => { + tracing::error!("Driver could not be removed: {}", err); + match err { + JoinError::NoCall => CrackedMessage::CrackedError(CrackedError::NotConnected), + _ => return Err(err.into()), + } + }, + }; - let msg = send_response_poise(ctx, CrackedMessage::Leaving, true).await?; + let msg = send_response_poise(ctx, crack_msg, true).await?; ctx.data().add_msg_to_cache(guild_id, msg); Ok(()) } diff --git a/crack-core/src/commands/music/lyrics.rs b/crack-core/src/commands/music/lyrics.rs index cd311e781..c66cdf7ce 100644 --- a/crack-core/src/commands/music/lyrics.rs +++ b/crack-core/src/commands/music/lyrics.rs @@ -1,63 +1,45 @@ -use std::sync::Arc; -use tokio::sync::Mutex; - -use lyric_finder::LyricResult; -use serenity::{all::GuildId, async_trait}; - use crate::{ - commands::MyAuxMetadata, errors::CrackedError, interface::create_lyrics_embed, Context, Error, + commands::MyAuxMetadata, errors::CrackedError, http_utils, + messaging::interface::create_lyrics_embed, Context, ContextExt, Error, }; -#[async_trait] -pub trait LyricFinderClient { - async fn get_lyric(&self, query: &str) -> anyhow::Result; -} - -#[async_trait] -impl LyricFinderClient for lyric_finder::Client { - async fn get_lyric(&self, query: &str) -> anyhow::Result { - self.get_lyric(query).await - } -} - -/// Search for song lyrics. #[cfg(not(tarpaulin_include))] #[poise::command(prefix_command, slash_command, guild_only)] +/// Search for song lyrics. pub async fn lyrics( ctx: Context<'_>, #[rest] #[description = "The query to search for"] query: Option, ) -> Result<(), Error> { - // The artist field seems to really just get in the way as it's the literal youtube channel name - // in many cases. - // let search_artist = track_handle.metadata().artist.clone().unwrap_or_default(); let query = query_or_title(ctx, query).await?; tracing::warn!("searching for lyrics for {}", query); - let lyric_finder_client = lyric_finder::Client::new(); - let (track, artists, lyric) = do_lyric_query(lyric_finder_client, query).await?; - create_lyrics_embed(ctx, artists, track, lyric) - .await - .map_err(Into::into) + let client = lyric_finder::Client::from_http_client(http_utils::get_client()); + + let res = client.get_lyric(&query).await?; + create_lyrics_embed(ctx, res).await.map_err(Into::into) } -/// Get the current call. -pub async fn get_call(ctx: Context<'_>) -> Result>, Error> { - let guild_id = get_guild_id(ctx).ok_or(CrackedError::NoGuildId)?; - let manager = songbird::get(ctx.serenity_context()) - .await - .ok_or(CrackedError::NotConnected)?; - let call = manager.get(guild_id).ok_or(CrackedError::NotConnected)?; - Ok(call) +#[cfg(not(tarpaulin_include))] +/// Internal function for searching for song lyrics. +pub async fn lyrics_internal(ctx: Context<'_>, query: Option) -> Result<(), Error> { + let query = query_or_title(ctx, query).await?; + tracing::warn!("searching for lyrics for {}", query); + + let client = lyric_finder::Client::from_http_client(http_utils::get_client()); + + let res = client.get_lyric(&query).await?; + create_lyrics_embed(ctx, res).await.map_err(Into::into) } /// Get the current track name as either the query or the title of the current track. +#[cfg(not(tarpaulin_include))] pub async fn query_or_title(ctx: Context<'_>, query: Option) -> Result { match query { Some(query) => Ok(query), None => { - let call = get_call(ctx).await?; + let call = ctx.get_call().await?; let handler = call.lock().await; let track_handle = handler .queue() @@ -66,148 +48,127 @@ pub async fn query_or_title(ctx: Context<'_>, query: Option) -> Result().unwrap(); - let track_opt = data.track.clone(); - let title_opt = data.title.clone(); - if let Some(t) = track_opt { - Ok(t) - } else if let Some(t) = title_opt { - Ok(t) - } else { - Err(CrackedError::NoTrackName.into()) - } + tracing::info!("data: {:?}", data); + data.track + .clone() + .or(data.title.clone()) + .ok_or(CrackedError::NoTrackName.into()) }, } } -pub async fn do_lyric_query( - client: impl LyricFinderClient, - query: String, -) -> Result<(String, String, String), Error> { - let result = match client.get_lyric(&query).await { - Ok(result) => Ok::(result), - Err(e) => { - tracing::error!("lyric search failed: {}", e); - Err(CrackedError::Anyhow(e).into()) - }, - }?; - let (track, artists, lyric) = match result { - lyric_finder::LyricResult::Some { - track, - artists, - lyric, - } => { - tracing::warn!("{} by {}'s lyric:\n{}", track, artists, lyric); - (track, artists, lyric) - }, - lyric_finder::LyricResult::None => { - tracing::error!("lyric not found! query: {}", query); - ( - "Unknown".to_string(), - "Unknown".to_string(), - "Lyric not found!".to_string(), - ) - }, - }; - - Ok((track, artists, lyric)) -} - -#[async_trait] -pub trait ContextWithGuildId { - fn guild_id(&self) -> Option; -} - -#[async_trait] -impl ContextWithGuildId for poise::Context<'_, U, E> { - fn guild_id(&self) -> Option { - Some(GuildId::new(1)) - } -} - -fn get_guild_id(ctx: impl ContextWithGuildId) -> Option { - ctx.guild_id() -} -#[cfg(test)] -mod test { - use super::*; - use mockall::predicate::*; - use mockall::*; - - // Mock your dependencies here - // For example, a mock `lyric_finder::Client` might look like this - mock! { - LyricFinderClient{} - - #[async_trait] - impl LyricFinderClient for LyricFinderClient { - async fn get_lyric(&self, query: &str) -> anyhow::Result; - } - } - - #[tokio::test] - async fn test_do_lyric_query_not_found() { - let mut mock_client = MockLyricFinderClient::new(); - mock_client - .expect_get_lyric() - .returning(|_| Err(anyhow::Error::msg("Not found"))); - - let result = do_lyric_query(mock_client, "Invalid query".to_string()).await; - assert!(result.is_err()); - } - - // #[tokio::test] - // async fn test_query_or_title_with_query() { - // // Setup the test context and other necessary mock objects - // let ctx = ...; // Mocked context - // let query = Some("Some query".to_string()); - - // // Perform the test - // let result = query_or_title(ctx, query).await; - - // // Assert the outcome - // assert_eq!(result.unwrap(), "Some query"); - // } - - // #[tokio::test] - // async fn test_query_or_title_without_query() { - // // Setup the test context and other necessary mock objects - // // let ctx = ...; // Mocked context without a current track - // let ctx = poise::ApplicationContext:: - - // // Perform the test - // let result = query_or_title(ctx, None).await; - - // // Assert that an error is returned because there's no current track - // assert!(result.is_err()); - // } - - #[tokio::test] - async fn test_do_lyric_query_found() { - // Setup the mocked `lyric_finder::Client` - let mut mock_client = MockLyricFinderClient::new(); - mock_client - .expect_get_lyric() - .with(eq("Some query")) - .times(1) - .return_once(|_| { - Ok(lyric_finder::LyricResult::Some { - track: "Some track".to_string(), - artists: "Some artist".to_string(), - lyric: "Some lyrics".to_string(), - }) - }); - - // Perform the test - let result = do_lyric_query(mock_client, "Some query".to_string()).await; - - // Assert the outcome - assert_eq!( - result.unwrap(), - ( - "Some track".to_string(), - "Some artist".to_string(), - "Some lyrics".to_string() - ) - ); - } -} +// pub async fn do_lyric_query( +// client: lyric_finder::Client, +// query: String, +// ) -> Result<(String, String, String), Error> { +// let (track, artists, lyric) = match client.get_lyric(&query).await { +// Ok(lyric_finder::LyricResult::Some { +// track, +// artists, +// lyric, +// }) => { +// tracing::warn!("{} by {}'s lyric:\n{}", track, artists, lyric); +// (track, artists, lyric) +// }, +// Ok(lyric_finder::LyricResult::None) => { +// tracing::error!("lyric not found! query: {}", query); +// ( +// "Unknown".to_string(), +// "Unknown".to_string(), +// "Lyric not found!".to_string(), +// ) +// }, +// Err(e) => { +// tracing::error!("lyric query error: {}", e); +// return Err(e.into()); +// }, +// }; + +// Ok((track, artists, lyric)) +// } + +// #[cfg(test)] +// mod test { +// use super::*; + +// // // Mock your dependencies here +// // // For example, a mock `lyric_finder::Client` might look like this +// // mock! { +// // LyricFinderClient{} + +// // #[async_trait] +// // impl LyricFinderClient for LyricFinderClient { +// // async fn get_lyric(&self, query: &str) -> anyhow::Result; +// // } +// // } + +// #[tokio::test] +// async fn test_do_lyric_query_not_found() { +// let client = lyric_finder::Client::new(); +// let result = do_lyric_query(client, "Hit That The Offpspring".to_string()).await; +// match result { +// Ok((track, artists, lyric)) => { +// assert_eq!(track, "Hit That"); +// assert_eq!(artists, "The Offspring"); +// assert_ne!(lyric, "Lyric not found!"); +// }, +// Err(_) => panic!("Unexpected error"), +// } +// } + +// // #[tokio::test] +// // async fn test_query_or_title_with_query() { +// // // Setup the test context and other necessary mock objects +// // let ctx = ...; // Mocked context +// // let query = Some("Some query".to_string()); + +// // // Perform the test +// // let result = query_or_title(ctx, query).await; + +// // // Assert the outcome +// // assert_eq!(result.unwrap(), "Some query"); +// // } + +// // #[tokio::test] +// // async fn test_query_or_title_without_query() { +// // // Setup the test context and other necessary mock objects +// // // let ctx = ...; // Mocked context without a current track +// // let ctx = poise::ApplicationContext:: + +// // // Perform the test +// // let result = query_or_title(ctx, None).await; + +// // // Assert that an error is returned because there's no current track +// // assert!(result.is_err()); +// // } + +// // #[tokio::test] +// // async fn test_do_lyric_query_found() { +// // // Setup the mocked `lyric_finder::Client` +// // let mut mock_client = MockLyricFinderClient::new(); +// // mock_client +// // .expect_get_lyric() +// // .with(eq("Some query")) +// // .times(1) +// // .return_once(|_| { +// // Ok(lyric_finder::LyricResult::Some { +// // track: "Some track".to_string(), +// // artists: "Some artist".to_string(), +// // lyric: "Some lyrics".to_string(), +// // }) +// // }); + +// // // Perform the test +// // let result = do_lyric_query(mock_client, "Some query".to_string()).await; + +// // // Assert the outcome +// // assert_eq!( +// // result.unwrap(), +// // ( +// // "Some track".to_string(), +// // "Some artist".to_string(), +// // "Some lyrics".to_string() +// // ) +// // ); +// // } +// } diff --git a/crack-core/src/commands/music/mod.rs b/crack-core/src/commands/music/mod.rs index 59a6b2efe..8b1569f3a 100644 --- a/crack-core/src/commands/music/mod.rs +++ b/crack-core/src/commands/music/mod.rs @@ -4,7 +4,6 @@ pub mod clean; pub mod clear; pub mod collector; pub mod doplay; -pub mod doplay_utils; pub mod dosearch; pub mod gambling; pub mod grab; @@ -15,6 +14,7 @@ pub mod lyrics; pub mod manage_sources; pub mod nowplaying; pub mod pause; +pub mod play_utils; pub mod playlog; pub mod queue; pub mod remove; diff --git a/crack-core/src/commands/music/nowplaying.rs b/crack-core/src/commands/music/nowplaying.rs index cafcb7930..165256832 100644 --- a/crack-core/src/commands/music/nowplaying.rs +++ b/crack-core/src/commands/music/nowplaying.rs @@ -1,6 +1,6 @@ use crate::{ - errors::CrackedError, interface::create_now_playing_embed, utils::send_embed_response_poise, - Context, Error, + errors::CrackedError, messaging::interface::create_now_playing_embed, + utils::send_embed_response_poise, Context, Error, }; use serenity::all::GuildId; use serenity::prelude::Mutex; diff --git a/crack-core/src/commands/music/play_utils/mod.rs b/crack-core/src/commands/music/play_utils/mod.rs new file mode 100644 index 000000000..a55791d93 --- /dev/null +++ b/crack-core/src/commands/music/play_utils/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod query; +pub(crate) mod queue; + +pub(crate) use query::*; +pub(crate) use queue::*; diff --git a/crack-core/src/commands/music/play_utils/query.rs b/crack-core/src/commands/music/play_utils/query.rs new file mode 100644 index 000000000..6d5be8b83 --- /dev/null +++ b/crack-core/src/commands/music/play_utils/query.rs @@ -0,0 +1,825 @@ +use super::queue::{queue_track_back, queue_track_front}; +use super::{queue_keyword_list_back, queue_query_list_offset}; +use crate::guild::operations::GuildSettingsOperations; +use crate::{ + commands::{check_banned_domains, MyAuxMetadata}, + errors::{verify, CrackedError}, + http_utils, + messaging::{ + interface::{send_no_query_provided, send_search_failed}, + message::CrackedMessage, + messages::SPOTIFY_AUTH_FAILED, + }, + sources::{ + rusty_ytdl::RustyYoutubeClient, + spotify::{Spotify, SpotifyTrack, SPOTIFY}, + youtube::{ + search_query_to_source_and_metadata_rusty, search_query_to_source_and_metadata_ytdl, + video_info_to_source_and_metadata, + }, + }, + utils::{edit_response_poise, send_search_response, yt_search_select}, + Context, Error, +}; +use ::serenity::all::{Attachment, CreateAttachment, CreateMessage}; +use colored::Colorize; +use poise::serenity_prelude as serenity; +use rusty_ytdl::search::{Playlist, SearchOptions, SearchType}; +use songbird::{ + input::{AuxMetadata, Compose as _, HttpRequest, Input as SongbirdInput, YoutubeDl}, + tracks::TrackHandle, + Call, +}; +use std::{ + ops::Deref, + path::Path, + process::{Output, Stdio}, + sync::Arc, +}; +use tokio::{process::Command, sync::Mutex}; +use url::Url; + +#[derive(Clone, Debug)] +/// Enum for type of possible queries we have to handle +pub enum QueryType { + Keywords(String), + KeywordList(Vec), + VideoLink(String), + SpotifyTracks(Vec), + PlaylistLink(String), + File(serenity::Attachment), + NewYoutubeDl((YoutubeDl, AuxMetadata)), + YoutubeSearch(String), + None, +} + +pub struct Queries { + queries: Vec, +} + +impl Queries { + pub fn new(queries: Vec) -> Self { + Self { queries } + } + + pub fn is_empty(&self) -> bool { + self.queries.is_empty() + } + + pub fn len(&self) -> usize { + self.queries.len() + } + + pub fn iter(&self) -> std::slice::Iter { + self.queries.iter() + } +} + +impl Deref for Queries { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.queries + } +} + +impl From> for Queries { + fn from(v: Vec) -> Self { + let queries = v.into_iter().map(QueryType::Keywords).collect(); + Queries::new(queries) + } +} + +impl From> for Queries { + fn from(v: Vec) -> Self { + let queries = v + .into_iter() + .map(|x| QueryType::Keywords(x.build_query())) + .collect(); + Queries::new(queries) + } +} + +impl From for Queries { + fn from(v: Playlist) -> Self { + let queries = v + .videos + .into_iter() + .map(|x| QueryType::VideoLink(x.url)) + .collect(); + Queries::new(queries) + } +} + +impl From for Vec { + fn from(q: Queries) -> Vec { + q.queries + } +} + +impl QueryType { + /// Build a query string from the query type. + pub fn build_query(&self) -> Option { + match self { + QueryType::Keywords(keywords) => Some(keywords.clone()), + QueryType::KeywordList(keywords_list) => Some(keywords_list.join(" ")), + QueryType::VideoLink(url) => Some(url.clone()), + QueryType::SpotifyTracks(tracks) => Some( + tracks + .iter() + .map(|x| x.build_query()) + .collect::>() + .join(" "), + ), + QueryType::PlaylistLink(url) => Some(url.clone()), + QueryType::File(file) => Some(file.url.clone()), + QueryType::NewYoutubeDl((_src, metadata)) => metadata.source_url.clone(), + QueryType::YoutubeSearch(query) => Some(query.clone()), + QueryType::None => None, + } + } + + // FIXME: Do you want to have a reqwest client we keep around and pass into + // this instead of creating a new one every time? + // FIXME: This is super expensive, literally we need to do this a lot better. + pub async fn get_download_status_and_filename( + &self, + mp3: bool, + ) -> Result<(bool, String), Error> { + // FIXME: Don't hardcode this. + let prefix = "/data/downloads"; + let extension = if mp3 { "mp3" } else { "webm" }; + let client = http_utils::get_client().clone(); + // tracing::warn!("query_type: {:?}", query_type); + match self { + QueryType::YoutubeSearch(_) => Err(Box::new(CrackedError::Other( + "Download not valid with search results.", + ))), + QueryType::VideoLink(url) => { + tracing::warn!("Mode::Download, QueryType::VideoLink"); + let (output, metadata) = download_file_ytdlp(url, mp3).await?; + let status = output.status.success(); + let url = metadata.source_url.unwrap(); + let file_name = format!( + "{}/{} [{}].{}", + prefix, + metadata.title.unwrap(), + url.split('=').last().unwrap(), + extension, + ); + Ok((status, file_name)) + }, + QueryType::NewYoutubeDl((_src, metadata)) => { + tracing::warn!("Mode::Download, QueryType::NewYoutubeDl"); + let url = metadata.source_url.as_ref().unwrap(); + let file_name = format!( + "{}/{} [{}].{}", + prefix, + metadata.title.as_ref().unwrap(), + url.split('=').last().unwrap(), + extension, + ); + tracing::warn!("file_name: {}", file_name); + let (output, _metadata) = download_file_ytdlp(url, mp3).await?; + let status = output.status.success(); + Ok((status, file_name)) + }, + QueryType::Keywords(query) => { + tracing::warn!("In Keywords"); + let mut ytdl = YoutubeDl::new(client, format!("ytsearch:{}", query)); + let metadata = ytdl.aux_metadata().await.unwrap(); + let url = metadata.source_url.unwrap(); + let (output, metadata) = download_file_ytdlp(&url, mp3).await?; + + let file_name = format!( + "{}/{} [{}].{}", + prefix, + metadata.title.unwrap(), + url.split('=').last().unwrap(), + extension, + ); + let status = output.status.success(); + Ok((status, file_name)) + }, + QueryType::File(file) => { + tracing::warn!("In File"); + Ok((true, file.url.to_owned().to_string())) + }, + QueryType::PlaylistLink(url) => { + tracing::warn!("In PlaylistLink"); + let (output, metadata) = download_file_ytdlp(url, mp3).await?; + let file_name = format!( + "{}/{} [{}].{}", + prefix, + metadata.title.unwrap(), + url.split('=').last().unwrap(), + extension, + ); + let status = output.status.success(); + Ok((status, file_name)) + }, + QueryType::SpotifyTracks(tracks) => { + tracing::warn!("In SpotifyTracks"); + let keywords_list = tracks + .iter() + .map(|x| x.build_query()) + .collect::>(); + let url = format!("ytsearch:{}", keywords_list.first().unwrap()); + let mut ytdl = YoutubeDl::new(client, url.clone()); + let metadata = ytdl.aux_metadata().await.unwrap(); + let (output, _metadata) = download_file_ytdlp(&url, mp3).await?; + let file_name = format!( + "{}/{} [{}].{}", + prefix, + metadata.title.unwrap(), + url.split('=').last().unwrap(), + extension, + ); + let status = output.status.success(); + Ok((status, file_name)) + }, + QueryType::KeywordList(keywords_list) => { + tracing::warn!("In KeywordList"); + let url = format!("ytsearch:{}", keywords_list.join(" ")); + let mut ytdl = YoutubeDl::new(client, url.clone()); + tracing::warn!("ytdl: {:?}", ytdl); + let metadata = ytdl.aux_metadata().await.unwrap(); + let (output, _metadata) = download_file_ytdlp(&url, mp3).await?; + let file_name = format!( + "{}/{} [{}].{}", + prefix, + metadata.title.unwrap(), + url.split('=').last().unwrap(), + extension, + ); + let status = output.status.success(); + Ok((status, file_name)) + }, + QueryType::None => Err(Box::new(CrackedError::Other("No query provided!"))), + } + } + + pub async fn mode_download(&self, ctx: Context<'_>, mp3: bool) -> Result { + let (status, file_name) = self.get_download_status_and_filename(mp3).await?; + ctx.channel_id() + .send_message( + ctx, + CreateMessage::new() + .content(format!("Download status {}", status)) + .add_file(CreateAttachment::path(Path::new(&file_name)).await?), + ) + .await?; + + Ok(false) + } + + pub async fn mode_search( + &self, + ctx: Context<'_>, + call: Arc>, + ) -> Result, CrackedError> { + match self { + QueryType::Keywords(keywords) => { + self.mode_search_keywords(ctx, call, keywords.clone()).await + }, + QueryType::SpotifyTracks(tracks) => { + self.mode_search_keywords( + ctx, + call, + tracks + .iter() + .map(|x| x.build_query()) + .collect::>() + .join(" "), + ) + .await + }, + QueryType::YoutubeSearch(query) => { + self.mode_search_keywords(ctx, call, query.clone()).await + }, + _ => send_search_failed(ctx).await.map(|_| Vec::new()), + } + } + + pub async fn mode_search_keywords( + &self, + ctx: Context<'_>, + call: Arc>, + keywords: String, + ) -> Result, CrackedError> { + let reqwest_client = ctx.data().http_client.clone(); + let search_results = YoutubeDl::new_search(reqwest_client, keywords) + .search(None) + .await?; + // let user_id = ctx.author().id; + let qt = yt_search_select( + ctx.serenity_context().clone(), + ctx.channel_id(), + search_results, + ) + .await?; + queue_track_back(ctx, &call, &qt).await + // update_queue_messages(&ctx, ctx.data(), &queue, guild_id).await + } + + pub async fn mode_next( + &self, + ctx: Context<'_>, + call: Arc>, + search_msg: &mut serenity::Message, + ) -> Result { + match self { + QueryType::Keywords(_) + | QueryType::VideoLink(_) + | QueryType::File(_) + | QueryType::NewYoutubeDl(_) => { + tracing::info!("Mode::Next, QueryType::Keywords|VideoLink|File|NewYoutubeDl"); + queue_track_front(ctx, &call, self).await?; + }, + // FIXME + QueryType::PlaylistLink(url) => { + let _guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let rusty_ytdl = RustyYoutubeClient::new()?; + let playlist: Playlist = rusty_ytdl.get_playlist(url.clone()).await?; + queue_query_list_offset(ctx, call, Queries::from(playlist).to_vec(), 1, search_msg) + .await?; + }, + QueryType::KeywordList(keywords_list) => { + queue_query_list_offset( + ctx, + call, + Queries::from(keywords_list.clone()).to_vec(), + 1, + search_msg, + ) + .await?; + }, + QueryType::SpotifyTracks(tracks) => { + // let keywords_list = tracks + // .iter() + // .map(|x| x.build_query()) + // .collect::>(); + queue_query_list_offset( + ctx, + call, + Queries::from(tracks.clone()).to_vec(), + 1, + search_msg, + ) + .await?; + }, + QueryType::YoutubeSearch(_) => { + return Err(CrackedError::Other("Not implemented yet!")); + }, + QueryType::None => { + return Ok(false); + }, + } + Ok(true) + } + + pub async fn mode_end( + &self, + ctx: Context<'_>, + call: Arc>, + search_msg: &mut crate::Message, + ) -> Result { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + match self { + QueryType::YoutubeSearch(query) => { + tracing::trace!("Mode::End, QueryType::YoutubeSearch"); + + let res = YoutubeDl::new_search(http_utils::get_client().clone(), query.clone()) + .search(None) + .await?; + let user_id = ctx.author().id; + send_search_response(ctx, guild_id, user_id, query.clone(), res).await?; + Ok(true) + }, + QueryType::Keywords(_) | QueryType::VideoLink(_) | QueryType::NewYoutubeDl(_) => { + tracing::warn!("### Mode::End, QueryType::Keywords | QueryType::VideoLink"); + match queue_track_back(ctx, &call, self).await { + Ok(_) => (), + Err(e) => { + tracing::error!("queue_track_back error: {:?}", e); + return Ok(false); + }, + }; + Ok(true) + }, + // FIXME + QueryType::PlaylistLink(url) => { + tracing::trace!("Mode::End, QueryType::PlaylistLink"); + // Let's use the new YouTube rust library for this + let rusty_ytdl = RustyYoutubeClient::new()?; + let playlist: Playlist = rusty_ytdl.get_playlist(url.clone()).await?; + queue_keyword_list_back(ctx, call, Queries::from(playlist).to_vec(), search_msg) + .await?; + // queue_yt_playlist(ctx, call, guild_id, playlist, search_msg).await?; + Ok(true) + }, + QueryType::SpotifyTracks(tracks) => { + let queries = tracks + .iter() + .map(|x| QueryType::Keywords(x.build_query())) + .collect::>(); + queue_keyword_list_back(ctx, call, queries, search_msg).await?; + Ok(true) + }, + QueryType::KeywordList(keywords_list) => { + tracing::trace!("Mode::End, QueryType::KeywordList"); + let queries = keywords_list + .iter() + .map(|x| QueryType::Keywords(x.clone())) + .collect::>(); + queue_keyword_list_back(ctx, call, queries, search_msg).await?; + Ok(true) + }, + QueryType::File(file) => { + tracing::trace!("Mode::End, QueryType::File"); + let _queue = queue_track_back(ctx, &call, &QueryType::File(file.clone())).await?; + // update_queue_messages(ctx.http(), ctx.data(), &queue, guild_id).await; + Ok(true) + }, + QueryType::None => send_no_query_provided(ctx).await.map(|_| false), + } + } + + pub async fn mode_rest( + &self, + ctx: Context<'_>, + call: Arc>, + search_msg: &mut crate::Message, + ) -> Result { + match self { + QueryType::VideoLink(url) | QueryType::PlaylistLink(url) => { + // FIXME + let mut src = YoutubeDl::new(http_utils::get_client().clone(), url.clone()); + let metadata = src.aux_metadata().await?; + queue_track_back(ctx, &call, &QueryType::NewYoutubeDl((src, metadata))).await?; + Ok(true) + }, + QueryType::KeywordList(keywords_list) => { + let queries = keywords_list + .iter() + .map(|x| QueryType::Keywords(x.clone())) + .collect::>(); + queue_keyword_list_back(ctx, call, queries, search_msg).await?; + Ok(true) + }, + QueryType::SpotifyTracks(tracks) => { + let queries = tracks + .iter() + .map(|x| QueryType::Keywords(x.build_query())) + .collect::>(); + queue_keyword_list_back(ctx, call, queries, search_msg).await?; + Ok(true) + }, + _ => { + ctx.defer().await?; // Why did I do this? + edit_response_poise(ctx, CrackedMessage::PlayAllFailed).await?; + Ok(false) + }, + } + } + + pub async fn mode_jump( + &self, + _ctx: Context<'_>, + _call: Arc>, + ) -> Result { + Err(CrackedError::Other("Not implemented yet!")) + // match self { + // QueryType::YoutubeSearch(query) => { + // return Err(CrackedError::Other("Not implemented yet!").into()); + // }, + // QueryType::Keywords(_) + // | QueryType::VideoLink(_) + // | QueryType::File(_) + // | QueryType::NewYoutubeDl(_) => { + // let mut queue = enqueue_track_pgwrite(ctx, &call, &query_type).await?; + + // if !queue_was_empty { + // rotate_tracks(&call, 1).await.ok(); + // queue = force_skip_top_track(&call.lock().await).await?; + // } + // }, + // QueryType::PlaylistLink(url) => { + // tracing::error!("Mode::Jump, QueryType::PlaylistLink"); + // // let urls = YouTubeRestartable::ytdl_playlist(&url, mode) + // // .await + // // .ok_or(CrackedError::PlayListFail)?; + // // FIXME + // let _src = YoutubeDl::new(Client::new(), url); + // // .ok_or(CrackedError::Other("failed to fetch playlist"))? + // // .into_iter() + // // .for_each(|track| async { + // // let _ = enqueue_track(&call, &QueryType::File(track)).await; + // // }); + // let urls = vec!["".to_string()]; + // let mut insert_idx = 1; + + // for (i, url) in urls.into_iter().enumerate() { + // let mut queue = + // insert_track(ctx, &call, &QueryType::VideoLink(url), insert_idx).await?; + + // if i == 0 && !queue_was_empty { + // queue = force_skip_top_track(&call.lock().await).await?; + // } else { + // insert_idx += 1; + // } + // } + // }, + // // FIXME + // QueryType::SpotifyTracks(tracks) => { + // let mut insert_idx = 1; + // let keywords_list = tracks + // .iter() + // .map(|x| x.build_query()) + // .collect::>(); + + // for (i, keywords) in keywords_list.into_iter().enumerate() { + // let mut queue = + // insert_track(ctx, &call, &QueryType::Keywords(keywords), insert_idx) + // .await?; + + // if i == 0 && !queue_was_empty { + // queue = force_skip_top_track(&call.lock().await).await?; + // } else { + // insert_idx += 1; + // } + // } + // }, + // // FIXME + // QueryType::KeywordList(keywords_list) => { + // let mut insert_idx = 1; + + // for (i, keywords) in keywords_list.into_iter().enumerate() { + // let mut queue = + // insert_track(ctx, &call, &QueryType::Keywords(keywords), insert_idx) + // .await?; + + // if i == 0 && !queue_was_empty { + // queue = force_skip_top_track(&call.lock().await).await?; + // } else { + // insert_idx += 1; + // } + // } + // }, + // QueryType::None => { + // let embed = CreateEmbed::default() + // .description(format!("{}", CrackedError::Other("No query provided!"))) + // .footer(CreateEmbedFooter::new("No query provided!")); + // send_embed_response_poise(ctx, embed).await?; + // return Ok(false); + // }, + // } + } + + // FIXME: Do you want to have a reqwest client we keep around and pass into + // this instead of creating a new one every time? + pub async fn get_track_source_and_metadata( + &self, + ) -> Result<(SongbirdInput, Vec), CrackedError> { + use colored::Colorize; + let client = http_utils::get_client().clone(); + tracing::warn!("{}", format!("query_type: {:?}", self).red()); + match self { + QueryType::YoutubeSearch(query) => { + tracing::error!("In YoutubeSearch"); + let mut ytdl = YoutubeDl::new_search(client, query.clone()); + let mut res = Vec::new(); + let asdf = ytdl.search(None).await?; + for metadata in asdf { + let my_metadata = MyAuxMetadata::Data(metadata); + res.push(my_metadata); + } + Ok((ytdl.into(), res)) + }, + QueryType::VideoLink(query) => { + tracing::warn!("In VideoLink"); + video_info_to_source_and_metadata(client.clone(), query.clone()).await + // let mut ytdl = YoutubeDl::new(client, query); + // tracing::warn!("ytdl: {:?}", ytdl); + // let metadata = ytdl.aux_metadata().await?; + // let my_metadata = MyAuxMetadata::Data(metadata); + // Ok((ytdl.into(), vec![my_metadata])) + }, + QueryType::Keywords(query) => { + tracing::warn!("In Keywords"); + let res = search_query_to_source_and_metadata_rusty( + client.clone(), + QueryType::Keywords(query.clone()), + ) + .await; + match res { + Ok((input, metadata)) => Ok((input, metadata)), + Err(_) => { + tracing::error!("falling back to ytdl!"); + search_query_to_source_and_metadata_ytdl(client.clone(), query.clone()) + .await + }, + } + }, + QueryType::File(file) => { + tracing::warn!("In File"); + Ok(( + HttpRequest::new(client, file.url.to_owned()).into(), + vec![MyAuxMetadata::default()], + )) + }, + QueryType::NewYoutubeDl(data) => { + let (ytdl, aux_metadata) = data.clone(); + Ok((ytdl.into(), vec![MyAuxMetadata::Data(aux_metadata)])) + }, + QueryType::PlaylistLink(url) => { + tracing::warn!("In PlaylistLink"); + let rytdl = RustyYoutubeClient::new_with_client(client.clone()).unwrap(); + let search_options = SearchOptions { + limit: 100, + search_type: SearchType::Playlist, + ..Default::default() + }; + + let res = rytdl.rusty_ytdl.search(url, Some(&search_options)).await?; + let mut metadata = Vec::with_capacity(res.len()); + for r in res { + metadata.push(MyAuxMetadata::Data( + RustyYoutubeClient::search_result_to_aux_metadata(&r), + )); + } + let ytdl = YoutubeDl::new(client.clone(), url.clone()); + tracing::warn!("ytdl: {:?}", ytdl); + Ok((ytdl.into(), metadata)) + }, + QueryType::SpotifyTracks(tracks) => { + tracing::error!("In SpotifyTracks, this is broken"); + let keywords_list = tracks + .iter() + .map(|x| x.build_query()) + .collect::>(); + let mut ytdl = YoutubeDl::new( + client, + format!("ytsearch:{}", keywords_list.first().unwrap()), + ); + tracing::warn!("ytdl: {:?}", ytdl); + let metdata = ytdl.aux_metadata().await.unwrap(); + let my_metadata = MyAuxMetadata::Data(metdata); + Ok((ytdl.into(), vec![my_metadata])) + }, + QueryType::KeywordList(keywords_list) => { + tracing::warn!("In KeywordList"); + let mut ytdl = + YoutubeDl::new(client, format!("ytsearch:{}", keywords_list.join(" "))); + tracing::warn!("ytdl: {:?}", ytdl); + let metdata = match ytdl.aux_metadata().await { + Ok(metadata) => metadata, + Err(e) => { + tracing::error!("yt-dlp error: {}", e); + return Err(CrackedError::AudioStream(e)); + }, + }; + let my_metadata = MyAuxMetadata::Data(metdata); + Ok((ytdl.into(), vec![my_metadata])) + }, + QueryType::None => unimplemented!(), + } + } +} + +/// Download a file and upload it as an mp3. +async fn download_file_ytdlp_mp3(url: &str) -> Result<(Output, AuxMetadata), Error> { + let metadata = YoutubeDl::new( + reqwest::ClientBuilder::new().use_rustls_tls().build()?, + url.to_string(), + ) + .aux_metadata() + .await?; + + let args = [ + "--extract-audio", + "--audio-format", + "mp3", + "--audio-quality", + "0", + url, + ]; + let child = Command::new("yt-dlp") + .args(args) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + + tracing::warn!("yt-dlp"); + + let output = child.wait_with_output().await?; + Ok((output, metadata)) +} + +/// Download a file and upload it as an attachment. +async fn download_file_ytdlp(url: &str, mp3: bool) -> Result<(Output, AuxMetadata), Error> { + if mp3 || url.contains("youtube.com") { + return download_file_ytdlp_mp3(url).await; + } + + let metadata = YoutubeDl::new(http_utils::get_client().clone(), url.to_string()) + .aux_metadata() + .await?; + + let child = Command::new("yt-dlp") + .arg(url) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + + tracing::warn!("yt-dlp"); + + let output = child.wait_with_output().await?; + Ok((output, metadata)) +} + +/// Returns the QueryType for a given URL (or query string, or file attachment) +pub async fn query_type_from_url( + ctx: Context<'_>, + url: &str, + file: Option, +) -> Result, Error> { + tracing::info!("url: {}", url); + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + + let query_type = match Url::parse(url) { + Ok(url_data) => match url_data.host_str() { + Some("open.spotify.com") | Some("spotify.link") => { + let final_url = http_utils::resolve_final_url(url).await?; + tracing::info!( + "spotify: {} -> {}", + url.underline().blue(), + final_url.underline().bright_blue() + ); + let spotify = SPOTIFY.lock().await; + let spotify = verify(spotify.as_ref(), CrackedError::Other(SPOTIFY_AUTH_FAILED))?; + Some(Spotify::extract(spotify, &final_url).await?) + }, + Some("cdn.discordapp.com") => { + tracing::info!("{}: {}", "attachement file".blue(), url.underline().blue()); + Some(QueryType::File(file.unwrap())) + }, + Some("www.youtube.com") => { + // Handle youtube playlist + if url.contains("playlist") { + tracing::warn!("{}: {}", "youtube playlist".blue(), url.underline().blue()); + Some(QueryType::PlaylistLink(url.to_string())) + } else { + Some(QueryType::VideoLink(url.to_string())) + } + }, + // For all other domains fall back to yt-dlp. + Some(other) => { + tracing::warn!("query_type_from_url: domain: {other}, using yt-dlp"); + tracing::warn!( + "query_type_from_use: {}: {}", + "LINK".blue(), + url.underline().blue() + ); + let mut ytdl = YoutubeDl::new(ctx.data().http_client.clone(), url.to_string()); + // This can fail whenever yt-dlp cannot parse a track from the URL. + let metadata = match ytdl.aux_metadata().await { + Ok(metadata) => metadata, + Err(e) => { + tracing::error!("yt-dlp error: {}", e); + return Err(CrackedError::AudioStream(e).into()); + }, + }; + Some(QueryType::NewYoutubeDl((ytdl, metadata))) + }, + None => { + // handle spotify:track:3Vr5jdQHibI2q0A0KW4RWk format? + // TODO: Why is this a thing? + if url.starts_with("spotify:") { + let parts = url.split(':').collect::>(); + let final_url = + format!("https://open.spotify.com/track/{}", parts.last().unwrap()); + tracing::warn!("spotify: {} -> {}", url, final_url); + let spotify = SPOTIFY.lock().await; + let spotify = + verify(spotify.as_ref(), CrackedError::Other(SPOTIFY_AUTH_FAILED))?; + Some(Spotify::extract(spotify, &final_url).await?) + } else { + Some(QueryType::Keywords(url.to_string())) + } + }, + }, + Err(e) => { + tracing::error!("Url::parse error: {}", e); + Some(QueryType::Keywords(url.to_string())) + }, + }; + let guild_settings = ctx + .data() + .get_guild_settings(guild_id) + .await + .ok_or(CrackedError::NoGuildSettings)?; + check_banned_domains(&guild_settings, query_type).map_err(Into::into) +} diff --git a/crack-core/src/commands/music/doplay_utils.rs b/crack-core/src/commands/music/play_utils/queue.rs similarity index 59% rename from crack-core/src/commands/music/doplay_utils.rs rename to crack-core/src/commands/music/play_utils/queue.rs index b16c3ded2..a042e08f8 100644 --- a/crack-core/src/commands/music/doplay_utils.rs +++ b/crack-core/src/commands/music/play_utils/queue.rs @@ -1,209 +1,254 @@ -use super::{Mode, QueryType}; -use crate::db::Metadata; -use crate::errors::verify; -use crate::handlers::track_end::update_queue_messages; -use crate::http_utils; +use super::QueryType; use crate::{ - commands::{get_track_source_and_metadata, MyAuxMetadata, RequestingUser}, - db::{aux_metadata_to_db_structures, PlayLog, User}, + commands::{Mode, MyAuxMetadata, RequestingUser}, + db::{aux_metadata_to_db_structures, Metadata, PlayLog, User}, + errors::{verify, CrackedError}, + handlers::track_end::update_queue_messages, + http_utils::CacheHttpExt, + Context as CrackContext, ContextExt, Error, +}; +use serenity::all::{CacheHttp, ChannelId, CreateEmbed, EditMessage, GuildId, Message, UserId}; +use songbird::{ + input::{AuxMetadata, Input as SongbirdInput}, + tracks::{Track, TrackHandle}, + Call, }; -use crate::{errors::CrackedError, Context, Error}; - -use rusty_ytdl::search::Playlist as YTPlaylist; -use serenity::all::{Cache, ChannelId, CreateEmbed, EditMessage, GuildId, Message, UserId}; -use songbird::input::AuxMetadata; -use songbird::tracks::TrackHandle; -use songbird::Call; -use songbird::{input::Input as SongbirdInput, tracks::Track}; use sqlx::PgPool; use std::sync::Arc; use tokio::sync::Mutex; -struct QueueTrackData { - title: String, - url: String, +/// Data structure for a track that is ready to be played. +pub struct TrackReadyData { + pub track: Track, + pub metadata: MyAuxMetadata, + pub user_id: UserId, + pub username: String, +} + +/// Takes a query and returns a track that is ready to be played, along with relevant metadata. +pub async fn ready_query( + ctx: CrackContext<'_>, + query_type: QueryType, +) -> Result { + let user_id = ctx.author().id; + let (source, metadata_vec): (SongbirdInput, Vec) = + query_type.get_track_source_and_metadata().await?; + let metadata = match metadata_vec.first() { + Some(x) => x.clone(), + None => { + return Err(CrackedError::Other("metadata.first() failed")); + }, + }; + let track: Track = source.into(); + + let username = ctx.user_id_to_username_or_default(user_id); + + Ok(TrackReadyData { + track, + metadata, + user_id, + username, + }) +} + +/// Pushes a track to the front of the queue, after readying it. +pub async fn queue_track_ready_front( + call: &Arc>, + ready_track: TrackReadyData, +) -> Result, CrackedError> { + let mut handler = call.lock().await; + let track_handle = handler.enqueue(ready_track.track).await; + let new_q = handler.queue().current_queue(); + handler.queue().modify_queue(|queue| { + let back = queue.pop_back().unwrap(); + queue.insert(1, back); + }); + drop(handler); + let mut map = track_handle.typemap().write().await; + map.insert::(ready_track.metadata.clone()); + map.insert::(RequestingUser::UserId(ready_track.user_id)); + drop(map); + Ok(new_q) +} + +/// Pushes a track to the back of the queue, after readying it. +pub async fn queue_track_ready_back( + call: &Arc>, + ready_track: TrackReadyData, +) -> Result, CrackedError> { + let mut handler = call.lock().await; + let track_handle = handler.enqueue(ready_track.track).await; + let new_q = handler.queue().current_queue(); + drop(handler); + let mut map = track_handle.typemap().write().await; + map.insert::(ready_track.metadata.clone()); + map.insert::(RequestingUser::UserId(ready_track.user_id)); + Ok(new_q) +} + +/// Pushes a track to the front of the queue. +pub async fn queue_track_front( + ctx: CrackContext<'_>, + call: &Arc>, + query_type: &QueryType, +) -> Result, CrackedError> { + let ready_track = ready_query(ctx, query_type.clone()).await?; + ctx.send_track_metadata_write_msg(&ready_track); + let q = queue_track_ready_front(call, ready_track).await?; + Ok(q) +} + +/// Pushes a track to the front of the queue. +pub async fn queue_track_back( + ctx: CrackContext<'_>, + call: &Arc>, + query_type: &QueryType, +) -> Result, CrackedError> { + let ready_track = ready_query(ctx, query_type.clone()).await?; + ctx.send_track_metadata_write_msg(&ready_track); + queue_track_ready_back(call, ready_track).await } /// Queue a list of tracks to be played. -#[cfg(not(tarpaulin_include))] -async fn queue_tracks( - ctx: Context<'_>, +pub async fn queue_ready_track_list( call: Arc>, - tracks: Vec, - search_msg: &mut Message, -) -> Result<(), Error> { - let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - let http = ctx.http(); - let n = tracks.len() as f32; - let mut i: f32 = 0.0_f32; - for track in tracks { - // Update the search message with what's queuing right now. - search_msg - .edit( - ctx.http(), - EditMessage::new().embed(CreateEmbed::default().description(format!( - "Queuing: [{}]({})\n{}% Done...", - track.title, - track.url, - (i / n) * 100.0 - ))), - ) - .await?; - i += 1.0_f32; - let queue_res = - enqueue_track_pgwrite(ctx, &call, &QueryType::VideoLink(track.url.to_string())).await; - let queue = match queue_res { - Ok(q) => q, - Err(e) => { - tracing::error!("Error: {}", e); - continue; - }, - }; - update_queue_messages(http, ctx.data(), &queue, guild_id).await; + _user_id: UserId, + tracks: Vec, + mode: Mode, +) -> Result, Error> { + let mut handler = call.lock().await; + for (idx, ready_track) in tracks.into_iter().enumerate() { + let TrackReadyData { + track, + metadata, + user_id, + .. + } = ready_track; + let track_handle = handler.enqueue(track).await; + let mut map = track_handle.typemap().write().await; + map.insert::(metadata); + map.insert::(RequestingUser::UserId(user_id)); + if mode == Mode::Next { + handler.queue().modify_queue(|queue| { + let back = queue.pop_back().unwrap(); + queue.insert(idx + 1, back); + }); + } } - Ok(()) + Ok(handler.queue().current_queue()) } -/// Queue a YouTube playlist to be played. +/// Queue a list of keywords to be played from the end of the queue. #[cfg(not(tarpaulin_include))] -pub async fn queue_yt_playlist<'a>( - ctx: Context<'_>, +pub async fn queue_keyword_list_back<'a>( + ctx: CrackContext<'_>, call: Arc>, - _guild_id: GuildId, - playlist: YTPlaylist, - search_msg: &'a mut Message, + queries: Vec, + msg: &'a mut Message, ) -> Result<(), Error> { - queue_tracks( - ctx, - call, - playlist - .videos + let first = queries + .first() + .ok_or(CrackedError::Other("queries.first()"))?; + queue_vec_query_type(ctx, call.clone(), vec![first.clone()], Mode::End).await?; + let queries = queries[1..].to_vec(); + for chunk in queries.chunks(10) { + let to_queue_str = chunk .iter() - .map(|x| QueueTrackData { - title: x.title.clone(), - url: x.url.clone(), - }) - .collect(), - search_msg, - ) - .await + .map(|q| q.build_query().unwrap_or_default()) + .collect::>() + .join("\n"); + msg.edit( + ctx, + EditMessage::new().embed(CreateEmbed::default().description(format!( + "Queuing {} songs... \n{}", + chunk.len(), + to_queue_str + ))), + ) + .await?; + queue_vec_query_type(ctx, call.clone(), chunk.to_vec(), Mode::End).await? + } + Ok(()) } -/// Queue a list of keywords to be played +/// Queue a list of keywords to be played with an offset. #[cfg(not(tarpaulin_include))] -pub async fn queue_keyword_list<'a>( - ctx: Context<'_>, +pub async fn queue_vec_query_type( + ctx: CrackContext<'_>, call: Arc>, - keyword_list: Vec, - msg: &'a mut Message, + queries: Vec, + _mode: Mode, ) -> Result<(), Error> { - queue_keyword_list_w_offset(ctx, call, keyword_list, 0, msg).await + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + + let mut tracks = Vec::new(); + + for query in queries { + let ready_track = ready_query(ctx, query).await?; + ctx.send_track_metadata_write_msg(&ready_track); + tracks.push(ready_track); + } + let queue = queue_ready_track_list(call, ctx.author().id, tracks, Mode::End).await?; + update_queue_messages(&ctx, ctx.data(), &queue, guild_id).await; + Ok(()) } -/// Queue a list of keywords to be played with an offset. +/// Queue a list of queries to be played with a given offset. +/// N.B. The offset must be 0 < offset < queue.len() + 1 #[cfg(not(tarpaulin_include))] -pub async fn queue_keyword_list_w_offset<'a>( - ctx: Context<'_>, +pub async fn queue_query_list_offset<'a>( + ctx: CrackContext<'_>, call: Arc>, - keyword_list: Vec, + queries: Vec, offset: usize, - search_msg: &'a mut Message, + _search_msg: &'a mut Message, ) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - let mut failed: usize = 0; - let n = keyword_list.len() as f32; - for (idx, keywords) in keyword_list.into_iter().enumerate() { - search_msg - .edit( - ctx.http(), - EditMessage::new().embed(CreateEmbed::default().description(format!( - "Queuing: {}\n{}% Done...", - keywords, - (idx as f32 / n) * 100.0 - ))), - ) - .await?; - let ins_track_res = insert_track( - ctx, - &call, - &QueryType::Keywords(keywords), - idx + offset - failed, - ) - .await; - let queue = match ins_track_res { - Ok(x) => x, - Err(e) => { - tracing::error!("insert_track error: {}", e); - failed += 1; - Vec::new() - }, - }; - if queue.is_empty() { - continue; - } - // TODO: Perhaps pass this off to a background task? - update_queue_messages(&ctx.serenity_context().http, ctx.data(), &queue, guild_id).await; - } - - Ok(()) -} -/// Inserts a track into the queue at the specified index. -#[cfg(not(tarpaulin_include))] -pub async fn insert_track( - ctx: Context<'_>, - call: &Arc>, - query_type: &QueryType, - idx: usize, -) -> Result, CrackedError> { - let handler = call.lock().await; - let queue_size = handler.queue().len(); - drop(handler); - tracing::trace!("queue_size: {}, idx: {}", queue_size, idx); + // Can this starting section be simplified? + let queue_size = { + let handler = call.lock().await; + handler.queue().len() + }; if queue_size <= 1 { - let queue = enqueue_track_pgwrite(ctx, call, query_type).await?; - return Ok(queue); + return queue_vec_query_type(ctx, call, queries, Mode::End).await; } verify( - idx > 0 && idx <= queue_size + 1, - CrackedError::NotInRange("index", idx as isize, 1, queue_size as isize), + offset > 0 && offset <= queue_size + 1, + CrackedError::NotInRange("index", offset as isize, 1, queue_size as isize), )?; - enqueue_track_pgwrite(ctx, call, query_type).await?; + let mut tracks = Vec::new(); + for query in queries { + let ready_track = ready_query(ctx, query).await?; + ctx.send_track_metadata_write_msg(&ready_track); + tracks.push(ready_track); + } - let handler = call.lock().await; - handler.queue().modify_queue(|queue| { - let back = queue.pop_back().unwrap(); - queue.insert(idx, back); - }); + let mut handler = call.lock().await; + for (idx, ready_track) in tracks.into_iter().enumerate() { + let track = ready_track.track; + let metadata = ready_track.metadata; + let user_id = ready_track.user_id; + + // let mut handler = call.lock().await; + let track_handle = handler.enqueue(track).await; + let mut map = track_handle.typemap().write().await; + map.insert::(metadata); + map.insert::(RequestingUser::UserId(user_id)); + handler.queue().modify_queue(|q| { + let back = q.pop_back().unwrap(); + q.insert(idx + offset, back); + }) + } - Ok(handler.queue().current_queue()) -} + let cur_q = handler.queue().current_queue(); + drop(handler); + update_queue_messages(&ctx, ctx.data(), &cur_q, guild_id).await; -/// Enqueues a track and adds metadata to the database. (concise parameters) -#[cfg(not(tarpaulin_include))] -pub async fn enqueue_track_pgwrite( - ctx: Context<'_>, - call: &Arc>, - query_type: &QueryType, -) -> Result, CrackedError> { - // let database_pool = get_db_or_err!(ctx); - // let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - // let channel_id = ctx.channel_id(); - // let user_id = ctx.author().id; - // let http = ctx.http(); - enqueue_track_pgwrite_asdf( - ctx.data().database_pool.as_ref().unwrap(), - ctx.guild_id().unwrap(), - ctx.channel_id(), - ctx.author().id, - ctx.cache(), - call, - query_type, - ) - .await + Ok(()) } /// Writes metadata to the database for a playing track. @@ -266,21 +311,24 @@ pub async fn write_metadata_pg( } /// Enqueues a track and adds metadata to the database. (parameters broken out) +// TODO: This is redundant with the other queuing functions. Remove it. #[cfg(not(tarpaulin_include))] pub async fn enqueue_track_pgwrite_asdf( database_pool: &PgPool, guild_id: GuildId, channel_id: ChannelId, user_id: UserId, - cache: &Cache, + cache_http: impl CacheHttp, call: &Arc>, query_type: &QueryType, ) -> Result, CrackedError> { + // use crate::sources::youtube::get_track_source_and_metadata; + tracing::info!("query_type: {:?}", query_type); // is this comment still relevant to this section of code? // safeguard against ytdl dying on a private/deleted video and killing the playlist let (source, metadata): (SongbirdInput, Vec) = - get_track_source_and_metadata(query_type.clone()).await?; + query_type.get_track_source_and_metadata().await?; let res = match metadata.first() { Some(x) => x.clone(), None => { @@ -289,8 +337,7 @@ pub async fn enqueue_track_pgwrite_asdf( }; let track: Track = source.into(); - // let username = http_utils::http_to_username_or_default(http, user_id).await; - let username = http_utils::cache_to_username_or_default(cache, user_id); + let username = cache_http.user_id_to_username_or_default(user_id); let MyAuxMetadata::Data(aux_metadata) = res.clone(); @@ -316,6 +363,7 @@ pub async fn enqueue_track_pgwrite_asdf( } /// Get the play mode and the message from the parameters to the play command. +// TODO: There is a lot of cruft in this from the older version of this. Clean it up. pub fn get_mode(is_prefix: bool, msg: Option, mode: Option) -> (Mode, String) { let opt_mode = mode.clone(); if is_prefix { @@ -375,6 +423,7 @@ pub fn get_mode(is_prefix: bool, msg: Option, mode: Option) -> ( /// Due to the way that the way the poise library works with auto filling them /// based on types, it could be kind of mangled if the prefix version of the /// command is used. +// TODO: Old and crufty. Clean up. pub fn get_msg( mode: Option, query_or_url: Option, @@ -400,7 +449,7 @@ pub fn get_msg( /// Rotates the queue by `n` tracks to the right. #[cfg(not(tarpaulin_include))] -pub async fn rotate_tracks( +pub async fn _rotate_tracks( call: &Arc>, n: usize, ) -> Result, CrackedError> { diff --git a/crack-core/src/commands/music/queue.rs b/crack-core/src/commands/music/queue.rs index 915333057..72641dc38 100644 --- a/crack-core/src/commands/music/queue.rs +++ b/crack-core/src/commands/music/queue.rs @@ -1,9 +1,9 @@ use crate::{ errors::CrackedError, handlers::track_end::ModifyQueueHandler, - interface::{build_nav_btns, create_queue_embed}, + messaging::interface::{create_nav_btns, create_queue_embed}, messaging::messages::QUEUE_EXPIRED, - utils::{calculate_num_pages, forget_queue_message, get_interaction}, + utils::{calculate_num_pages, forget_queue_message}, Context, Error, }; use ::serenity::builder::{ @@ -12,7 +12,8 @@ use ::serenity::builder::{ use ::serenity::futures::StreamExt; use poise::CreateReply; use songbird::{Event, TrackEvent}; -use std::{cmp::min, ops::Add, sync::Arc, sync::RwLock, time::Duration}; +use std::{cmp::min, ops::Add, sync::Arc, time::Duration}; +use tokio::sync::RwLock; const EMBED_TIMEOUT: u64 = 3600; @@ -20,6 +21,8 @@ const EMBED_TIMEOUT: u64 = 3600; #[cfg(not(tarpaulin_include))] #[poise::command(slash_command, prefix_command, aliases("list", "q"), guild_only)] pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { + use crate::utils::get_interaction_new; + tracing::info!("queue called"); let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; tracing::info!("guild_id: {}", guild_id); @@ -40,15 +43,15 @@ pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { let num_pages = calculate_num_pages(&tracks); tracing::info!("num_pages: {}", num_pages); - let mut message = match get_interaction(ctx) { - Some(interaction) => { + let mut message = match get_interaction_new(ctx) { + Some(crate::utils::CommandOrMessageInteraction::Command(interaction)) => { interaction .create_response( &ctx.serenity_context().http, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new() .embed(create_queue_embed(&tracks, 0).await) - .components(build_nav_btns(0, num_pages)), + .components(create_nav_btns(0, num_pages)), ), ) .await?; @@ -61,7 +64,7 @@ pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { .send( CreateReply::default() .embed(create_queue_embed(&tracks, 0).await) - .components(build_nav_btns(0, num_pages)), + .components(create_nav_btns(0, num_pages)), ) .await?; reply.into_message().await? @@ -76,7 +79,7 @@ pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { let data = ctx.data(); data.guild_cache_map .lock() - .unwrap() + .await .entry(guild_id) .or_default() .queue_messages @@ -87,7 +90,7 @@ pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { Event::Track(TrackEvent::End), ModifyQueueHandler { http: ctx.serenity_context().http.clone(), - _cache: ctx.serenity_context().cache.clone(), + cache: ctx.serenity_context().cache.clone(), data: data.clone(), call: call.clone(), guild_id, @@ -106,7 +109,7 @@ pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { let tracks = call.lock().await.queue().current_queue(); let page_num = { - let mut page_wlock = page.write().unwrap(); + let mut page_wlock = page.write().await; *page_wlock = match btn_id.as_str() { "<<" => 0, @@ -123,7 +126,7 @@ pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { CreateInteractionResponse::UpdateMessage( CreateInteractionResponseMessage::new() .add_embed(create_queue_embed(&tracks, page_num).await) - .components(build_nav_btns(page_num, num_pages)), + .components(create_nav_btns(page_num, num_pages)), ), ) .await?; diff --git a/crack-core/src/commands/music/skip.rs b/crack-core/src/commands/music/skip.rs index fe12230cc..cf0b98eeb 100644 --- a/crack-core/src/commands/music/skip.rs +++ b/crack-core/src/commands/music/skip.rs @@ -88,7 +88,7 @@ pub async fn downvote(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; - let call = get_call_with_fail_msg(ctx, guild_id).await?; + let call = get_call_with_fail_msg(ctx).await?; let handler = call.lock().await; let queue = handler.queue(); diff --git a/crack-core/src/commands/music/stop.rs b/crack-core/src/commands/music/stop.rs index 6aee9d7fd..02bfeae54 100644 --- a/crack-core/src/commands/music/stop.rs +++ b/crack-core/src/commands/music/stop.rs @@ -12,7 +12,7 @@ use crate::{ #[poise::command(slash_command, prefix_command, guild_only)] pub async fn stop(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); - ctx.data().set_autoplay(guild_id, false); + ctx.data().set_autoplay(guild_id, false).await; let manager = songbird::get(ctx.serenity_context()).await.unwrap(); let call = manager.get(guild_id).unwrap(); diff --git a/crack-core/src/commands/music/summon.rs b/crack-core/src/commands/music/summon.rs index b212cac07..80923032d 100644 --- a/crack-core/src/commands/music/summon.rs +++ b/crack-core/src/commands/music/summon.rs @@ -39,7 +39,7 @@ pub async fn summon( let channel_id = get_channel_id_for_summon(channel, channel_id_str, guild.clone(), user_id).await?; - let call: Arc> = match manager.get(guild.id) { + let call: Arc> = match manager.get(guild_id) { Some(call) => { let handler = call.lock().await; let has_current_connection = handler.current_connection().is_some(); @@ -52,7 +52,7 @@ pub async fn summon( Ok(call.clone()) } }, - None => manager.join(guild.id, channel_id).await.map_err(|e| { + None => manager.join(guild_id, channel_id).await.map_err(|e| { tracing::error!("Error joining channel: {:?}", e); CrackedError::JoinChannelError(e) }), @@ -83,10 +83,10 @@ pub async fn summon( handler.remove_all_global_events(); } { - let _ = register_voice_handlers(buffer, call.clone()).await; + let _ = register_voice_handlers(buffer, call.clone(), ctx.serenity_context().clone()).await; let mut handler = call.lock().await; { - let guild_settings_map = ctx.data().guild_settings_map.write().unwrap(); + let guild_settings_map = ctx.data().guild_settings_map.write().await; // guild_settings_map // .entry(guild_id) diff --git a/crack-core/src/commands/music/volume.rs b/crack-core/src/commands/music/volume.rs index 23a09d13e..364013a7c 100644 --- a/crack-core/src/commands/music/volume.rs +++ b/crack-core/src/commands/music/volume.rs @@ -66,7 +66,7 @@ pub async fn volume( ctx.data() .guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .or_insert_with(|| { GuildSettings::new( @@ -79,7 +79,7 @@ pub async fn volume( .data() .guild_settings_map .read() - .unwrap() + .await .get(&guild_id) .unwrap() .clone(); @@ -103,7 +103,7 @@ pub async fn volume( let new_vol = (to_set.unwrap() as f32) / 100.0; let old_vol = { - let mut guild_settings_guard = ctx.data().guild_settings_map.write().unwrap(); + let mut guild_settings_guard = ctx.data().guild_settings_map.write().await; let guild_settings = guild_settings_guard .entry(guild_id) .and_modify(|guild_settings| { diff --git a/crack-core/src/commands/music/vote.rs b/crack-core/src/commands/music/vote.rs index 00b17009d..97b13f7ea 100644 --- a/crack-core/src/commands/music/vote.rs +++ b/crack-core/src/commands/music/vote.rs @@ -32,7 +32,7 @@ pub async fn vote_internal(ctx: Context<'_>) -> Result<(), Error> { tracing::info!("user_id: {:?}, guild_id: {:?}", user_id, guild_id); - let bot_id: UserId = http_utils::get_bot_id(ctx.http()).await?; + let bot_id: UserId = http_utils::get_bot_id(ctx).await?; tracing::info!("bot_id: {:?}", bot_id); let has_voted = match check_and_record_vote( ctx.data().database_pool.as_ref().unwrap(), diff --git a/crack-core/src/commands/music/voteskip.rs b/crack-core/src/commands/music/voteskip.rs index 91aba596a..19ebd13c3 100644 --- a/crack-core/src/commands/music/voteskip.rs +++ b/crack-core/src/commands/music/voteskip.rs @@ -86,13 +86,13 @@ pub async fn voteskip(ctx: Context<'_>) -> Result<(), Error> { // This is to prevent users from voting to skip a track, then leaving the voice channel. // TODO: Should this be moved to a separate module? Or should it be moved to a separate file? pub async fn forget_skip_votes(data: &Data, guild_id: GuildId) -> Result<(), Error> { - match data.guild_cache_map.lock() { - Ok(mut lock) => { - lock.entry(guild_id) - .and_modify(|cache| cache.current_skip_votes = HashSet::new()) - .or_default(); - Ok(()) - }, - Err(e) => Err(CrackedError::PoisonError(e.to_string().into()).into()), - } + let _res = data + .guild_cache_map + .lock() + .await + .entry(guild_id) + .and_modify(|cache| cache.current_skip_votes = HashSet::new()) + .or_default(); + + Ok(()) } diff --git a/crack-core/src/commands/music_utils.rs b/crack-core/src/commands/music_utils.rs index b93822c2b..dcac5438a 100644 --- a/crack-core/src/commands/music_utils.rs +++ b/crack-core/src/commands/music_utils.rs @@ -1,8 +1,9 @@ use crate::connection::get_voice_channel_for_user; +use crate::guild::operations::GuildSettingsOperations; use crate::handlers::{IdleHandler, TrackEndHandler}; use crate::messaging::message::CrackedMessage; use crate::utils::send_embed_response_poise; -use crate::CrackContext; +use crate::ContextExt as _; use crate::CrackedError; use crate::{Context, Error}; use poise::serenity_prelude::Mentionable; @@ -15,7 +16,7 @@ use std::{ }; use tokio::sync::Mutex; -/// Set the global handlers. +/// Set the global handlers for the bot in a call. #[cfg(not(tarpaulin_include))] pub async fn set_global_handlers( ctx: Context<'_>, @@ -33,26 +34,27 @@ pub async fn set_global_handlers( // unregister existing events and register idle notifier handler.remove_all_global_events(); - let guild_settings_map = data.guild_settings_map.read().unwrap().clone(); + let guild_settings = data + .get_guild_settings(guild_id) + .await + .ok_or(CrackedError::NoGuildSettings)?; - let _ = guild_settings_map.get(&guild_id).map(|guild_settings| { - let timeout = guild_settings.timeout; - if timeout > 0 { - let premium = guild_settings.premium; - handler.add_global_event( - Event::Periodic(Duration::from_secs(5), None), - IdleHandler { - http: ctx.serenity_context().http.clone(), - manager: manager.clone(), - channel_id, - guild_id: Some(guild_id), - limit: timeout as usize, - count: Default::default(), - no_timeout: Arc::new(AtomicBool::new(premium)), - }, - ); - } - }); + let timeout = guild_settings.timeout; + if timeout > 0 { + let premium = guild_settings.premium; + handler.add_global_event( + Event::Periodic(Duration::from_secs(5), None), + IdleHandler { + http: ctx.serenity_context().http.clone(), + manager: manager.clone(), + channel_id, + guild_id: Some(guild_id), + limit: timeout as usize, + count: Default::default(), + no_timeout: Arc::new(AtomicBool::new(premium)), + }, + ); + } handler.add_global_event( Event::Track(TrackEvent::End), @@ -60,6 +62,7 @@ pub async fn set_global_handlers( guild_id, cache: ctx.serenity_context().cache.clone(), http: ctx.serenity_context().http.clone(), + // cache_http: Arc::new(ctx.serenity_context()), call: call.clone(), data: ctx.data().clone(), }, @@ -74,43 +77,43 @@ pub async fn set_global_handlers( } /// Get the call handle for songbird. -// FIXME: Does this need to take the GuildId? #[cfg(not(tarpaulin_include))] -pub async fn get_call_with_fail_msg( - ctx: Context<'_>, - guild_id: GuildId, -) -> Result>, Error> { - let manager = songbird::get(ctx.serenity_context()).await.unwrap(); - match manager.get(guild_id) { - Some(call) => Ok(call), - None => { - // try to join a voice channel if not in one just yet - //match summon_short(ctx).await { - // TODO: Don't just return an error on failure, do something smarter. - let channel_id = { - let guild = ctx.guild().ok_or(CrackedError::NoGuildCached)?; - get_voice_channel_for_user(&guild.clone(), &ctx.author().id)? - }; - match manager.join(guild_id, channel_id).await { - Ok(call) => { - let text = set_global_handlers(ctx, call.clone(), guild_id, channel_id).await?; +pub async fn get_call_with_fail_msg(ctx: Context<'_>) -> Result>, CrackedError> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let manager = songbird::get(ctx.serenity_context()) + .await + .ok_or(CrackedError::NoSongbird)?; + + // Return the call if it already exists + let maybe_call = manager.get(guild_id); + if let Some(call) = maybe_call { + return Ok(call); + } - let msg = ctx - .send(CreateReply::default().content(text).ephemeral(true)) - .await? - .into_message() - .await?; - ctx.add_msg_to_cache(guild_id, msg); - Ok(call) - }, - Err(_) => { - // FIXME: Do something smarter here also. - let embed = CreateEmbed::default() - .description(format!("{}", CrackedError::NotConnected)); - send_embed_response_poise(ctx, embed).await?; - Err(CrackedError::NotConnected.into()) - }, - } + // Otherwise, try to join the channel of the user who sent the message. + let channel_id = { + let guild = ctx.guild().ok_or(CrackedError::NoGuildCached)?; + get_voice_channel_for_user(&guild.clone(), &ctx.author().id)? + }; + match manager.join(guild_id, channel_id).await { + // If we successfully joined the channel, set the global handlers. + // TODO: This should probably be a separate function. + Ok(call) => { + let text = set_global_handlers(ctx, call.clone(), guild_id, channel_id).await?; + + let msg = ctx + .send(CreateReply::default().content(text).ephemeral(true)) + .await? + .into_message() + .await?; + ctx.add_msg_to_cache_nonasync(guild_id, msg); + Ok(call) + }, + Err(err) => { + // FIXME: Do something smarter here also. + let embed = CreateEmbed::default().description(format!("{}", err)); + send_embed_response_poise(ctx, embed).await?; + Err(CrackedError::JoinChannelError(err)) }, } } diff --git a/crack-core/src/commands/osint.rs b/crack-core/src/commands/osint.rs index 926b7b2a8..74c0287c8 100644 --- a/crack-core/src/commands/osint.rs +++ b/crack-core/src/commands/osint.rs @@ -2,11 +2,12 @@ use crate::utils::send_channel_message; pub use crate::{ messaging::message::CrackedMessage, utils::{send_response_poise, SendMessageParams}, - Context, Error, Result, + Context, Error, }; use crack_osint::VirusTotalClient; use crack_osint::{get_scan_result, scan_url}; use poise::CreateReply; +use std::result::Result; use std::sync::Arc; /// Osint Commands diff --git a/crack-core/src/commands/ping.rs b/crack-core/src/commands/ping.rs index 64d312603..98b37b39a 100644 --- a/crack-core/src/commands/ping.rs +++ b/crack-core/src/commands/ping.rs @@ -6,12 +6,12 @@ use crate::{Context, Error}; #[cfg(not(tarpaulin_include))] #[poise::command(slash_command, prefix_command)] pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { - ping_(ctx).await + ping_internal(ctx).await } -/// Ping the bot implementation +/// Ping the bot internal function #[cfg(not(tarpaulin_include))] -pub async fn ping_(ctx: Context<'_>) -> Result<(), Error> { +pub async fn ping_internal(ctx: Context<'_>) -> Result<(), Error> { let start = std::time::Instant::now(); let msg = ctx.say("Pong!").await?; let end = std::time::Instant::now(); diff --git a/crack-core/src/commands/playlist/loadspotify.rs b/crack-core/src/commands/playlist/loadspotify.rs index ff735f908..c5bc7b317 100644 --- a/crack-core/src/commands/playlist/loadspotify.rs +++ b/crack-core/src/commands/playlist/loadspotify.rs @@ -5,7 +5,7 @@ use crate::{ http_utils, messaging::message::CrackedMessage, sources::spotify::{Spotify, SpotifyTrack, SPOTIFY}, - utils::send_embed_response_str, + utils::send_response_poise, Context, CrackedError, Error, }; use songbird::input::AuxMetadata; @@ -101,7 +101,7 @@ pub async fn loadspotify( let len = metadata_vec.len(); // Send the embed - send_embed_response_str(ctx, CrackedMessage::PlaylistCreated(name, len).to_string()).await?; + send_response_poise(ctx, CrackedMessage::PlaylistCreated(name, len), false).await?; Ok(()) } diff --git a/crack-core/src/commands/settings/get/all.rs b/crack-core/src/commands/settings/get/all.rs index 400a1c47b..2f52ced2f 100644 --- a/crack-core/src/commands/settings/get/all.rs +++ b/crack-core/src/commands/settings/get/all.rs @@ -21,7 +21,7 @@ pub async fn all(ctx: Context<'_>) -> Result<(), Error> { pub async fn get_settings(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); let settings_ro = { - let mut guild_settings_map = ctx.data().guild_settings_map.write().unwrap(); + let mut guild_settings_map = ctx.data().guild_settings_map.write().await; let settings = guild_settings_map .entry(guild_id) .or_insert(GuildSettings::new( diff --git a/crack-core/src/commands/settings/get/get_auto_role.rs b/crack-core/src/commands/settings/get/get_auto_role.rs index 95687ebbe..f017b2feb 100644 --- a/crack-core/src/commands/settings/get/get_auto_role.rs +++ b/crack-core/src/commands/settings/get/get_auto_role.rs @@ -16,7 +16,8 @@ use crate::{Context, Data, Error}; pub async fn auto_role(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); let data = ctx.data(); - get_auto_role(data, guild_id) + get_auto_role_internal(data, guild_id) + .await .map_or_else( || { send_response_poise( @@ -39,8 +40,8 @@ pub async fn auto_role(ctx: Context<'_>) -> Result<(), Error> { } /// Get the auto role for the server. -pub fn get_auto_role(data: &Data, guild_id: GuildId) -> Option { - let guild_settings_map = data.guild_settings_map.read().unwrap(); +pub async fn get_auto_role_internal(data: &Data, guild_id: GuildId) -> Option { + let guild_settings_map = data.guild_settings_map.read().await; let guild_settings = guild_settings_map.get(&guild_id)?; guild_settings .welcome_settings diff --git a/crack-core/src/commands/settings/get/get_idle_timeout.rs b/crack-core/src/commands/settings/get/get_idle_timeout.rs index 1d65b5bef..36aa8e0f2 100644 --- a/crack-core/src/commands/settings/get/get_idle_timeout.rs +++ b/crack-core/src/commands/settings/get/get_idle_timeout.rs @@ -15,7 +15,7 @@ use crate::{Context, Error}; pub async fn idle_timeout(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); let idle_timeout = { - let mut guild_settings_map = ctx.data().guild_settings_map.write().unwrap(); + let mut guild_settings_map = ctx.data().guild_settings_map.write().await; let settings = guild_settings_map .entry(guild_id) .or_insert(GuildSettings::new( diff --git a/crack-core/src/commands/settings/get/get_premium.rs b/crack-core/src/commands/settings/get/get_premium.rs index e3e5da5d0..2d5035bb3 100644 --- a/crack-core/src/commands/settings/get/get_premium.rs +++ b/crack-core/src/commands/settings/get/get_premium.rs @@ -3,8 +3,8 @@ use serenity::all::GuildId; use crate::{Context, Data, Error}; /// Get the current `premium` setting for the guild. -pub fn get_premium(data: &Data, guild_id: GuildId) -> bool { - let guild_settings_map = data.guild_settings_map.read().unwrap(); +pub async fn get_premium(data: &Data, guild_id: GuildId) -> bool { + let guild_settings_map = data.guild_settings_map.read().await; let guild_settings = guild_settings_map.get(&guild_id).unwrap(); guild_settings.premium } @@ -21,7 +21,7 @@ pub fn get_premium(data: &Data, guild_id: GuildId) -> bool { pub async fn premium(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); let data = ctx.data(); - let res = get_premium(data, guild_id); + let res = get_premium(data, guild_id).await; ctx.say(format!("Premium status: {}", res)) .await diff --git a/crack-core/src/commands/settings/get/get_volume.rs b/crack-core/src/commands/settings/get/get_volume.rs index ebbf6afca..9087d7269 100644 --- a/crack-core/src/commands/settings/get/get_volume.rs +++ b/crack-core/src/commands/settings/get/get_volume.rs @@ -1,14 +1,14 @@ use crate::{guild::settings::GuildSettings, Context, Error}; use serenity::all::GuildId; -use std::sync::RwLock; use std::{collections::HashMap, sync::Arc}; +use tokio::sync::RwLock; /// Get the current `volume` and `old_volume` setting for the guild. -pub fn get_volume( +pub async fn get_volume( guild_settings_map: Arc>>, guild_id: GuildId, ) -> (f32, f32) { - let guild_settings_map = guild_settings_map.read().unwrap(); + let guild_settings_map = guild_settings_map.read().await; let guild_settings = guild_settings_map.get(&guild_id).unwrap(); (guild_settings.volume, guild_settings.old_volume) } @@ -24,7 +24,7 @@ pub fn get_volume( pub async fn volume(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); let data = ctx.data(); - let (vol, old_vol) = get_volume(data.guild_settings_map.clone(), guild_id); + let (vol, old_vol) = get_volume(data.guild_settings_map.clone(), guild_id).await; ctx.say(format!("vol: {}, old_vol: {}", vol, old_vol)) .await @@ -34,27 +34,24 @@ pub async fn volume(ctx: Context<'_>) -> Result<(), Error> { #[cfg(test)] mod test { + use super::get_volume; + use super::{Arc, RwLock}; + use crate::guild::settings::GuildSettings; + use serenity::model::id::GuildId; + use crate::guild::settings::DEFAULT_VOLUME_LEVEL; #[tokio::test] async fn test_volume() { - use super::get_volume; - use crate::guild::settings::GuildSettings; - use serenity::model::id::GuildId; - use std::sync::{Arc, RwLock}; let guild_settings_map = Arc::new(RwLock::new(std::collections::HashMap::new())); let guild_id = GuildId::new(1); let _guild_settings = guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .or_insert(GuildSettings::new(guild_id, Some("!"), None)); - let (vol, old_vol) = get_volume(guild_settings_map.clone(), guild_id); - assert_eq!(vol, DEFAULT_VOLUME_LEVEL); - assert_eq!(old_vol, DEFAULT_VOLUME_LEVEL); - - let (vol, old_vol) = get_volume(guild_settings_map.clone(), guild_id); + let (vol, old_vol) = get_volume(guild_settings_map, guild_id).await; assert_eq!(vol, DEFAULT_VOLUME_LEVEL); assert_eq!(old_vol, DEFAULT_VOLUME_LEVEL); } diff --git a/crack-core/src/commands/settings/get/get_welcome_settings.rs b/crack-core/src/commands/settings/get/get_welcome_settings.rs index c80b3a3cd..4c60fcfd9 100644 --- a/crack-core/src/commands/settings/get/get_welcome_settings.rs +++ b/crack-core/src/commands/settings/get/get_welcome_settings.rs @@ -14,7 +14,7 @@ use crate::{Context, Error}; pub async fn welcome_settings(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); let welcome_settings = { - let mut guild_settings_map = ctx.data().guild_settings_map.write().unwrap(); + let mut guild_settings_map = ctx.data().guild_settings_map.write().await; let settings = guild_settings_map .entry(guild_id) .or_insert(GuildSettings::new( diff --git a/crack-core/src/commands/settings/get/log_channels.rs b/crack-core/src/commands/settings/get/log_channels.rs index c5cbba4fb..4bda200ca 100644 --- a/crack-core/src/commands/settings/get/log_channels.rs +++ b/crack-core/src/commands/settings/get/log_channels.rs @@ -19,7 +19,7 @@ pub async fn all_log_channel(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; { let all_log_channel = { - let mut guild_settings_map = ctx.data().guild_settings_map.write().unwrap(); + let mut guild_settings_map = ctx.data().guild_settings_map.write().await; let settings = guild_settings_map .entry(guild_id) .or_insert(GuildSettings::new( @@ -58,7 +58,7 @@ pub async fn join_leave_log_channel(ctx: Context<'_>) -> Result<(), Error> { .ok_or(crate::errors::CrackedError::NoGuildId)?; { let join_leave_log_channel = { - let mut guild_settings_map = ctx.data().guild_settings_map.write().unwrap(); + let mut guild_settings_map = ctx.data().guild_settings_map.write().await; let settings = guild_settings_map .entry(guild_id) .or_insert(GuildSettings::new( diff --git a/crack-core/src/commands/settings/prefix.rs b/crack-core/src/commands/settings/prefix.rs index 651dbd714..6503c3c5b 100644 --- a/crack-core/src/commands/settings/prefix.rs +++ b/crack-core/src/commands/settings/prefix.rs @@ -15,7 +15,7 @@ pub async fn add_prefix( let guild_id = ctx.guild_id().unwrap(); let guild_name = get_guild_name(ctx.serenity_context(), guild_id).unwrap_or_default(); let additional_prefixes = { - let mut settings = ctx.data().guild_settings_map.write().unwrap(); + let mut settings = ctx.data().guild_settings_map.write().await; let new_settings = settings .entry(guild_id) .and_modify(|e| { @@ -55,7 +55,7 @@ pub async fn clear_prefixes( let guild_id = ctx.guild_id().unwrap(); let guild_name = get_guild_name(ctx.serenity_context(), guild_id).unwrap_or_default(); let additional_prefixes = { - let mut settings = ctx.data().guild_settings_map.write().unwrap(); + let mut settings = ctx.data().guild_settings_map.write().await; let new_settings = settings .entry(guild_id) .and_modify(|e| { @@ -86,7 +86,7 @@ pub async fn clear_prefixes( pub async fn get_prefixes(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); let additional_prefixes = { - let settings = ctx.data().guild_settings_map.read().unwrap(); + let settings = ctx.data().guild_settings_map.read().await; settings .get(&guild_id) .map(|e| e.additional_prefixes.clone()) diff --git a/crack-core/src/commands/settings/print_settings.rs b/crack-core/src/commands/settings/print_settings.rs index 31ed03103..459a5e74f 100644 --- a/crack-core/src/commands/settings/print_settings.rs +++ b/crack-core/src/commands/settings/print_settings.rs @@ -9,7 +9,7 @@ use serenity::{ #[poise::command(prefix_command, owners_only, ephemeral, hide_in_help)] pub async fn print_settings(ctx: Context<'_>) -> Result<(), Error> { - let guild_settings_map = ctx.data().guild_settings_map.read().unwrap().clone(); //.unwrap().clone(); + let guild_settings_map = ctx.data().guild_settings_map.read().await.clone(); //.unwrap().clone(); for (guild_id, settings) in guild_settings_map.iter() { send_response_poise( diff --git a/crack-core/src/commands/settings/set/set_all_log_channel.rs b/crack-core/src/commands/settings/set/set_all_log_channel.rs index c31ade8e5..6b0fbca3a 100644 --- a/crack-core/src/commands/settings/set/set_all_log_channel.rs +++ b/crack-core/src/commands/settings/set/set_all_log_channel.rs @@ -63,7 +63,7 @@ pub async fn set_all_log_channel_data( Ok(data .guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { e.set_all_log_channel(channel_id.into()); diff --git a/crack-core/src/commands/settings/set/set_auto_role.rs b/crack-core/src/commands/settings/set/set_auto_role.rs index 6408b64d8..6cc2a3e0a 100644 --- a/crack-core/src/commands/settings/set/set_auto_role.rs +++ b/crack-core/src/commands/settings/set/set_auto_role.rs @@ -21,7 +21,7 @@ pub async fn auto_role( .data() .guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { e.set_auto_role(Some(auto_role_id)); diff --git a/crack-core/src/commands/settings/set/set_idle_timeout.rs b/crack-core/src/commands/settings/set/set_idle_timeout.rs index 5181124e8..6c3cacc2b 100644 --- a/crack-core/src/commands/settings/set/set_idle_timeout.rs +++ b/crack-core/src/commands/settings/set/set_idle_timeout.rs @@ -25,7 +25,7 @@ pub async fn idle_timeout( let _res = data .guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| e.timeout = timeout) .or_insert_with(|| { diff --git a/crack-core/src/commands/settings/set/set_join_leave_log_channel.rs b/crack-core/src/commands/settings/set/set_join_leave_log_channel.rs index 3013b844d..fa77f70d2 100644 --- a/crack-core/src/commands/settings/set/set_join_leave_log_channel.rs +++ b/crack-core/src/commands/settings/set/set_join_leave_log_channel.rs @@ -1,4 +1,5 @@ use crate::errors::CrackedError; +use crate::guild::operations::GuildSettingsOperations; use crate::messaging::message::CrackedMessage; use crate::utils::send_response_poise; use crate::Context; @@ -42,14 +43,14 @@ pub async fn join_leave_log_channel( let _ = data .guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { e.set_join_leave_log_channel(channel_id.get()); }); - let opt_settings = data.guild_settings_map.read().unwrap().clone(); - let settings = opt_settings.get(&guild_id); + let settings_temp = data.get_guild_settings(guild_id).await; + let settings = settings_temp.as_ref(); let pg_pool = ctx.data().database_pool.clone().unwrap(); settings.map(|s| s.save(&pg_pool)).unwrap().await?; diff --git a/crack-core/src/commands/settings/set/set_music_channel.rs b/crack-core/src/commands/settings/set/set_music_channel.rs index 431aeecd4..fe8993b36 100644 --- a/crack-core/src/commands/settings/set/set_music_channel.rs +++ b/crack-core/src/commands/settings/set/set_music_channel.rs @@ -1,9 +1,9 @@ -use serenity::all::Channel; - +use crate::guild::operations::GuildSettingsOperations; use crate::{ errors::CrackedError, messaging::message::CrackedMessage, utils::send_response_poise, Context, Error, }; +use serenity::all::Channel; #[poise::command(prefix_command, required_permissions = "ADMINISTRATOR")] pub async fn music_channel( @@ -25,27 +25,21 @@ pub async fn music_channel( }; let data = ctx.data(); - let _ = data - .guild_settings_map - .write() - .unwrap() - .entry(guild_id) - .and_modify(|e| { - e.set_music_channel(channel_id.get()); - }); + let _ = data.set_music_channel(guild_id, channel_id).await; - let opt_settings = data.guild_settings_map.read().unwrap().clone(); + let opt_settings = data.guild_settings_map.read().await.clone(); let settings = opt_settings.get(&guild_id); let pg_pool = ctx.data().database_pool.clone().unwrap(); settings.map(|s| s.save(&pg_pool)).unwrap().await?; - send_response_poise( + let msg = send_response_poise( ctx, CrackedMessage::Other(format!("Music channel set to {}", channel_id)), true, ) .await?; + data.add_msg_to_cache(guild_id, msg); Ok(()) } diff --git a/crack-core/src/commands/settings/set/set_premium.rs b/crack-core/src/commands/settings/set/set_premium.rs index d18b4f3f3..35de365d9 100644 --- a/crack-core/src/commands/settings/set/set_premium.rs +++ b/crack-core/src/commands/settings/set/set_premium.rs @@ -1,62 +1,47 @@ -use serenity::all::GuildId; -use std::collections::HashMap; -use std::sync::Arc; -use std::sync::RwLock; - +use crate::commands::CrackedError; use crate::db::GuildEntity; -use crate::guild::settings::GuildSettings; use crate::messaging::message::CrackedMessage; -use crate::utils::get_guild_name; use crate::utils::send_response_poise; use crate::{Context, Error}; -/// Convenience type for readability. -type TSGuildSettingsMap = Arc>>; - -/// Do the actual settings of the premium status internally. -pub async fn do_set_premium( - guild_id: serenity::model::id::GuildId, - guild_name: String, - prefix: String, - guild_settings_map: TSGuildSettingsMap, - premium: bool, -) -> Result { - let mut write_guard = guild_settings_map.write().unwrap(); - let settings = write_guard - .entry(guild_id) - .and_modify(|e| { - e.premium = premium; - }) - .or_insert( - GuildSettings::new(guild_id, Some(&prefix.clone()), Some(guild_name.clone())) - .with_premium(premium), - ); - Ok(settings.clone()) -} - +// /// Convenience type for readability. +// type TSGuildSettingsMap = Arc>>; + +// /// Do the actual settings of the premium status internally. +// pub async fn do_set_premium( +// guild_id: serenity::model::id::GuildId, +// guild_name: String, +// prefix: String, +// guild_settings_map: TSGuildSettingsMap, +// premium: bool, +// ) -> Result { +// let mut write_guard = guild_settings_map.write().await; +// let settings = write_guard +// .entry(guild_id) +// .and_modify(|e| { +// e.premium = premium; +// }) +// .or_insert( +// GuildSettings::new(guild_id, Some(&prefix.clone()), Some(guild_name.clone())) +// .with_premium(premium), +// ); +// Ok(settings.clone()) +// } + +use crate::guild::operations::GuildSettingsOperations; /// Internal set premium function without #command macro. #[cfg(not(tarpaulin_include))] -pub async fn set_premium_(ctx: Context<'_>, premium: bool) -> Result { - let guild_id = ctx.guild_id().unwrap(); - let guild_name = get_guild_name(ctx.serenity_context(), guild_id).unwrap_or_default(); - let prefix = ctx.data().bot_settings.get_prefix(); +pub async fn set_premium_internal(ctx: Context<'_>, premium: bool) -> Result<(), CrackedError> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - // here premium is parsed - let guild_settings_map = ctx.data().guild_settings_map.clone(); // Clone the guild_settings_map - let settings = do_set_premium( - guild_id, - guild_name.clone(), - prefix, - guild_settings_map, // Use the cloned guild_settings_map - premium, - ) - .await?; + ctx.data().set_premium(guild_id, premium).await; let pool = ctx.data().database_pool.clone().unwrap(); - GuildEntity::update_premium(&pool, settings.guild_id.get() as i64, premium) + GuildEntity::update_premium(&pool, guild_id.get() as i64, premium) .await .unwrap(); - send_response_poise(ctx, CrackedMessage::Premium(premium), true).await?; - Ok(settings) + let msg = send_response_poise(ctx, CrackedMessage::Premium(premium), true).await?; + ctx.data().add_msg_to_cache(guild_id, msg); + Ok(()) } /// Set the premium status of the guild. @@ -66,24 +51,5 @@ pub async fn premium( ctx: Context<'_>, #[description = "True or false setting for premium."] premium: bool, ) -> Result<(), Error> { - set_premium_(ctx, premium).await.map(|_| ()) -} - -#[cfg(test)] -mod test { - use super::*; - - /// Test setting premium of the Guild settings structure - #[tokio::test] - async fn test_set_premium() { - let guild_id = GuildId::new(1); - let guild_name = "test".to_string(); - let prefix = "!".to_string(); - let guild_settings_map = HashMap::::default(); // Change the type to GuildSettingsMap - let params = Arc::new(RwLock::new(guild_settings_map)); - let settings = do_set_premium(guild_id, guild_name, prefix, params, true) - .await - .unwrap(); - assert_eq!(settings.premium, true); - } + set_premium_internal(ctx, premium).await.map_err(Into::into) } diff --git a/crack-core/src/commands/settings/set/set_volume.rs b/crack-core/src/commands/settings/set/set_volume.rs index ba1dda979..187175e5a 100644 --- a/crack-core/src/commands/settings/set/set_volume.rs +++ b/crack-core/src/commands/settings/set/set_volume.rs @@ -6,12 +6,12 @@ use crate::{ use serenity::all::GuildId; /// Get the current `volume` and `old_volume` setting for the guild. -pub fn set_volume( +pub async fn set_volume( guild_settings_map: &GuildSettingsMapParam, guild_id: GuildId, vol: f32, ) -> (f32, f32) { - let mut guild_settings_mut = guild_settings_map.write().unwrap(); + let mut guild_settings_mut = guild_settings_map.write().await; guild_settings_mut .entry(guild_id) .and_modify(|e| { @@ -34,7 +34,7 @@ pub async fn volume( let (vol, old_vol) = { let guild_settings_map = &ctx.data().guild_settings_map; - set_volume(guild_settings_map, guild_id, volume) + set_volume(guild_settings_map, guild_id, volume).await }; let msg = ctx @@ -52,31 +52,31 @@ mod test { use crate::guild::settings::{GuildSettingsMapParam, DEFAULT_VOLUME_LEVEL}; use serenity::model::id::GuildId; - #[test] - fn test_set_volume() { + #[tokio::test] + async fn test_set_volume() { let guild_id = GuildId::new(1); let guild_settings_map = GuildSettingsMapParam::default(); - let (vol, old_vol) = set_volume(&guild_settings_map, guild_id, 0.5); + let (vol, old_vol) = set_volume(&guild_settings_map, guild_id, 0.5).await; assert_eq!(vol, 0.5); assert_eq!(old_vol, DEFAULT_VOLUME_LEVEL); assert_eq!( guild_settings_map .read() - .unwrap() + .await .get(&guild_id) .unwrap() .volume, vol ); - let (vol, old_vol) = set_volume(&guild_settings_map, guild_id, 0.6); + let (vol, old_vol) = set_volume(&guild_settings_map, guild_id, 0.6).await; assert_eq!(vol, 0.6); assert_eq!(old_vol, 0.5); assert_eq!( guild_settings_map .read() - .unwrap() + .await .get(&guild_id) .unwrap() .volume, diff --git a/crack-core/src/commands/settings/set/set_welcome_settings.rs b/crack-core/src/commands/settings/set/set_welcome_settings.rs index 26833ec3b..671bdfc88 100644 --- a/crack-core/src/commands/settings/set/set_welcome_settings.rs +++ b/crack-core/src/commands/settings/set/set_welcome_settings.rs @@ -4,7 +4,39 @@ use crate::{ utils::get_guild_name, Context, Data, Error, }; -use serenity::all::{Channel, GuildId}; +use serenity::all::{Channel, GuildId, Role}; + +/// Set password verification for the server. +#[poise::command(prefix_command, ephemeral, required_permissions = "ADMINISTRATOR")] +#[cfg(not(tarpaulin_include))] +pub async fn password_verify( + ctx: Context<'_>, + #[description = "The channel use for verification message"] channel: Channel, + #[description = "Password to verify"] password: String, + #[description = "Role to add after successful verification"] auto_role: Role, + #[rest] + #[description = "Welcome message template use {user} for username"] + message: String, +) -> Result<(), Error> { + let prefix = ctx.data().bot_settings.get_prefix(); + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let welcome_settings = WelcomeSettings { + channel_id: Some(channel.id().get()), + message: Some(message.clone()), + auto_role: Some(auto_role.id.get()), + password: Some(password), + }; + let msg = set_welcome_settings( + ctx.data().clone(), + guild_id, + get_guild_name(ctx.serenity_context(), guild_id), + prefix.to_string(), + welcome_settings, + ) + .await?; + ctx.say(msg).await?; + Ok(()) +} /// Set the welcome settings for the server. #[poise::command(prefix_command, ephemeral, required_permissions = "ADMINISTRATOR")] @@ -22,6 +54,7 @@ pub async fn welcome_settings( channel_id: Some(channel.id().get()), message: Some(message.clone()), auto_role: None, + password: None, }; let msg = set_welcome_settings( ctx.data().clone(), @@ -48,7 +81,7 @@ pub async fn set_welcome_settings( let channel_id = init_welcome_settings.channel_id.unwrap(); let message = init_welcome_settings.message.as_ref().unwrap(); let res = { - let mut write_guard = guild_settings_map.write().unwrap(); + let mut write_guard = guild_settings_map.write().await; write_guard .entry(guild_id) .and_modify(|e| { @@ -90,7 +123,7 @@ mod test { let init_welcome_settings = WelcomeSettings { channel_id: Some(1), message: Some("Welcome {user}!".to_string()), - auto_role: None, + ..Default::default() }; let res = diff --git a/crack-core/src/commands/settings/toggle/self_deafen.rs b/crack-core/src/commands/settings/toggle/self_deafen.rs index 924b5da70..77a7573a8 100644 --- a/crack-core/src/commands/settings/toggle/self_deafen.rs +++ b/crack-core/src/commands/settings/toggle/self_deafen.rs @@ -38,7 +38,7 @@ pub async fn toggle_self_deafen( let res = data .guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { e.toggle_self_deafen(); diff --git a/crack-core/src/commands/settings/toggle/toggle_autopause.rs b/crack-core/src/commands/settings/toggle/toggle_autopause.rs index 3767415bf..84ca24bad 100644 --- a/crack-core/src/commands/settings/toggle/toggle_autopause.rs +++ b/crack-core/src/commands/settings/toggle/toggle_autopause.rs @@ -42,7 +42,7 @@ pub async fn toggle_autopause_( let res = data .guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { e.toggle_autopause(); diff --git a/crack-core/src/commands/version.rs b/crack-core/src/commands/version.rs index 5b8f36425..7d5053dd1 100644 --- a/crack-core/src/commands/version.rs +++ b/crack-core/src/commands/version.rs @@ -1,3 +1,4 @@ +use crate::guild::operations::GuildSettingsOperations; use crate::{messaging::message::CrackedMessage, utils::send_response_poise, Context, Error}; /// Get the current version of the bot. @@ -5,11 +6,7 @@ use crate::{messaging::message::CrackedMessage, utils::send_response_poise, Cont #[poise::command(slash_command, prefix_command)] pub async fn version(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); - let reply_with_embed = ctx - .data() - .get_guild_settings(guild_id) - .unwrap() - .reply_with_embed; + let reply_with_embed = ctx.data().get_reply_with_embed(guild_id).await; let current = option_env!("CARGO_PKG_VERSION").unwrap_or_else(|| "Unknown"); let hash = option_env!("GIT_HASH").unwrap_or_else(|| "Unknown"); let msg = send_response_poise( @@ -21,6 +18,6 @@ pub async fn version(ctx: Context<'_>) -> Result<(), Error> { reply_with_embed, ) .await?; - ctx.data().add_msg_to_cache(ctx.guild_id().unwrap(), msg); + ctx.data().add_msg_to_cache(guild_id, msg); Ok(()) } diff --git a/crack-core/src/db/metadata.rs b/crack-core/src/db/metadata.rs index da0252fa3..107cf9ae6 100644 --- a/crack-core/src/db/metadata.rs +++ b/crack-core/src/db/metadata.rs @@ -140,6 +140,27 @@ impl Metadata { thumbnail: r.thumbnail, }) } + + /// Get a metadata entry by id (url). + pub async fn get_by_url(pool: &PgPool, url: &str) -> Result, CrackedError> { + let r: Option = sqlx::query_as!(MetadataRead, r#" + SELECT + metadata.id, metadata.track, metadata.artist, metadata.album, metadata.date, metadata.channels, metadata.channel, metadata.start_time, metadata.duration, metadata.sample_rate, metadata.source_url, metadata.title, metadata.thumbnail + FROM + metadata + WHERE + metadata.source_url = $1 + "#, + url + ) + .fetch_optional(pool) + .await + .map_err(CrackedError::SQLX)?; + match r { + Some(r) => Ok(Some(r.into())), + None => Ok(None), + } + } } impl From for Metadata { diff --git a/crack-core/src/db/mod.rs b/crack-core/src/db/mod.rs index f487b4fb8..1c5c3503f 100644 --- a/crack-core/src/db/mod.rs +++ b/crack-core/src/db/mod.rs @@ -4,6 +4,7 @@ pub mod play_log; pub mod playlist; pub mod track_reaction; pub mod user; +pub mod worker_pool; pub use guild::*; pub use metadata::*; @@ -11,3 +12,4 @@ pub use play_log::*; pub use playlist::*; pub use track_reaction::*; pub use user::*; +pub use worker_pool::*; diff --git a/crack-core/src/db/playlist.rs b/crack-core/src/db/playlist.rs index efd49aee9..c25db19ec 100644 --- a/crack-core/src/db/playlist.rs +++ b/crack-core/src/db/playlist.rs @@ -1,8 +1,6 @@ -use crate::db::user::User; -use songbird::tracks::TrackHandle; -use sqlx::{postgres::PgQueryResult, query, PgPool}; - +use crate::db::{user::User, Metadata, MetadataRead}; use crate::CrackedError; +use sqlx::{postgres::PgQueryResult, query, PgPool}; /// Playlist db structure (does not old the tracks) #[derive(Debug, Default)] @@ -23,15 +21,11 @@ pub struct PlaylistTrack { pub channel_id: Option, } +/// Implementation of the Playlist struct for writing to the database impl Playlist { + /// Create a new playlist for a user. pub async fn create(pool: &PgPool, name: &str, user_id: i64) -> Result { if User::get_user(pool, user_id).await.is_none() { - // match User::insert_user(pool, user_id, "FAKENAME".to_string()).await { - // Ok(_) => (), - // Err(e) => { - // return Err(CrackedError::SQLX(e)); - // } - // } return Err(CrackedError::Other( "(playlist::create) User does not exist", )); @@ -48,6 +42,7 @@ impl Playlist { Ok(rec) } + /// Add a track to a playlist. pub async fn add_track( pool: &PgPool, playlist_id: i32, @@ -193,12 +188,14 @@ impl Playlist { .await } + /// Get the metadata for the tracks for a playlist. This is what is needed + /// to queue the playlist. pub async fn get_track_metadata_for_playlist( pool: &PgPool, playlist_id: i32, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { sqlx::query_as!( - crate::db::MetadataRead, + MetadataRead, r#" SELECT metadata.id, track, artist, album, date, channels, channel, start_time, duration, sample_rate, source_url, title, thumbnail @@ -213,13 +210,14 @@ impl Playlist { .map(|r| r.into_iter().map(|r| r.into()).collect()) } + /// Gets the metadata for a playlist for a user by playlist name. pub async fn get_track_metadata_for_playlist_name( pool: &PgPool, playlist_name: String, user_id: i64, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { sqlx::query_as!( - crate::db::MetadataRead, + MetadataRead, r#" SELECT metadata.id, track, artist, album, date, channels, channel, start_time, duration, sample_rate, source_url, title, thumbnail @@ -231,7 +229,7 @@ impl Playlist { ) .fetch_all(pool) .await - .map(|r| r.into_iter().map(|r| r.into()).collect()) + .map(|r| r.into_iter().map(Into::into).collect()) } /// Delete a playlist by playlist name and user ID @@ -258,82 +256,3 @@ impl Playlist { Self::delete_playlist(pool, playlist_id).await.map(|_| ()) } } - -use crate::db::metadata::Metadata; - -pub async fn track_handle_to_db_structures( - _pool: &PgPool, - _track_handle: TrackHandle, - _playlist_id: i64, - _guild_id: i64, - _channel_id: i64, -) -> Result<(Metadata, PlaylistTrack), CrackedError> { - // 1. Extract metadata from TrackHandle - Err(CrackedError::Other("not implemented")) - // track_handle.action(View).await?; - // track_handle.get - // let track = track_handle.metadata().track.clone(); - // let title = track_handle.metadata().title.clone(); - // let artist = track_handle.metadata().artist.clone(); - // let album = Some("".to_string()); - // let date = track_handle - // .metadata() - // .date - // .clone() - // .map(|d| NaiveDate::parse_from_str(&d, "%Y-%m-%d").unwrap_or_default()); - // let channels = track_handle.metadata().channels; - // let channel = Some(channel_id); - // let start_time = track_handle - // .metadata() - // .start_time - // .map(|d| d.as_secs() as i64); - // let duration = track_handle.metadata().duration.map(|d| d.as_secs() as i64); - // let sample_rate = track_handle.metadata().sample_rate.map(i64::from); - // let source_url = track_handle.metadata().source_url.clone(); - // let thumbnail = track_handle.metadata().thumbnail.clone(); - - // let metadata = sqlx::query_as!( - // Metadata, - // r#"INSERT INTO - // metadata (track, artist, album, date, channels, channel, start_time, duration, sample_rate, source_url, title, thumbnail) - // VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - // RETURNING id, track, artist, album, date, channels, channel, start_time, duration, sample_rate, source_url, title, thumbnail - // "#, - // track, - // artist, - // album, - // date, - // channels, - // channel, - // start_time, - // duration, - // sample_rate, - // source_url, - // title, - // thumbnail - // ) - // .fetch_one(pool) - // .await - // .map_err(CrackedError::SQLX)?; - - // let guild_id_opt = Some(guild_id); - // let channel_id_opt = Some(channel_id); - // // 3. Populate the PlaylistTrack structure - // let playlist_track = sqlx::query_as!( - // PlaylistTrack, - // r#"INSERT INTO playlist_track - // (playlist_id, metadata_id, guild_id, channel_id) - // VALUES (?, ?, ?, ?) - // RETURNING id, playlist_id, metadata_id, guild_id, channel_id - // "#, - // playlist_id, - // metadata.id, - // guild_id_opt, - // channel_id_opt - // ) - // .fetch_one(pool) - // .await - // .map_err(CrackedError::SQLX)?; - - // Ok((metadata, playlist_track)) -} diff --git a/crack-core/src/db/user.rs b/crack-core/src/db/user.rs index 6981ae884..e0dab0916 100644 --- a/crack-core/src/db/user.rs +++ b/crack-core/src/db/user.rs @@ -18,6 +18,13 @@ pub struct User { pub last_seen: chrono::NaiveDate, } +/// UserTrace is a struct that represents a trace of a user's activity. +pub struct UserTrace { + pub user_id: i64, + pub ts: chrono::NaiveDateTime, + pub whence: Option, +} + /// UserVote is a struct that represents a vote from a user for the bot on a toplist site. #[derive(sqlx::FromRow)] pub struct UserVote { @@ -63,6 +70,18 @@ impl User { } } + /// Records the last seen time of a user. + /// This probably should be brough out of the db part of the code. + pub async fn record_last_seen(pool: &PgPool, user_id: i64) -> Result { + sqlx::query_as!( + UserTrace, + r#"INSERT INTO public.user_trace (user_id, ts, whence) VALUES ($1, now(), NULL) RETURNING user_id, ts, whence"#, + user_id + ) + .fetch_one(pool) + .await + } + /// Insert a user into the database if it's new, update the username and lastseen otherwise. pub async fn insert_or_update_user( pool: &PgPool, diff --git a/crack-core/src/db/worker_pool.rs b/crack-core/src/db/worker_pool.rs new file mode 100644 index 000000000..2a9759d07 --- /dev/null +++ b/crack-core/src/db/worker_pool.rs @@ -0,0 +1,154 @@ +use serenity::all::{ChannelId, GuildId, UserId}; +use songbird::input::AuxMetadata; +use sqlx::postgres::PgPool; +use std::fmt; +use std::fmt::{Display, Formatter}; +use tokio::sync::mpsc; +use tracing; + +use crate::db::{metadata::aux_metadata_to_db_structures, Metadata, PlayLog, User}; +use crate::CrackedError; + +// TODO: Make this configurable, and experiment to find a good default. +const CHANNEL_BUF_SIZE: usize = 1024; + +/// Data needed to write a metadata entry to the database. +#[derive(Debug, Clone)] +pub struct MetadataMsg { + pub aux_metadata: AuxMetadata, + pub user_id: UserId, + pub username: String, + pub guild_id: GuildId, + pub channel_id: ChannelId, +} + +impl Display for MetadataMsg { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!( + f, + "MetadataMsg {{ aux_metadata: {:?}, user_id: {:?}, username: {:?}, guild_id: {:?}, channel_id: {:?} }}", + self.aux_metadata, self.user_id, self.username, self.guild_id, self.channel_id + ) + } +} + +/// Writes metadata to the database for a playing track. +pub async fn write_metadata_pg( + database_pool: &PgPool, + data: MetadataMsg, +) -> Result { + let MetadataMsg { + aux_metadata, + user_id, + username, + guild_id, + channel_id, + } = data; + let returned_metadata = { + let (metadata, _playlist_track) = match aux_metadata_to_db_structures( + &aux_metadata, + guild_id.get() as i64, + channel_id.get() as i64, + ) { + Ok(x) => x, + Err(e) => { + tracing::error!("aux_metadata_to_db_structures error: {}", e); + return Err(CrackedError::Other("aux_metadata_to_db_structures error")); + }, + }; + let updated_metadata = + match crate::db::metadata::Metadata::get_or_create(database_pool, &metadata).await { + Ok(x) => x, + Err(e) => { + tracing::error!("crate::db::metadata::Metadata::create error: {}", e); + metadata.clone() + }, + }; + + match User::insert_or_update_user(database_pool, user_id.get() as i64, username).await { + Ok(_) => { + tracing::info!("Users::insert_or_update"); + }, + Err(e) => { + tracing::error!("Users::insert_or_update error: {}", e); + }, + }; + match PlayLog::create( + database_pool, + user_id.get() as i64, + guild_id.get() as i64, + updated_metadata.id as i64, + ) + .await + { + Ok(x) => { + tracing::info!("PlayLog::create: {:?}", x); + }, + Err(e) => { + tracing::error!("PlayLog::create error: {}", e); + }, + }; + metadata + }; + Ok(returned_metadata) +} + +/// Run a worker that writes metadata to the database. +pub async fn run_db_worker(mut receiver: mpsc::Receiver, pool: PgPool) { + while let Some(message) = receiver.recv().await { + tracing::trace!("Received message in run_db_worker: {}", message); + match write_metadata_pg(&pool, message).await { + Ok(_) => tracing::trace!("Metadata written to database"), + Err(e) => tracing::warn!("Failed to write metadata to database: {}", e), + } + } +} + +/// Setup the workers to handle db writes asynchronously during runtime. +pub async fn setup_workers(pool: PgPool) -> mpsc::Sender { + let (tx, rx) = mpsc::channel(CHANNEL_BUF_SIZE); + let pool = pool.clone(); + tokio::spawn(async move { + run_db_worker(rx, pool).await; + }); + tx +} + +#[cfg(test)] +mod test { + use super::*; + + pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./test_migrations"); + + #[sqlx::test(migrator = "MIGRATOR")] + async fn test_workers(pool: PgPool) { + let url = "https://www.youtube.com/watch?v=6n3pFFPSlW4".to_string(); + let sender = setup_workers(pool.clone()).await; + let data = MetadataMsg { + aux_metadata: AuxMetadata { + source_url: Some(url.clone()), + ..Default::default() + }, + user_id: UserId::new(1), + username: "test".to_string(), + guild_id: GuildId::new(1), + channel_id: ChannelId::new(1), + }; + sender.send(data).await.unwrap(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + let metadata = crate::db::metadata::Metadata::get_by_url(&pool, &url) + .await + .unwrap() + .unwrap(); + assert_eq!(metadata.id, 1); + } + + #[sqlx::test(migrator = "MIGRATOR")] + async fn test_get_failed_metadata(pool: PgPool) { + let url = "https://www.youtube.com/watch?v=6n3pFFPSlW4".to_string(); + let metadata = crate::db::metadata::Metadata::get_by_url(&pool, &url) + .await + .unwrap(); + assert!(metadata.is_none()); + } +} diff --git a/crack-core/src/errors.rs b/crack-core/src/errors.rs index d6cbc4f5f..0c96c333a 100644 --- a/crack-core/src/errors.rs +++ b/crack-core/src/errors.rs @@ -1,10 +1,12 @@ use crate::messaging::messages::{ - EMPTY_SEARCH_RESULT, FAIL_ANOTHER_CHANNEL, FAIL_AUTHOR_DISCONNECTED, FAIL_AUTHOR_NOT_FOUND, - FAIL_EMPTY_VECTOR, FAIL_INVALID_TOPGG_TOKEN, FAIL_NOTHING_PLAYING, FAIL_NOT_IMPLEMENTED, + EMPTY_SEARCH_RESULT, FAIL_ANOTHER_CHANNEL, FAIL_AUDIO_STREAM_RUSTY_YTDL_METADATA, + FAIL_AUTHOR_DISCONNECTED, FAIL_AUTHOR_NOT_FOUND, FAIL_EMPTY_VECTOR, FAIL_INSERT, + FAIL_INVALID_PERMS, FAIL_INVALID_TOPGG_TOKEN, FAIL_NOTHING_PLAYING, FAIL_NOT_IMPLEMENTED, FAIL_NO_SONGBIRD, FAIL_NO_VIRUSTOTAL_API_KEY, FAIL_NO_VOICE_CONNECTION, FAIL_PARSE_TIME, - FAIL_PLAYLIST_FETCH, FAIL_WRONG_CHANNEL, GUILD_ONLY, NOT_IN_MUSIC_CHANNEL, NO_CHANNEL_ID, - NO_DATABASE_POOL, NO_GUILD_CACHED, NO_GUILD_ID, NO_GUILD_SETTINGS, QUEUE_IS_EMPTY, - ROLE_NOT_FOUND, SPOTIFY_AUTH_FAILED, UNAUTHORIZED_USER, + FAIL_PLAYLIST_FETCH, FAIL_TO_SET_CHANNEL_SIZE, FAIL_WRONG_CHANNEL, GUILD_ONLY, + NOT_IN_MUSIC_CHANNEL, NO_CHANNEL_ID, NO_DATABASE_POOL, NO_GUILD_CACHED, NO_GUILD_ID, + NO_GUILD_SETTINGS, NO_USER_AUTOPLAY, QUEUE_IS_EMPTY, ROLE_NOT_FOUND, SPOTIFY_AUTH_FAILED, + UNAUTHORIZED_USER, }; use crate::Error; use audiopus::error::Error as AudiopusError; @@ -25,6 +27,7 @@ use std::process::ExitStatus; pub enum CrackedError { AlreadyConnected(Mention), AudioStream(AudioStreamError), + AudioStreamRustyYtdlMetadata, AuthorDisconnected(Mention), AuthorNotFound, Anyhow(anyhow::Error), @@ -34,11 +37,14 @@ pub enum CrackedError { DurationParseError(String, String), EmptySearchResult, EmptyVector(&'static str), + FailedToInsert, + FailedToSetChannelSize(String, ChannelId, u32, Error), GuildOnly, JoinChannelError(JoinError), Json(serde_json::Error), InvalidIP(String), InvalidTopGGToken, + InvalidPermissions, IO(std::io::Error), LogChannelWarning(&'static str, GuildId), NotInRange(&'static str, isize, isize, isize), @@ -98,6 +104,9 @@ impl Display for CrackedError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::AudioStream(err) => f.write_str(&format!("{err}")), + Self::AudioStreamRustyYtdlMetadata => { + f.write_str(FAIL_AUDIO_STREAM_RUSTY_YTDL_METADATA) + }, Self::AuthorDisconnected(mention) => { f.write_fmt(format_args!("{} {}", FAIL_AUTHOR_DISCONNECTED, mention)) }, @@ -116,10 +125,15 @@ impl Display for CrackedError { }, Self::EmptySearchResult => f.write_str(EMPTY_SEARCH_RESULT), Self::EmptyVector(msg) => f.write_str(&format!("{} {}", FAIL_EMPTY_VECTOR, msg)), + Self::FailedToInsert => f.write_str(FAIL_INSERT), + Self::FailedToSetChannelSize(name, id, size, err) => f.write_str(&format!( + "{FAIL_TO_SET_CHANNEL_SIZE} {name}, {id}, {size}\n{err}" + )), Self::GuildOnly => f.write_str(GUILD_ONLY), Self::IO(err) => f.write_str(&format!("{err}")), Self::InvalidIP(ip) => f.write_str(&format!("Invalid ip {}", ip)), Self::InvalidTopGGToken => f.write_str(FAIL_INVALID_TOPGG_TOKEN), + Self::InvalidPermissions => f.write_str(FAIL_INVALID_PERMS), Self::JoinChannelError(err) => f.write_str(&format!("{err}")), Self::Json(err) => f.write_str(&format!("{err}")), Self::LogChannelWarning(event_name, guild_id) => f.write_str(&format!( @@ -145,7 +159,7 @@ impl Display for CrackedError { }, Self::NoGuildSettings => f.write_str(NO_GUILD_SETTINGS), Self::NoLogChannel => f.write_str("No log channel"), - Self::NoUserAutoplay => f.write_str("(auto)"), + Self::NoUserAutoplay => f.write_str(NO_USER_AUTOPLAY), Self::NothingPlaying => f.write_str(FAIL_NOTHING_PLAYING), Self::NoSongbird => f.write_str(FAIL_NO_SONGBIRD), Self::NoVirusTotalApiKey => f.write_str(FAIL_NO_VIRUSTOTAL_API_KEY), @@ -202,6 +216,13 @@ impl PartialEq for CrackedError { } } +/// Provides an implementation to convert a [`VideoError`] to a [`CrackedError`]. +impl From for CrackedError { + fn from(err: VideoError) -> Self { + Self::VideoError(err) + } +} + /// Provides an implementation to convert a [`AudioStreamError`] to a [`CrackedError`]. impl From for CrackedError { fn from(err: AudioStreamError) -> Self { @@ -209,6 +230,13 @@ impl From for CrackedError { } } +/// Provides an implementation to convert a [`AudioStreamError`] to a [`CrackedError`]. +impl From for AudioStreamError { + fn from(x: CrackedError) -> Self { + AudioStreamError::Fail(Box::new(x)) + } +} + /// Provides an implementation to convert a [`sqlx::Error`] to a [`CrackedError`]. impl From for CrackedError { fn from(err: sqlx::Error) -> Self { @@ -285,12 +313,6 @@ impl From for CrackedError { } } -impl From for CrackedError { - fn from(err: VideoError) -> CrackedError { - CrackedError::VideoError(err) - } -} - /// Types that implement this trait can be tested as true or false and also provide /// a way of unpacking themselves. pub trait Verifiable { diff --git a/crack-core/src/guild/cache.rs b/crack-core/src/guild/cache.rs index 6ae4b5642..709851233 100644 --- a/crack-core/src/guild/cache.rs +++ b/crack-core/src/guild/cache.rs @@ -4,11 +4,12 @@ use self::serenity::model::{ }; use chrono::{DateTime, Utc}; use poise::serenity_prelude as serenity; -use std::{collections::BTreeMap, sync::RwLock}; +use std::collections::BTreeMap; use std::{ collections::{HashMap, HashSet}, sync::Arc, }; +use tokio::sync::RwLock; use typemap_rev::TypeMapKey; type QueueMessage = (Message, Arc>); diff --git a/crack-core/src/guild/cam_rules.rs b/crack-core/src/guild/cam_rules.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crack-core/src/guild/operations.rs b/crack-core/src/guild/operations.rs index aa17fbf4b..3105b81bd 100644 --- a/crack-core/src/guild/operations.rs +++ b/crack-core/src/guild/operations.rs @@ -1,72 +1,90 @@ +use crate::{errors::CrackedError, Data, GuildSettings}; +use serenity::all::{ChannelId, Context as SerenityContext, GuildId}; use std::{future::Future, sync::Arc}; -use ::serenity::all::Context as SerenityContext; -use serenity::all::{ChannelId, GuildId}; - -use crate::errors::CrackedError; - -use super::settings::GuildSettings; - pub trait GuildSettingsOperations { - fn get_guild_settings(&self, guild_id: GuildId) -> Option; - fn set_guild_settings(&self, guild_id: GuildId, settings: crate::GuildSettings); + fn get_guild_settings(&self, guild_id: GuildId) -> impl Future>; + fn set_guild_settings( + &self, + guild_id: GuildId, + settings: GuildSettings, + ) -> impl Future>; fn get_or_create_guild_settings( &self, guild_id: GuildId, name: Option, prefix: Option<&str>, - ) -> GuildSettings; + ) -> impl Future; fn save_guild_settings( &self, guild_id: GuildId, ) -> impl Future>; - fn get_music_channel(&self, guild_id: GuildId) -> Option; - fn set_music_channel(&self, guild_id: GuildId, channel_id: ChannelId); - fn get_timeout(&self, guild_id: GuildId) -> Option; - fn set_timeout(&self, guild_id: GuildId, timeout: u32); - fn get_premium(&self, guild_id: GuildId) -> Option; - fn set_premium(&self, guild_id: GuildId, premium: bool); - fn get_prefix(&self, guild_id: GuildId) -> Option; - fn set_prefix(&self, guild_id: GuildId, prefix: String); - fn add_prefix(&self, guild_id: GuildId, prefix: String); - fn get_autopause(&self, guild_id: GuildId) -> bool; - fn set_autopause(&self, guild_id: GuildId, autopause: bool); - fn get_autoplay(&self, guild_id: GuildId) -> bool; - fn set_autoplay(&self, guild_id: GuildId, autoplay: bool); + fn get_music_channel(&self, guild_id: GuildId) -> impl Future>; + fn set_music_channel( + &self, + guild_id: GuildId, + channel_id: ChannelId, + ) -> impl Future; + fn get_timeout(&self, guild_id: GuildId) -> impl Future>; + fn set_timeout(&self, guild_id: GuildId, timeout: u32) -> impl Future; + fn get_premium(&self, guild_id: GuildId) -> impl Future>; + fn set_premium(&self, guild_id: GuildId, premium: bool) -> impl Future; + fn get_prefix(&self, guild_id: GuildId) -> impl Future>; + fn set_prefix(&self, guild_id: GuildId, prefix: String) -> impl Future; + fn add_prefix(&self, guild_id: GuildId, prefix: String) -> impl Future; + fn get_additional_prefixes(&self, guild_id: GuildId) -> impl Future>; + fn set_additional_prefixes( + &self, + guild_id: GuildId, + prefixes: Vec, + ) -> impl Future; + fn get_autopause(&self, guild_id: GuildId) -> impl Future; + fn set_autopause(&self, guild_id: GuildId, autopause: bool) -> impl Future; + fn get_autoplay(&self, guild_id: GuildId) -> impl Future; + fn set_autoplay(&self, guild_id: GuildId, autoplay: bool) -> impl Future; + fn get_reply_with_embed(&self, guild_id: GuildId) -> impl Future; + fn set_reply_with_embed(&self, guild_id: GuildId, as_embed: bool) + -> impl Future; } -impl GuildSettingsOperations for crate::Data { - fn get_or_create_guild_settings( +/// Implementation of the guild settings operations. +impl GuildSettingsOperations for Data { + /// Get the guild settings for a guild, creating them if they don't exist. + async fn get_or_create_guild_settings( &self, guild_id: GuildId, name: Option, prefix: Option<&str>, ) -> GuildSettings { - self.get_guild_settings(guild_id).unwrap_or({ + self.get_guild_settings(guild_id).await.unwrap_or({ let settings = GuildSettings::new(guild_id, prefix, name); - self.set_guild_settings(guild_id, settings.clone()); + self.set_guild_settings(guild_id, settings.clone()).await; settings }) } - fn get_guild_settings(&self, guild_id: GuildId) -> Option { - self.guild_settings_map - .read() - .unwrap() - .get(&guild_id) - .cloned() + + /// Get the guild settings for a guild. + async fn get_guild_settings(&self, guild_id: GuildId) -> Option { + self.guild_settings_map.read().await.get(&guild_id).cloned() } - fn set_guild_settings(&self, guild_id: GuildId, settings: crate::GuildSettings) { + /// Set the guild settings for a guild. + async fn set_guild_settings( + &self, + guild_id: GuildId, + settings: GuildSettings, + ) -> Option { self.guild_settings_map .write() - .unwrap() - .insert(guild_id, settings); + .await + .insert(guild_id, settings) } - fn get_music_channel(&self, guild_id: GuildId) -> Option { + /// Get the music channel for the guild. + async fn get_music_channel(&self, guild_id: GuildId) -> Option { self.guild_settings_map .read() - .unwrap() + .await .get(&guild_id) .and_then(|x| { x.command_channels @@ -76,36 +94,40 @@ impl GuildSettingsOperations for crate::Data { }) } - fn set_music_channel(&self, guild_id: GuildId, channel_id: ChannelId) { + /// Set the music channel for the guild. + async fn set_music_channel(&self, guild_id: GuildId, channel_id: ChannelId) { self.guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { e.set_music_channel(channel_id.get()); }); } + /// Save the guild settings to the database. async fn save_guild_settings(&self, guild_id: GuildId) -> Result<(), CrackedError> { - let opt_settings = self.guild_settings_map.read().unwrap().clone(); + let opt_settings = self.guild_settings_map.read().await; let settings = opt_settings.get(&guild_id); let pg_pool = self.database_pool.clone().unwrap(); settings.map(|s| s.save(&pg_pool)).unwrap().await } - fn get_timeout(&self, guild_id: GuildId) -> Option { + /// Get the idle timeout for the bot in VC for the guild. + async fn get_timeout(&self, guild_id: GuildId) -> Option { self.guild_settings_map .read() - .unwrap() + .await .get(&guild_id) .map(|x| x.timeout) } - fn set_timeout(&self, guild_id: GuildId, timeout: u32) { + /// Set the idle timeout for the bot in VC for the guild. + async fn set_timeout(&self, guild_id: GuildId, timeout: u32) { self.guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { e.timeout = timeout; @@ -113,90 +135,136 @@ impl GuildSettingsOperations for crate::Data { .key(); } - fn get_premium(&self, guild_id: GuildId) -> Option { + /// Get the premium status for a guild. + async fn get_premium(&self, guild_id: GuildId) -> Option { self.guild_settings_map .read() - .unwrap() + .await .get(&guild_id) .map(|x| x.premium) } - fn set_premium(&self, guild_id: GuildId, premium: bool) { + /// Set the premium status for a guild. + async fn set_premium(&self, guild_id: GuildId, premium: bool) { self.guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { e.premium = premium; }); } - fn get_prefix(&self, guild_id: GuildId) -> Option { + /// Get the prefix for a guild. + async fn get_prefix(&self, guild_id: GuildId) -> Option { self.guild_settings_map .read() - .unwrap() + .await .get(&guild_id) .map(|x| x.prefix.clone()) } - fn set_prefix(&self, guild_id: GuildId, prefix: String) { + /// Set the prefix for a guild. + async fn set_prefix(&self, guild_id: GuildId, prefix: String) { self.guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { e.prefix = prefix; }); } - fn add_prefix(&self, guild_id: GuildId, prefix: String) { + /// Add a prefix to the additional prefixes in guild settings. + async fn add_prefix(&self, guild_id: GuildId, prefix: String) { self.guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { - e.prefix.push_str(&prefix); + e.additional_prefixes.push(prefix); }); } - fn get_autopause(&self, guild_id: GuildId) -> bool { + /// Get the additional prefixes + async fn get_additional_prefixes(&self, guild_id: GuildId) -> Vec { self.guild_settings_map .read() - .unwrap() + .await + .get(&guild_id) + .map(|x| x.additional_prefixes.clone()) + .unwrap_or_default() + } + + /// Add a prefix to the additional prefixes in guild settings. + async fn set_additional_prefixes(&self, guild_id: GuildId, prefixes: Vec) { + self.guild_settings_map + .write() + .await + .entry(guild_id) + .and_modify(|e| { + e.additional_prefixes = prefixes; + }); + } + + /// Get the current autopause settings. + async fn get_autopause(&self, guild_id: GuildId) -> bool { + self.guild_settings_map + .read() + .await .get(&guild_id) .map(|x| x.autopause) .unwrap_or(false) } - fn set_autopause(&self, guild_id: GuildId, autopause: bool) { + /// Set the autopause setting. + async fn set_autopause(&self, guild_id: GuildId, autopause: bool) { self.guild_settings_map .write() - .unwrap() + .await .entry(guild_id) .and_modify(|e| { e.autopause = autopause; }); } - /// Get the current autoplay settings - fn get_autoplay(&self, guild_id: GuildId) -> bool { + /// Get the current autoplay settings. + async fn get_autoplay(&self, guild_id: GuildId) -> bool { self.guild_cache_map .lock() - .unwrap() + .await .get(&guild_id) .map(|settings| settings.autoplay) .unwrap_or(true) } /// Set the autoplay setting - fn set_autoplay(&self, guild_id: GuildId, autoplay: bool) { + async fn set_autoplay(&self, guild_id: GuildId, autoplay: bool) { self.guild_cache_map .lock() - .unwrap() + .await .entry(guild_id) .or_default() .autoplay = autoplay; } + + /// Get the current reply with embed setting. + async fn get_reply_with_embed(&self, guild_id: GuildId) -> bool { + self.guild_settings_map + .read() + .await + .get(&guild_id) + .map_or(true, |x| x.reply_with_embed) + } + + /// Set the reply with embed setting. + async fn set_reply_with_embed(&self, guild_id: GuildId, as_embed: bool) -> bool { + self.guild_settings_map + .read() + .await + .get(&guild_id) + .map_or(as_embed, |x| x.reply_with_embed) + } } /// Get all guilds the bot is in (that are cached). @@ -220,10 +288,10 @@ mod test { }; use serenity::model::id::ChannelId; use std::collections::HashMap; - use std::sync::RwLock; + use tokio::sync::RwLock; - #[test] - fn test_get_guild_settings() { + #[tokio::test] + async fn test_get_guild_settings() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -238,15 +306,15 @@ mod test { })); assert_eq!( - data.get_guild_settings(guild_id), + data.get_guild_settings(guild_id).await, Some(crate::GuildSettings { ..Default::default() }) ); } - #[test] - fn test_get_or_create_guild_settings() { + #[tokio::test] + async fn test_get_or_create_guild_settings() { let guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); let data = Data(Arc::new(DataInner { @@ -255,7 +323,8 @@ mod test { })); assert_eq!( - data.get_or_create_guild_settings(guild_id, None, None), + data.get_or_create_guild_settings(guild_id, None, None) + .await, crate::GuildSettings { guild_id, ..Default::default() @@ -263,8 +332,8 @@ mod test { ); } - #[test] - fn test_set_guild_settings() { + #[tokio::test] + async fn test_set_guild_settings() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -283,18 +352,19 @@ mod test { crate::GuildSettings { ..Default::default() }, - ); + ) + .await; assert_eq!( - data.get_guild_settings(guild_id), + data.get_guild_settings(guild_id).await, Some(crate::GuildSettings { ..Default::default() }) ); } - #[test] - fn test_get_music_channel() { + #[tokio::test] + async fn test_get_music_channel() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); let channel_id = ChannelId::new(2); @@ -318,11 +388,11 @@ mod test { ..Default::default() }))); - assert_eq!(data.get_music_channel(guild_id), Some(channel_id)); + assert_eq!(data.get_music_channel(guild_id).await, Some(channel_id)); } - #[test] - fn test_set_music_channel() { + #[tokio::test] + async fn test_set_music_channel() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); let channel_id = ChannelId::new(2); @@ -346,13 +416,16 @@ mod test { ..Default::default() }))); - data.set_music_channel(guild_id, ChannelId::new(3)); + data.set_music_channel(guild_id, ChannelId::new(3)).await; - assert_eq!(data.get_music_channel(guild_id), Some(ChannelId::new(3))); + assert_eq!( + data.get_music_channel(guild_id).await, + Some(ChannelId::new(3)) + ); } - #[test] - fn test_get_timeout() { + #[tokio::test] + async fn test_get_timeout() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -367,11 +440,11 @@ mod test { ..Default::default() }))); - assert_eq!(data.get_timeout(guild_id), Some(5)); + assert_eq!(data.get_timeout(guild_id).await, Some(5)); } - #[test] - fn test_set_timeout() { + #[tokio::test] + async fn test_set_timeout() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -386,13 +459,13 @@ mod test { ..Default::default() }))); - data.set_timeout(guild_id, 10); + data.set_timeout(guild_id, 10).await; - assert_eq!(data.get_timeout(guild_id), Some(10)); + assert_eq!(data.get_timeout(guild_id).await, Some(10)); } - #[test] - fn test_get_premium() { + #[tokio::test] + async fn test_get_premium() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -407,11 +480,11 @@ mod test { ..Default::default() }))); - assert_eq!(data.get_premium(guild_id), Some(true)); + assert_eq!(data.get_premium(guild_id).await, Some(true)); } - #[test] - fn test_set_premium() { + #[tokio::test] + async fn test_set_premium() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -426,13 +499,13 @@ mod test { ..Default::default() }))); - data.set_premium(guild_id, false); + data.set_premium(guild_id, false).await; - assert_eq!(data.get_premium(guild_id), Some(false)); + assert_eq!(data.get_premium(guild_id).await, Some(false)); } - #[test] - fn test_get_prefix() { + #[tokio::test] + async fn test_get_prefix() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -447,11 +520,11 @@ mod test { ..Default::default() }))); - assert_eq!(data.get_prefix(guild_id), Some("!".to_string())); + assert_eq!(data.get_prefix(guild_id).await, Some("!".to_string())); } - #[test] - fn test_set_prefix() { + #[tokio::test] + async fn test_set_prefix() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -466,13 +539,13 @@ mod test { ..Default::default() }))); - data.set_prefix(guild_id, "?".to_string()); + data.set_prefix(guild_id, "?".to_string()).await; - assert_eq!(data.get_prefix(guild_id), Some("?".to_string())); + assert_eq!(data.get_prefix(guild_id).await, Some("?".to_string())); } - #[test] - fn test_add_prefix() { + #[tokio::test] + async fn test_add_prefix() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -487,13 +560,17 @@ mod test { ..Default::default() }))); - data.add_prefix(guild_id, "?".to_string()); + data.add_prefix(guild_id, "?".to_string()).await; - assert_eq!(data.get_prefix(guild_id), Some("!?".to_string())); + assert_eq!(data.get_prefix(guild_id).await, Some("!".to_string())); + assert_eq!( + data.get_additional_prefixes(guild_id).await, + vec!["?".to_string()] + ); } - #[test] - fn test_get_autopause() { + #[tokio::test] + async fn test_get_autopause() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -508,11 +585,11 @@ mod test { ..Default::default() }))); - assert_eq!(data.get_autopause(guild_id), true); + assert_eq!(data.get_autopause(guild_id).await, true); } - #[test] - fn test_set_autopause() { + #[tokio::test] + async fn test_set_autopause() { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); guild_settings_map.insert( @@ -527,8 +604,8 @@ mod test { ..Default::default() }))); - data.set_autopause(guild_id, false); + data.set_autopause(guild_id, false).await; - assert_eq!(data.get_autopause(guild_id), false); + assert_eq!(data.get_autopause(guild_id).await, false); } } diff --git a/crack-core/src/guild/permissions.rs b/crack-core/src/guild/permissions.rs index dec94fd0f..132f23bab 100644 --- a/crack-core/src/guild/permissions.rs +++ b/crack-core/src/guild/permissions.rs @@ -24,7 +24,7 @@ impl ConvertToHashSetString for serde_json::Value { } } -/// Implementation of ConvertToHashSetString for Vec. +/// Implementation of ConvertToHashSetString for `Vec`. impl ConvertToHashSetString for Vec { fn convert(self) -> HashSetString { self.into_iter().collect() @@ -50,7 +50,7 @@ impl ConvertToHashSetU64 for serde_json::Value { } } -/// Implementation of ConvertToHashSetU64 for Vec. +/// Implementation of ConvertToHashSetU64 for `Vec`. impl ConvertToHashSetU64 for Vec { fn convert(self) -> HashSetU64 { self.iter().map(|&x| x as u64).collect() diff --git a/crack-core/src/guild/settings.rs b/crack-core/src/guild/settings.rs index cd5505728..39f76d643 100644 --- a/crack-core/src/guild/settings.rs +++ b/crack-core/src/guild/settings.rs @@ -218,6 +218,7 @@ pub struct WelcomeSettings { pub channel_id: Option, pub message: Option, pub auto_role: Option, + pub password: Option, } impl Display for WelcomeSettings { @@ -236,11 +237,50 @@ impl From for WelcomeSettings { channel_id: settings_db.channel_id.map(|x| x as u64), message: settings_db.message, auto_role: settings_db.auto_role.map(|x| x as u64), + password: None, } } } impl WelcomeSettings { + /// Create a new empty welcome settings struct. + pub fn new() -> Self { + Default::default() + } + + /// Set the channel id, returning a new WelcomeSettings. + pub fn with_channel_id(self, channel_id: u64) -> Self { + Self { + channel_id: Some(channel_id), + ..self + } + } + + /// Set the message, returning a new WelcomeSettings. + pub fn with_message(self, message: String) -> Self { + Self { + message: Some(message), + ..self + } + } + + /// Set the auto role, returning a new WelcomeSettings. + pub fn with_auto_role(self, auto_role: u64) -> Self { + Self { + auto_role: Some(auto_role), + ..self + } + } + + /// Set the password, returning a new WelcomeSettings. + pub fn with_password(self, password: String) -> Self { + Self { + password: Some(password), + ..self + } + } + + /// Save the welcome settings to the database. pub async fn save(&self, pool: &PgPool, guild_id: u64) -> Result<(), CrackedError> { crate::db::GuildEntity::write_welcome_settings(pool, guild_id as i64, self) .await @@ -556,20 +596,24 @@ impl GuildSettings { Ok(()) } + /// Set the premium status, mutating. pub fn with_premium(self, premium: bool) -> Self { Self { premium, ..self } } + /// Toggle the autopause setting, mutating. pub fn toggle_autopause(&mut self) -> &mut Self { self.autopause = !self.autopause; self } + /// Toggle the autoplay setting, mutating. pub fn toggle_self_deafen(&mut self) -> &mut Self { self.self_deafen = !self.self_deafen; self } + /// Set the allowed domains, mutating. pub fn set_allowed_domains(&mut self, allowed_str: &str) { let allowed = allowed_str .split(';') @@ -580,6 +624,7 @@ impl GuildSettings { self.allowed_domains = allowed; } + /// Set the banned domains, mutating. pub fn set_banned_domains(&mut self, banned_str: &str) { let banned = banned_str .split(';') @@ -590,6 +635,7 @@ impl GuildSettings { self.banned_domains = banned; } + /// Set the music channel, without mutating. pub fn set_music_channel(&mut self, channel_id: u64) -> &mut Self { self.command_channels.set_music_channel( ChannelId::new(channel_id), @@ -599,6 +645,7 @@ impl GuildSettings { self } + /// Update the allowed domains. pub fn update_domains(&mut self) { if !self.allowed_domains.is_empty() && !self.banned_domains.is_empty() { self.banned_domains.clear(); @@ -610,37 +657,45 @@ impl GuildSettings { } } + /// Authorize a user. pub fn authorize_user(&mut self, user_id: i64) -> &mut Self { self.authorized_users.entry(user_id as u64).or_insert(0); self } + /// Deauthorize a user. pub fn deauthorize_user(&mut self, user_id: i64) { if self.authorized_users.contains_key(&(user_id as u64)) { self.authorized_users.remove(&(user_id as u64)); } } + /// Check if the user is authorized. pub fn check_authorized(&self, user_id: u64) -> bool { self.authorized_users.contains_key(&user_id) } + /// Check if the user is authorized. pub fn check_authorized_user_id(&self, user_id: UserId) -> bool { self.authorized_users.contains_key(&user_id.into()) } + /// Check if the user is a mod. pub fn check_mod(&self, user_id: u64) -> bool { self.authorized_users.get(&user_id).unwrap_or(&0) >= &MOD_VAL } + /// Check if the user is a mod. pub fn check_mod_user_id(&self, user_id: UserId) -> bool { self.authorized_users.get(&user_id.into()).unwrap_or(&0) >= &MOD_VAL } + /// Check if the user is an admin. pub fn check_admin(&self, user_id: u64) -> bool { self.authorized_users.get(&user_id).unwrap_or(&0) >= &ADMIN_VAL } + /// Check if the user is an admin. pub fn check_admin_user_id(&self, user_id: UserId) -> bool { self.authorized_users.get(&user_id.into()).unwrap_or(&0) >= &ADMIN_VAL } @@ -661,25 +716,30 @@ impl GuildSettings { self } + /// Set allow all domains, mutating. pub fn set_allow_all_domains(&mut self, allow: bool) -> &mut Self { self.allow_all_domains = Some(allow); self } + /// Set the idle timeout for the bot, mutating. pub fn set_timeout(&mut self, timeout: u32) -> &mut Self { self.timeout = timeout; self } + /// Set the idle timeout for the bot, without mutating. pub fn with_timeout(self, timeout: u32) -> Self { Self { timeout, ..self } } + /// Set the welcome settings, mutating pub fn set_welcome_settings(&mut self, welcome_settings: WelcomeSettings) -> &mut Self { self.welcome_settings = Some(welcome_settings); self } + /// Set the welcome settings, without mutating. pub fn with_welcome_settings(self, welcome_settings: Option) -> Self { Self { welcome_settings, @@ -687,6 +747,7 @@ impl GuildSettings { } } + /// Another set welcome settings pub fn set_welcome_settings2( &mut self, channel_id: u64, @@ -697,10 +758,12 @@ impl GuildSettings { channel_id: Some(channel_id), message: Some(message.to_string()), auto_role, + ..Default::default() }); self } + /// And a third. pub fn set_welcome_settings3(&mut self, channel_id: u64, message: String) -> &mut Self { self.welcome_settings = Some(WelcomeSettings { channel_id: Some(channel_id), @@ -710,10 +773,13 @@ impl GuildSettings { .clone() .map(|x| x.auto_role) .unwrap_or_default(), + + ..Default::default() }); self } + /// Set the auto role,without mutating. pub fn with_auto_role(self, auto_role: Option) -> Self { let welcome_settings = if let Some(welcome_settings) = self.welcome_settings { WelcomeSettings { @@ -733,6 +799,7 @@ impl GuildSettings { } } + /// Set the auto role, mutating. pub fn set_auto_role(&mut self, auto_role: Option) -> &mut Self { if let Some(welcome_settings) = &mut self.welcome_settings { welcome_settings.auto_role = auto_role; @@ -768,11 +835,13 @@ impl GuildSettings { } } + /// Set the guild name, mutating. pub fn set_prefix(&mut self, prefix: &str) -> &mut Self { self.prefix = prefix.to_string(); self } + /// Set the default additional prefixes, mutating. pub fn set_default_additional_prefixes(&mut self) -> &mut Self { self.additional_prefixes = ADDITIONAL_PREFIXES .to_vec() @@ -782,11 +851,13 @@ impl GuildSettings { self } + /// Set the ignored channels, mutating. pub fn set_ignored_channels(&mut self, ignored_channels: HashSet) -> &mut Self { self.ignored_channels = ignored_channels; self } + /// Get the guild name. pub fn get_guild_name(&self) -> String { if self.guild_name.is_empty() { self.guild_id.to_string() @@ -795,10 +866,12 @@ impl GuildSettings { } } + /// Get the prefix. pub fn get_prefix(&self) -> &str { &self.prefix } + /// Set the all log channel, with mutating. pub fn set_all_log_channel(&mut self, channel_id: u64) -> &mut Self { if let Some(log_settings) = &mut self.log_settings { log_settings.all_log_channel = Some(channel_id); @@ -810,6 +883,7 @@ impl GuildSettings { self } + /// Set the join/leave log channel, without mutating. pub fn with_join_leave_log_channel(&self, channel_id: u64) -> Self { let log_settings = if let Some(log_settings) = self.log_settings.clone() { LogSettings { @@ -828,6 +902,7 @@ impl GuildSettings { } } + /// Set the command channels, notmutating. pub fn with_command_channels(&self, command_channels: CommandChannels) -> Self { Self { command_channels, @@ -835,6 +910,7 @@ impl GuildSettings { } } + /// Set the server join/leave log channel, mutating. pub fn set_join_leave_log_channel(&mut self, channel_id: u64) -> &mut Self { if let Some(log_settings) = &mut self.log_settings { log_settings.join_leave_log_channel = Some(channel_id); @@ -983,7 +1059,7 @@ impl TypeMapKey for AtomicU16Key { /// Convenience type for the GuildSettingsMap pub type GuildSettingsMapParam = - std::sync::Arc>>; + std::sync::Arc>>; #[cfg(test)] mod test { diff --git a/crack-core/src/handlers/event_log.rs b/crack-core/src/handlers/event_log.rs index 92eb1a6af..499ec3fde 100644 --- a/crack-core/src/handlers/event_log.rs +++ b/crack-core/src/handlers/event_log.rs @@ -1,13 +1,7 @@ -use std::{ - collections::HashMap, - sync::{Arc, RwLock}, -}; - use super::event_log_impl::*; - use crate::{ errors::CrackedError, guild::settings::GuildSettings, log_event, log_event2, - utils::send_log_embed_thumb, Data, Error, + utils::send_log_embed_thumb, ArcTRwMap, Data, Error, }; use colored::Colorize; use poise::{ @@ -15,6 +9,7 @@ use poise::{ FrameworkContext, }; use serde::{ser::SerializeStruct, Serialize}; +use serenity::all::User; #[derive(Debug)] pub struct LogEntry { @@ -36,12 +31,13 @@ impl Serialize for LogEntry { } } -pub fn get_log_channel( +/// Gets the log channel for a given guild. +pub async fn get_log_channel( channel_name: &str, guild_id: &GuildId, data: &Data, ) -> Option { - let guild_settings_map = data.guild_settings_map.read().unwrap().clone(); + let guild_settings_map = data.guild_settings_map.read().await; guild_settings_map .get(&guild_id.into()) .map(|x| x.get_log_channel(channel_name)) @@ -50,41 +46,38 @@ pub fn get_log_channel( /// Gets the log channel for a given event and guild. pub async fn get_channel_id( - guild_settings_map: &Arc>>, + guild_settings_map: &ArcTRwMap, guild_id: &GuildId, event: &FullEvent, ) -> Result { - let x = { - let guild_settings_map = guild_settings_map.read().unwrap().clone(); + let guild_settings_map = guild_settings_map.read().await; - let guild_settings = guild_settings_map - .get(guild_id) - .map(Ok) - .unwrap_or_else(|| { - tracing::error!("Failed to get guild_settings for guild_id {}", guild_id); - Err(CrackedError::LogChannelWarning( - event.snake_case_name(), - *guild_id, - )) - })? - .clone(); - match guild_settings.get_log_channel_type_fe(event) { - Some(channel_id) => { - if guild_settings.ignored_channels.contains(&channel_id.get()) { - return Err(CrackedError::LogChannelWarning( - event.snake_case_name(), - *guild_id, - )); - } - Ok(channel_id) - }, - None => Err(CrackedError::LogChannelWarning( + let guild_settings = guild_settings_map + .get(guild_id) + .map(Ok) + .unwrap_or_else(|| { + tracing::error!("Failed to get guild_settings for guild_id {}", guild_id); + Err(CrackedError::LogChannelWarning( event.snake_case_name(), *guild_id, - )), - } - }; - x + )) + })? + .clone(); + match guild_settings.get_log_channel_type_fe(event) { + Some(channel_id) => { + if guild_settings.ignored_channels.contains(&channel_id.get()) { + return Err(CrackedError::LogChannelWarning( + event.snake_case_name(), + *guild_id, + )); + } + Ok(channel_id) + }, + None => Err(CrackedError::LogChannelWarning( + event.snake_case_name(), + *guild_id, + )), + } } /// Handles (routes and logs) an event. @@ -96,9 +89,8 @@ pub async fn handle_event( _framework: FrameworkContext<'_, Data, Error>, data_global: &Data, ) -> Result<(), Error> { - use serenity::all::User; - - let event_log = Arc::new(&data_global.event_log); + // let event_log = Arc::new(&data_global.event_log); + let event_log = std::sync::Arc::new(&data_global.event_log_async); let event_name = event_in.snake_case_name(); let guild_settings = &data_global.guild_settings_map; @@ -111,7 +103,7 @@ pub async fn handle_event( event_in, new_data, &new_data.guild_id.unwrap(), - &ctx.http, + &ctx, event_log, event_name ) @@ -128,7 +120,7 @@ pub async fn handle_event( event_in, new_member, &new_member.guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -145,7 +137,7 @@ pub async fn handle_event( event_in, &log_data, guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -171,7 +163,7 @@ pub async fn handle_event( let _ = data_global .guild_cache_map .lock() - .unwrap() + .await .get_mut(&guild_id) .map(|x| x.time_ordered_messages.insert(now, new_message.clone())) .unwrap_or_default(); @@ -186,7 +178,7 @@ pub async fn handle_event( event_in, new_message, &new_message.guild_id.unwrap(), - &ctx.http, + &ctx, event_log, event_name ) @@ -199,7 +191,7 @@ pub async fn handle_event( event_in, event, &event.guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ) @@ -211,7 +203,7 @@ pub async fn handle_event( event_in, permission, &permission.guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -223,7 +215,7 @@ pub async fn handle_event( event_in, execution, &execution.guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -234,7 +226,7 @@ pub async fn handle_event( event_in, &rule, &rule.guild_id, - &ctx.http, + &ctx, event_log, event_name ), @@ -244,7 +236,7 @@ pub async fn handle_event( event_in, &(event_name.to_string(), rule.clone()), &rule.guild_id, - &ctx.http, + &ctx, event_log, event_name ), @@ -254,7 +246,7 @@ pub async fn handle_event( event_in, &(event_name, rule), &rule.guild_id, - &ctx.http, + &ctx, event_log, event_name ), @@ -264,7 +256,7 @@ pub async fn handle_event( event_in, &(event_name, category), &category.guild_id, - &ctx.http, + &ctx, event_log, event_name ), @@ -274,7 +266,7 @@ pub async fn handle_event( event_in, &(event_name, category), &category.guild_id, - &ctx.http, + &ctx, event_log, event_name ), @@ -284,7 +276,7 @@ pub async fn handle_event( event_in, &(channel, messages), &channel.guild_id, - &ctx.http, + &ctx, event_log, event_name ), @@ -294,7 +286,7 @@ pub async fn handle_event( event_in, &(event_name, pin), &pin.guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ), @@ -310,7 +302,7 @@ pub async fn handle_event( event_in, &(event_name, old, new), &guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -326,7 +318,7 @@ pub async fn handle_event( event_in, &log_data, guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -342,7 +334,7 @@ pub async fn handle_event( event_in, &log_data, guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -353,9 +345,9 @@ pub async fn handle_event( log_guild_create, guild_settings, event_in, - &(guild, is_new, guild_settings), + &(guild, is_new), &guild.id, - &ctx.http, + &ctx, event_log, event_name ) @@ -368,7 +360,7 @@ pub async fn handle_event( event_in, &(guild, is_new, guild_settings), &guild.id, - &ctx.http, + &ctx, event_log, event_name ) @@ -382,7 +374,7 @@ pub async fn handle_event( event_in, &log_data, &incomplete.id, - &ctx.http, + &ctx, event_log, event_name ) @@ -396,7 +388,7 @@ pub async fn handle_event( event_in, &log_data, &incomplete.id, - &ctx.http, + &ctx, event_log, event_name ) @@ -412,7 +404,7 @@ pub async fn handle_event( event_in, &log_data, guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -425,22 +417,26 @@ pub async fn handle_event( event_in, &log_data, guild_id, - &ctx.http, + &ctx, event_log, event_name ) }, + // FIXME: Do a better diff of the old and new member data. + // FIXME: Do we rely always on the cache from serenity or implement + // our in for any reason? (probably not needed). FullEvent::GuildMemberUpdate { old_if_available, new, event, } => { + // let local_event: GuildMemberUpdateEvent = event.clone(); let guild_name = event .guild_id .to_guild_cached(&ctx.cache) .map(|x| x.name.clone()) .unwrap_or_default(); - let guild_settings = data_global.guild_settings_map.read().unwrap().clone(); + let guild_settings = data_global.guild_settings_map.read().await.clone(); let new = new.clone().unwrap(); let maybe_log_channel = guild_settings .get(&new.guild_id) @@ -507,7 +503,7 @@ pub async fn handle_event( send_log_embed_thumb( &guild_name, &channel_id, - &ctx.http, + &ctx, &id.to_string(), &title, &description, @@ -520,9 +516,13 @@ pub async fn handle_event( tracing::debug!(title); }, } - event_log.write_log_obj_note(event_name, Some(notes), &(old_if_available, new)) + event_log + .write_log_obj_note_async(event_name, Some(notes), &(old_if_available, new, event)) + .await + }, + FullEvent::GuildMembersChunk { chunk } => { + event_log.write_log_obj_async(event_name, chunk).await }, - FullEvent::GuildMembersChunk { chunk } => event_log.write_log_obj(event_name, chunk), FullEvent::GuildRoleCreate { new } => { log_event!( log_guild_role_create, @@ -530,7 +530,7 @@ pub async fn handle_event( event_in, &new, &new.guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -547,7 +547,7 @@ pub async fn handle_event( event_in, &log_data, guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -564,7 +564,7 @@ pub async fn handle_event( event_in, &log_data, &new.guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -581,13 +581,13 @@ pub async fn handle_event( event_in, &log_data, &new.guild_id, - &ctx.http, + &ctx, event_log, event_name ) }, FullEvent::GuildScheduledEventCreate { event } => { - // event_log.write_log_obj(event_name, event) + // event_log.write_log_obj_async(event_name, event) let log_data = event; log_event!( log_guild_scheduled_event_create, @@ -595,7 +595,7 @@ pub async fn handle_event( event_in, &log_data, &event.guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -608,7 +608,7 @@ pub async fn handle_event( event_in, &log_data, &event.guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -621,7 +621,7 @@ pub async fn handle_event( event_in, &log_data, &event.guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -634,7 +634,7 @@ pub async fn handle_event( event_in, &log_data, &subscribed.guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -647,7 +647,7 @@ pub async fn handle_event( event_in, &log_data, &unsubscribed.guild_id, - &ctx.http, + &ctx, event_log, event_name ) @@ -663,13 +663,15 @@ pub async fn handle_event( event_in, &log_data, &guild_id, - &ctx.http, + &ctx, event_log, event_name ) }, FullEvent::GuildAuditLogEntryCreate { entry, guild_id } => { - event_log.write_log_obj(event_name, &(entry, guild_id)) + event_log + .write_log_obj_async(event_name, &(entry, guild_id)) + .await }, #[cfg(feature = "cache")] FullEvent::GuildUpdate { @@ -683,7 +685,7 @@ pub async fn handle_event( event_in, &log_data, &new_data.id, - &ctx.http, + &ctx, event_log, event_name ) @@ -700,7 +702,7 @@ pub async fn handle_event( event_in, &log_data, &new_data.id, - &ctx.http, + &ctx, event_log, event_name ) @@ -708,12 +710,12 @@ pub async fn handle_event( FullEvent::IntegrationCreate { integration } => { let log_data = integration; log_event!( - log_unimplemented_event, + log_integration_create, guild_settings, event_in, &log_data, &integration.guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ) @@ -721,12 +723,12 @@ pub async fn handle_event( FullEvent::IntegrationUpdate { integration } => { let log_data = integration; log_event!( - log_unimplemented_event, + log_integration_update, guild_settings, event_in, &log_data, &integration.guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ) @@ -738,25 +740,27 @@ pub async fn handle_event( } => { let log_data = &(integration_id, guild_id, application_id); log_event!( - log_unimplemented_event, + log_integration_delete, guild_settings, event_in, &log_data, &guild_id, - &ctx.http, + &ctx, event_log, event_name ) }, FullEvent::InteractionCreate { interaction } => { let log_data = interaction; + let guild_id = + crate::utils::interaction_to_guild_id(interaction).unwrap_or(GuildId::new(1)); log_event!( - log_unimplemented_event, + log_interaction_create, guild_settings, event_in, &log_data, - &GuildId::new(1), - &ctx.http, + &guild_id, + &ctx, event_log, event_name ) @@ -769,7 +773,7 @@ pub async fn handle_event( event_in, &log_data, &data.guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ) @@ -781,7 +785,7 @@ pub async fn handle_event( event_in, data, &data.guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ) @@ -798,7 +802,7 @@ pub async fn handle_event( event_in, &log_data, &guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ) @@ -807,7 +811,11 @@ pub async fn handle_event( channel_id, multiple_deleted_messages_ids, guild_id, - } => event_log.write_obj(&(channel_id, multiple_deleted_messages_ids, guild_id)), + } => { + event_log + .write_obj(&(channel_id, multiple_deleted_messages_ids, guild_id)) + .await + }, #[cfg(not(feature = "cache"))] FullEvent::MessageUpdate { old_if_available, @@ -833,7 +841,7 @@ pub async fn handle_event( event_in, &log_data, &event.guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ) @@ -863,11 +871,11 @@ pub async fn handle_event( event_in, &log_data, &event.guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ) - // event_log.write_log_obj(event_name, &(old_if_available, new, event)) + // event_log.write_log_obj_async(event_name, &(old_if_available, new, event)) }, FullEvent::ReactionAdd { add_reaction } => { log_event!( @@ -876,7 +884,7 @@ pub async fn handle_event( event_in, add_reaction, &add_reaction.guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ) @@ -888,7 +896,7 @@ pub async fn handle_event( event_in, removed_reaction, &removed_reaction.guild_id.unwrap_or_default(), - &ctx.http, + &ctx, event_log, event_name ) @@ -896,38 +904,62 @@ pub async fn handle_event( FullEvent::ReactionRemoveAll { channel_id, removed_from_message_id, - } => event_log.write_log_obj(event_name, &(channel_id, removed_from_message_id)), + } => { + event_log + .write_log_obj_async(event_name, &(channel_id, removed_from_message_id)) + .await + }, FullEvent::Ready { data_about_bot } => { tracing::info!("{} is connected!", data_about_bot.user.name); - event_log.write_log_obj(event_name, data_about_bot) + event_log + .write_log_obj_async(event_name, data_about_bot) + .await }, - FullEvent::Resume { event } => event_log.write_log_obj(event_name, event), + FullEvent::Resume { event } => event_log.write_log_obj_async(event_name, event).await, FullEvent::StageInstanceCreate { stage_instance } => { - event_log.write_log_obj(event_name, stage_instance) + event_log + .write_log_obj_async(event_name, stage_instance) + .await }, FullEvent::StageInstanceDelete { stage_instance } => { - event_log.write_log_obj(event_name, stage_instance) + event_log + .write_log_obj_async(event_name, stage_instance) + .await }, FullEvent::StageInstanceUpdate { stage_instance } => { - event_log.write_log_obj(event_name, stage_instance) + event_log + .write_log_obj_async(event_name, stage_instance) + .await + }, + FullEvent::ThreadCreate { thread } => { + event_log.write_log_obj_async(event_name, thread).await }, - FullEvent::ThreadCreate { thread } => event_log.write_log_obj(event_name, thread), FullEvent::ThreadDelete { thread, full_thread_data: _, - } => event_log.write_log_obj(event_name, thread), + } => event_log.write_log_obj_async(event_name, thread).await, FullEvent::ThreadListSync { thread_list_sync } => { - event_log.write_log_obj(event_name, thread_list_sync) + event_log + .write_log_obj_async(event_name, thread_list_sync) + .await }, FullEvent::ThreadMemberUpdate { thread_member } => { - event_log.write_log_obj(event_name, thread_member) + event_log + .write_log_obj_async(event_name, thread_member) + .await }, FullEvent::ThreadMembersUpdate { thread_members_update, - } => event_log.write_log_obj(event_name, thread_members_update), - FullEvent::ThreadUpdate { old, new } => event_log.write_log_obj(event_name, &(old, new)), - // FullEvent::Unknown { name, raw } => event_log.write_log_obj(event_name, &(name, raw)), + } => { + event_log + .write_log_obj_async(event_name, thread_members_update) + .await + }, + FullEvent::ThreadUpdate { old, new } => { + event_log.write_log_obj_async(event_name, &(old, new)).await + }, + // FullEvent::Unknown { name, raw } => event_log.write_log_obj_async(event_name, &(name, raw)), FullEvent::UserUpdate { old_data, new } => { let log_data = (old_data, new); let guild_id = new.member.as_ref().unwrap().guild_id.unwrap(); @@ -937,16 +969,22 @@ pub async fn handle_event( event_in, &log_data, &guild_id, - &ctx.http, + &ctx, event_log, event_name ) }, - FullEvent::VoiceServerUpdate { event } => event_log.write_log_obj(event_name, event), + FullEvent::VoiceServerUpdate { event } => { + event_log.write_log_obj_async(event_name, event).await + }, FullEvent::WebhookUpdate { guild_id, belongs_to_channel_id, - } => event_log.write_obj(&(guild_id, belongs_to_channel_id)), + } => { + event_log + .write_obj(&(guild_id, belongs_to_channel_id)) + .await + }, FullEvent::CacheReady { guilds } => { tracing::info!( "{}: {}", diff --git a/crack-core/src/handlers/event_log_impl.rs b/crack-core/src/handlers/event_log_impl.rs index 0e566451c..4b1baa640 100644 --- a/crack-core/src/handlers/event_log_impl.rs +++ b/crack-core/src/handlers/event_log_impl.rs @@ -1,28 +1,20 @@ use super::serenity::voice_state_diff_str; -use crate::{ - guild::settings::{GuildSettings, DEFAULT_PREFIX}, - http_utils::get_guild_name, - utils::send_log_embed_thumb, - Error, -}; +use crate::{http_utils::get_guild_name, utils::send_log_embed_thumb, Error}; use colored::Colorize; use serde::Serialize; -use serenity::all::InviteDeleteEvent; use serenity::all::{ - ActionExecution, ChannelId, ClientStatus, CommandPermissions, Context as SerenityContext, - CurrentUser, Guild, GuildChannel, GuildId, GuildScheduledEventUserAddEvent, - GuildScheduledEventUserRemoveEvent, Http, InviteCreateEvent, Member, Message, MessageId, + ActionExecution, ApplicationId, CacheHttp, ChannelId, ClientStatus, CommandPermissions, + Context as SerenityContext, CurrentUser, Guild, GuildChannel, GuildId, + GuildScheduledEventUserAddEvent, GuildScheduledEventUserRemoveEvent, Integration, + IntegrationId, Interaction, InviteCreateEvent, InviteDeleteEvent, Member, Message, MessageId, MessageUpdateEvent, Presence, Role, RoleId, ScheduledEvent, Sticker, StickerId, }; -use std::{ - collections::HashMap, - sync::{Arc, RwLock}, -}; +use std::{collections::HashMap, sync::Arc}; /// Catchall for logging events that are not implemented. pub async fn log_unimplemented_event( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: T, ) -> Result<(), Error> { let guild_name = crate::http_utils::get_guild_name(http, channel_id).await?; @@ -37,11 +29,120 @@ pub async fn log_unimplemented_event( Ok(()) } -/// Log Invite Create Event. +/// Log Integration Update Event. +#[cfg(not(tarpaulin_include))] +pub async fn log_integration_update( + channel_id: ChannelId, + cache_http: &impl CacheHttp, + log_data: &Integration, +) -> Result<(), Error> { + let integration = log_data.clone(); + let guild_id = integration.guild_id.unwrap_or_default(); + let title = format!("Integration Create Event {}", channel_id); + let description = serde_json::to_string_pretty(&log_data).unwrap_or_default(); + let avatar_url = ""; + let guild_name = get_guild_name(cache_http, channel_id).await?; + send_log_embed_thumb( + &guild_name, + &channel_id, + cache_http, + &guild_id.to_string(), + &title, + &description, + avatar_url, + ) + .await + .map(|_| ()) +} + +/// Log Integration Delete Event. +#[cfg(not(tarpaulin_include))] +pub async fn log_integration_delete( + channel_id: ChannelId, + cache_http: &impl CacheHttp, + log_data: &(&IntegrationId, &GuildId, &Option), +) -> Result<(), Error> { + let &(integration_id, guild_id, _application_id) = log_data; + let title = format!( + "Integration Delete Event {} {} {}", + integration_id, guild_id, channel_id + ); + let description = serde_json::to_string_pretty(log_data).unwrap_or_default(); + let avatar_url = ""; + let guild_name = get_guild_name(cache_http, channel_id).await?; + send_log_embed_thumb( + &guild_name, + &channel_id, + cache_http, + &guild_id.to_string(), + &title, + &description, + avatar_url, + ) + .await + .map(|_| ()) +} + +/// Log Integration Create Event. +#[cfg(not(tarpaulin_include))] +pub async fn log_integration_create( + channel_id: ChannelId, + cache_http: &impl CacheHttp, + log_data: &Integration, +) -> Result<(), Error> { + let integration = log_data.clone(); + let guild_id = integration.guild_id.unwrap_or_default(); + let title = format!("Integration Create Event {}", channel_id); + let description = serde_json::to_string_pretty(&log_data).unwrap_or_default(); + let avatar_url = ""; + let guild_name = get_guild_name(cache_http, channel_id).await?; + send_log_embed_thumb( + &guild_name, + &channel_id, + cache_http, + &guild_id.to_string(), + &title, + &description, + avatar_url, + ) + .await + .map(|_| ()) +} + +/// Log Interaction Create Event. +#[cfg(not(tarpaulin_include))] +pub async fn log_interaction_create( + channel_id: ChannelId, + cache_http: &impl CacheHttp, + log_data: &Interaction, +) -> Result<(), Error> { + use crate::utils::interaction_to_guild_id; + + let interaction = log_data.clone(); + // let guild_id = invite_create_event.guild_id.unwrap_or_default(); + let guild_id = interaction_to_guild_id(&interaction).unwrap_or(GuildId::new(1)); + let title = format!("Interaction Create Event {}", channel_id); + let description = serde_json::to_string_pretty(&log_data).unwrap_or_default(); + let avatar_url = ""; + let guild_name = get_guild_name(cache_http, channel_id).await?; + send_log_embed_thumb( + &guild_name, + &channel_id, + cache_http, + &guild_id.to_string(), + &title, + &description, + avatar_url, + ) + .await + .map(|_| ()) +} + +/// Log Invite Delete Event. #[cfg(not(tarpaulin_include))] pub async fn log_invite_delete( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &InviteDeleteEvent, ) -> Result<(), Error> { let invite_create_event = log_data.clone(); @@ -67,7 +168,7 @@ pub async fn log_invite_delete( #[cfg(not(tarpaulin_include))] pub async fn log_invite_create( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &InviteCreateEvent, ) -> Result<(), Error> { let invite_create_event = log_data.clone(); @@ -93,7 +194,7 @@ pub async fn log_invite_create( #[cfg(not(tarpaulin_include))] pub async fn log_guild_stickers_update( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &(&GuildId, &HashMap), ) -> Result<(), Error> { let (guild_id, _stickers): (&GuildId, &HashMap) = *log_data; @@ -118,7 +219,7 @@ pub async fn log_guild_stickers_update( #[cfg(not(tarpaulin_include))] pub async fn log_guild_scheduled_event_delete( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &ScheduledEvent, ) -> Result<(), Error> { let event = log_data.clone(); @@ -153,7 +254,7 @@ pub async fn log_guild_scheduled_event_delete( #[cfg(not(tarpaulin_include))] pub async fn log_guild_scheduled_event_user_add( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &GuildScheduledEventUserAddEvent, ) -> Result<(), Error> { let event = log_data.clone(); @@ -184,7 +285,7 @@ pub async fn log_guild_scheduled_event_user_add( #[cfg(not(tarpaulin_include))] pub async fn log_guild_scheduled_event_user_remove( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &GuildScheduledEventUserRemoveEvent, ) -> Result<(), Error> { let event = log_data.clone(); @@ -215,7 +316,7 @@ pub async fn log_guild_scheduled_event_user_remove( #[cfg(not(tarpaulin_include))] pub async fn log_guild_scheduled_event_update( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &ScheduledEvent, ) -> Result<(), Error> { let event = log_data.clone(); @@ -246,35 +347,26 @@ pub async fn log_guild_scheduled_event_update( .map(|_| ()) } -type RwGuildSettingsMap = RwLock>; - /// Logs a guild create event. #[cfg(not(tarpaulin_include))] pub async fn log_guild_create( channel_id: ChannelId, - http: &Arc, - log_data: &(&Guild, &Option, &Arc), + http: &impl CacheHttp, + log_data: &(&Guild, &Option), ) -> Result<(), Error> { - let &(guild, is_new, guild_settings_map) = log_data; - let guild_id = guild.id; + let &(guild, is_new) = log_data; let guild_name = crate::http_utils::get_guild_name(http, channel_id).await?; - // make sure we have the guild stored or store it - let _guild_settings = { - let map = guild_settings_map.read().unwrap().clone(); - let opt = map.get(&guild_id).or(None); - if let Some(guild_setting) = opt { - guild_setting.clone() - } else { - let new_settings = - GuildSettings::new(guild_id, Some(DEFAULT_PREFIX), Some(guild_name.clone())); - guild_settings_map - .write() - .unwrap() - .insert(guild_id, new_settings.clone()); - new_settings.clone() - } - }; + // FIXME! + // // make sure we have the guild stored or store it + // if guild_settings_map.read().await.get(&guild_id).is_none() { + // let new_settings = + // GuildSettings::new(guild_id, Some(DEFAULT_PREFIX), Some(guild_name.clone())); + // guild_settings_map + // .write() + // .await + // .insert(guild_id, new_settings.clone()); + // } let title = format!("Guild Create: {}", guild.name); let is_new_str = if !is_new.is_some() || !is_new.unwrap() { @@ -302,7 +394,7 @@ pub async fn log_guild_create( #[cfg(not(tarpaulin_include))] pub async fn log_guild_role_create( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &serenity::model::prelude::Role, ) -> Result<(), Error> { let guild_name = crate::http_utils::get_guild_name(http, channel_id).await?; @@ -326,7 +418,7 @@ pub async fn log_guild_role_create( #[cfg(not(tarpaulin_include))] pub async fn log_guild_role_delete( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &(&GuildId, &RoleId, &Option), ) -> Result<(), Error> { let (&_guild_id, &role_id, role) = log_data; @@ -353,7 +445,7 @@ pub async fn log_guild_role_delete( #[cfg(not(tarpaulin_include))] pub async fn log_automod_rule_update( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &(String, Rule), ) -> Result<(), Error> { let (_event_name, log_data) = log_data.clone(); @@ -384,7 +476,7 @@ pub async fn log_automod_rule_update( #[cfg(not(tarpaulin_include))] pub async fn log_guild_scheduled_event_create( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &ScheduledEvent, ) -> Result<(), Error> { let event = log_data.clone(); @@ -420,7 +512,7 @@ use serenity::model::guild::automod::Rule; #[cfg(not(tarpaulin_include))] pub async fn log_automod_rule_create( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &Rule, ) -> Result<(), Error> { let title = format!("Automod Rule Create: {}", log_data.creator_id); @@ -450,7 +542,7 @@ pub async fn log_automod_rule_create( #[cfg(not(tarpaulin_include))] pub async fn log_automod_command_execution( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &ActionExecution, ) -> Result<(), Error> { let title = format!("Automod Action Executed: {}", log_data.rule_id); @@ -480,7 +572,7 @@ pub async fn log_automod_command_execution( #[cfg(not(tarpaulin_include))] pub async fn log_command_permissions_update( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &CommandPermissions, ) -> Result<(), Error> { let permissions = log_data; @@ -505,7 +597,7 @@ pub async fn log_command_permissions_update( pub async fn log_channel_delete( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &(&GuildChannel, &Option>), ) -> Result<(), Error> { let &(guild_channel, messages) = log_data; @@ -532,7 +624,7 @@ pub async fn log_channel_delete( pub async fn log_message_delete( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &(&ChannelId, &MessageId, &Option), ) -> Result<(), Error> { let &(del_channel_id, message_id, guild_id) = log_data; @@ -560,7 +652,7 @@ pub async fn log_message_delete( pub async fn log_user_update( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &(&Option, &CurrentUser), ) -> Result { let &(old, new) = log_data; @@ -601,7 +693,7 @@ pub async fn log_user_update( pub async fn log_reaction_remove( channel_id_first: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &serenity::model::prelude::Reaction, ) -> Result<(), Error> { let reaction = log_data; @@ -630,7 +722,7 @@ pub async fn log_reaction_remove( pub async fn log_reaction_add( channel_id_first: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &serenity::model::prelude::Reaction, ) -> Result<(), Error> { let reaction = log_data; @@ -660,7 +752,7 @@ pub async fn log_reaction_add( /// Log a message update event. pub async fn log_message_update( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &( &Option, &Option, @@ -740,7 +832,7 @@ pub fn default_msg_string(msg: &MessageUpdateEvent) -> (String, String, String, /// Log a guild ban. pub async fn log_guild_ban_addition( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &(&str, &GuildId, &serenity::model::prelude::User), ) -> Result<(), Error> { let &(_event_name, _guild_id, user) = log_data; @@ -766,7 +858,7 @@ pub async fn log_guild_ban_addition( /// Log a guild ban removal pub async fn log_guild_ban_removal( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &(&str, &GuildId, &serenity::model::prelude::User), ) -> Result<(), Error> { let &(_event, _guild_id, user) = log_data; @@ -839,7 +931,7 @@ pub fn guild_role_diff( /// Log a guild role update event. pub async fn log_guild_role_update( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &( &Option, &serenity::model::prelude::Role, @@ -869,7 +961,7 @@ pub async fn log_guild_role_update( /// Log a guild role creation event. pub async fn log_guild_member_removal( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, log_data: &(&GuildId, &serenity::model::prelude::User, &Option), ) -> Result { let &(_guild_id, user, member_data_if_available) = log_data; @@ -896,7 +988,7 @@ pub async fn log_guild_member_removal( /// Log a guild member addition event. pub async fn log_guild_member_addition( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, new_member: &Member, ) -> Result { let avatar_url = new_member.user.avatar_url().unwrap_or_default(); @@ -1049,7 +1141,7 @@ impl std::fmt::Display for ClientStatusPrinter { #[cfg(not(tarpaulin_include))] pub async fn log_presence_update( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, new_data: &Presence, ) -> Result { let presence_str = PresencePrinter { @@ -1107,11 +1199,11 @@ pub async fn log_voice_state_update( .clone() .and_then(|x| x.user.avatar_url()) .unwrap_or_default(); - let guild_name = get_guild_name(&ctx.http, channel_id).await?; + let guild_name = get_guild_name(&ctx, channel_id).await?; send_log_embed_thumb( &guild_name, &channel_id, - &ctx.http, + &ctx, &new.user_id.to_string(), &title, &description, @@ -1123,7 +1215,7 @@ pub async fn log_voice_state_update( /// Noop log a typing start event. pub async fn log_typing_start_noop( _channel_id: ChannelId, - _http: &Arc, + _http: &impl CacheHttp, _event: &serenity::model::prelude::TypingStartEvent, ) -> Result { Ok(serenity::model::prelude::Message::default()) @@ -1132,21 +1224,16 @@ pub async fn log_typing_start_noop( /// Log a typing start event. pub async fn log_typing_start( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, event: &serenity::model::prelude::TypingStartEvent, ) -> Result { - let user = event.user_id.to_user(http.clone()).await?; + let user = event.user_id.to_user(http).await?; + let channel_name = channel_id.to_channel(http).await?.to_string(); let name = user.name.clone(); - let channel_name = http - .get_channel(channel_id) - .await - .ok() - .map(|x| x.to_string()) - .unwrap_or_default(); let guild = event .guild_id .unwrap_or_default() - .to_partial_guild(http.clone()) + .to_partial_guild(http) .await? .name; tracing::info!( @@ -1178,7 +1265,7 @@ pub async fn log_typing_start( pub async fn log_message( channel_id: ChannelId, - http: &Arc, + http: &impl CacheHttp, new_message: &serenity::model::prelude::Message, ) -> Result { let guild_name = get_guild_name(http, channel_id).await?; @@ -1204,7 +1291,9 @@ pub async fn log_message( #[macro_export] macro_rules! log_event { ($log_func:expr, $guild_settings:expr, $event:expr, $log_data:expr, $guild_id:expr, $http:expr, $event_log:expr, $event_name:expr) => {{ - $event_log.write_log_obj($event_name, $log_data)?; + $event_log + .write_log_obj_async($event_name, $log_data) + .await?; let channel_id = get_channel_id($guild_settings, $guild_id, $event).await?; $log_func(channel_id, $http, $log_data).await.map(|_| ()) }}; @@ -1213,8 +1302,21 @@ macro_rules! log_event { #[macro_export] macro_rules! log_event2 { ($log_func:expr, $guild_settings:expr, $event:expr, $log_data:expr, $guild_id:expr, $ctx:expr, $event_log:expr, $event_name:expr) => {{ - $event_log.write_log_obj($event_name, $log_data)?; + $event_log + .write_log_obj_async($event_name, $log_data) + .await?; let channel_id = get_channel_id($guild_settings, $guild_id, $event).await?; $log_func(channel_id, $ctx, $log_data).await.map(|_| ()) }}; } + +#[macro_export] +macro_rules! log_event_async { + ($log_func:expr, $guild_settings:expr, $event:expr, $log_data:expr, $guild_id:expr, $http:expr, $event_log:expr, $event_name:expr) => {{ + $event_log + .write_log_obj_async($event_name, $log_data) + .await?; + let channel_id = get_channel_id($guild_settings, $guild_id, $event).await?; + $log_func(channel_id, $http, $log_data).await.map(|_| ()) + }}; +} diff --git a/crack-core/src/handlers/mod.rs b/crack-core/src/handlers/mod.rs index 9b0f5a8a4..e9834ab52 100644 --- a/crack-core/src/handlers/mod.rs +++ b/crack-core/src/handlers/mod.rs @@ -4,6 +4,7 @@ pub mod idle; pub mod serenity; pub mod track_end; pub mod voice; +pub mod voice_chat_stats; pub use self::event_log::handle_event; pub use self::idle::IdleHandler; diff --git a/crack-core/src/handlers/serenity.rs b/crack-core/src/handlers/serenity.rs index 2e50b69cd..fe6145e21 100644 --- a/crack-core/src/handlers/serenity.rs +++ b/crack-core/src/handlers/serenity.rs @@ -2,8 +2,9 @@ use crate::{ db::GuildEntity, errors::CrackedError, guild::settings::{GuildSettings, GuildSettingsMap}, + handlers::voice_chat_stats::cam_status_loop, sources::spotify::{Spotify, SPOTIFY}, - BotConfig, CamKickConfig, Data, + BotConfig, Data, }; use ::serenity::{ all::Message, @@ -12,41 +13,24 @@ use ::serenity::{ }; use chrono::{DateTime, Utc}; use colored::Colorize; -use poise::serenity_prelude::{ - self as serenity, Channel, Error as SerenityError, Member, Mentionable, UserId, -}; +use poise::serenity_prelude::{self as serenity, Error as SerenityError, Member, Mentionable}; use serenity::{ async_trait, model::{gateway::Ready, id::GuildId, prelude::VoiceState}, ChannelId, {Context as SerenityContext, EventHandler}, }; use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, sync::{atomic::Ordering, Arc, Mutex}, time::SystemTime, }; -use tokio::time::{Duration, Instant}; +use tokio::time::Duration; pub struct SerenityHandler { pub data: Data, pub is_loop_running: std::sync::atomic::AtomicBool, } -#[derive(Copy, Clone, Debug)] -pub struct MyVoiceUserInfo { - pub user_id: UserId, - pub guild_id: GuildId, - pub channel_id: ChannelId, - pub camera_status: bool, - pub time_last_cam_change: Instant, -} - -impl MyVoiceUserInfo { - pub fn key(&self) -> (UserId, ChannelId) { - (self.user_id, self.channel_id) - } -} - #[async_trait] impl EventHandler for SerenityHandler { async fn ready(&self, ctx: SerenityContext, ready: Ready) { @@ -54,7 +38,7 @@ impl EventHandler for SerenityHandler { ctx.set_activity(Some(ActivityData::listening(format!( "{}play", - __self.data.bot_settings.get_prefix() + self.data.bot_settings.get_prefix() )))); // attempts to authenticate to spotify @@ -93,7 +77,7 @@ impl EventHandler for SerenityHandler { new_member.to_string().white() ); let guild_id = new_member.guild_id; - let guild_settings_map = self.data.guild_settings_map.read().unwrap().clone(); + let guild_settings_map = self.data.guild_settings_map.read().await.clone(); let guild_settings = guild_settings_map.get(&guild_id); // let guild_settings = guild_settings_map.get_mut(&guild_id); // guild_settings.cloned() @@ -118,7 +102,7 @@ impl EventHandler for SerenityHandler { let channel = serenity::ChannelId::new(channel); let x = channel .send_message( - &ctx.http, + &ctx, CreateMessage::default().content({ if message.contains("{user}") { message.replace( @@ -139,7 +123,7 @@ impl EventHandler for SerenityHandler { if let Some(role_id) = welcome.auto_role { tracing::info!("{}{}", "role_id: ".white(), role_id.to_string().white()); let role_id = serenity::RoleId::new(role_id); - match new_member.add_role(&ctx.http, role_id).await { + match new_member.add_role(&ctx, role_id).await { Ok(_) => { tracing::info!("{}{}", "role added: ".white(), role_id.to_string().white()); }, @@ -229,7 +213,7 @@ impl EventHandler for SerenityHandler { .data .guild_settings_map .read() - .unwrap() + .await .get(&new.guild_id.unwrap()) .map(|x| x.self_deafen) .unwrap_or_else(|| true); @@ -260,7 +244,7 @@ impl EventHandler for SerenityHandler { manager.remove(guild_id).await.ok(); } - // update_queue_messages(&ctx.http, &self.data, &[], guild_id).await; + // update_queue_messages(&ctx, &self.data, &[], guild_id).await; } // We use the cache_ready event just in case some cache operation is required in whatever use @@ -268,8 +252,11 @@ impl EventHandler for SerenityHandler { async fn cache_ready(&self, ctx: SerenityContext, guilds: Vec) { tracing::info!("Cache built successfully! {} guilds cached", guilds.len()); - for guildid in guilds.iter() { - tracing::info!("Guild: {:?}", guildid); + for guild_id in guilds.iter() { + match guild_id.name(ctx.clone()) { + Some(name) => tracing::info!("Guild: {name}"), + None => tracing::info!("Guild: {guild_id}"), + } } let config = self.data.bot_settings.clone(); @@ -290,7 +277,7 @@ impl EventHandler for SerenityHandler { let ctx1 = arc_ctx.clone(); let lock = ctx1.data.read().await; let guild_settings_map = lock.get::().unwrap(); - let mut data_write = self.data.guild_settings_map.write().unwrap(); + let mut data_write = self.data.guild_settings_map.write().await; let mut x = 0; for (key, value) in guild_settings_map.clone().iter() { @@ -353,6 +340,10 @@ impl EventHandler for SerenityHandler { cam_status_loop(ctx3.clone(), config3.clone(), guilds.clone()).await; }; + //let pool = self.data.database_pool.clone().unwrap(); + //let tx = setup_workers(pool).await; + //self.data.set_db_channel(tx); + // Now that the loop is running, we set the bool to true self.is_loop_running.swap(true, Ordering::Relaxed); } @@ -422,17 +413,18 @@ impl SerenityHandler { self.data .guild_settings_map .write() - .unwrap() + .await .insert(guild_id, default.clone()); - default - .save(&pool) - .await - .expect("Error saving guild settings"); - tracing::info!("saving guild {}...", default); + match default.save(&pool).await { + Ok(()) => tracing::info!("Saved guild {guild_name}..."), + Err(err) => tracing::error!("Failed to save guild {guild_name} due to {err}"), + } } } + /// Loads the stored guild settings from the DB. This is a major and important + /// function that allows the bot to persist settings across restarts. async fn load_guilds_settings_cache_ready( &self, ctx: &SerenityContext, @@ -473,7 +465,7 @@ impl SerenityHandler { .await .unwrap(); // .map_err(Into::into)?; - let mut guild_settings_map = self.data.guild_settings_map.write().unwrap(); + let mut guild_settings_map = self.data.guild_settings_map.write().await; // let _ = default..map_err(|err| { // tracing::error!("Failed to load guild {} settings due to {}", guild_id, err); @@ -488,7 +480,7 @@ impl SerenityHandler { match guild_settings_opt { Some(&mut ref guild_settings) => { - tracing::info!("loaded guild from db {}...", guild_settings); + tracing::trace!("loaded guild from db {}...", guild_settings); guild_settings_list.push(guild_settings.clone()); }, None => { @@ -517,7 +509,7 @@ impl SerenityHandler { if user.id == new.user_id && !new.deaf { guild .unwrap() - .edit_member(&ctx.http, new.user_id, EditMember::default().deafen(true)) + .edit_member(&ctx, new.user_id, EditMember::default().deafen(true)) .await .unwrap(); } @@ -562,68 +554,6 @@ async fn log_system_load(ctx: Arc, config: Arc) { } } -async fn check_camera_status(ctx: Arc, guild_id: GuildId) -> Vec { - let (voice_states, guild_name) = match guild_id.to_guild_cached(&ctx.cache) { - Some(guild) => (guild.voice_states.clone(), guild.name.clone()), - None => { - tracing::error!("Guild not found in cache"); - return vec![]; - }, - }; - - // let voice_states = &guild.voice_states; - let mut cams = vec![]; - let mut output: String = format!("{}\n", guild_name.bright_green()); - - for (user_id, voice_state) in voice_states { - if let Some(channel_id) = voice_state.channel_id { - let user = match user_id.to_user(&ctx).await { - Ok(user) => user, - Err(err) => { - tracing::error!("Error getting user: {}", err); - continue; - }, - }; - let channel_name = match channel_id.to_channel(&ctx).await { - Ok(channel) => match channel { - Channel::Guild(channel) => channel.name, - Channel::Private(channel) => channel.name(), - _ => String::from("unknown"), - }, - Err(err) => { - tracing::error!( - "Error getting channel name for channel {} in guild {}: {}", - channel_id, - guild_name, - err - ); - "MISSING_ACCESS".to_string() - }, - }; - - let info = MyVoiceUserInfo { - user_id, - guild_id, - channel_id, - camera_status: voice_state.self_video, - time_last_cam_change: Instant::now(), - }; - - cams.push(info); - output.push_str(&format!( - "{}|{}|{}|{}|{}\n", - &user.name, - &user.id, - &channel_name, - &channel_id, - if info.camera_status { "on" } else { "off" }, - )); - } - } - tracing::warn!("{}", output.bright_cyan()); - cams -} - /// Checks the guilds' message cache for messages that are older than the timeout interval. #[allow(dead_code)] async fn check_delete_old_messages( @@ -666,190 +596,36 @@ async fn check_delete_old_messages( } Ok(()) } - -async fn cam_status_loop(ctx: Arc, config: Arc, guilds: Vec) { - tokio::spawn(async move { - tracing::trace!( - target = "cam_status_loop", - "Starting camera status check loop" - ); - let cam_kick = config.cam_kick.clone().unwrap_or_default(); - let conf_guilds = cam_kick.iter().map(|x| x.guild_id).collect::>(); - let mut cam_status: HashMap<(UserId, ChannelId), MyVoiceUserInfo> = - HashMap::<(UserId, ChannelId), MyVoiceUserInfo>::new(); - let channels: HashMap = cam_kick - .iter() - .map(|x| (x.channel_id, x)) - .collect::>(); - conf_guilds - .iter() - .for_each(|x| tracing::error!("Guild: {}", x)); - tracing::warn!("conf_guilds: {}", format!("{:?}", conf_guilds).green()); - loop { - // We clone Context again here, because Arc is owned, so it moves to the - // new function. - // let new_cam_status = Arc::new(HashMap::::new()); - tracing::error!("Checking camera status for {} guilds", guilds.len()); - // Go through all the guilds we have cached and check the camera status - // for all the users we can see in voice channels. - let mut cams = vec![]; - for guild_id in &guilds { - cams.extend(check_camera_status(Arc::clone(&ctx), *guild_id).await); - } - tracing::trace!("num cams {}", cams.len()); - let mut new_cams = vec![]; - - for cam in cams.iter() { - if let Some(status) = cam_status.get(&cam.key()) { - if let Some(kick_conf) = channels.get(&status.channel_id.get()) { - tracing::warn!("kick_conf: {}", format!("{:?}", kick_conf).blue()); - if status.camera_status != cam.camera_status { - tracing::info!( - "Camera status changed for user {} to {}", - status.user_id, - cam.camera_status - ); - cam_status.insert(cam.key(), *cam); - } else { - tracing::info!( - target = "Camera", - "cur: {}, prev: {}", - status.camera_status, - cam.camera_status - ); - tracing::info!( - target = "Camera", - "elapsed: {:?}, timeout: {}", - status.time_last_cam_change.elapsed(), - kick_conf.cammed_down_timeout - ); - if !status.camera_status - && status.time_last_cam_change.elapsed() - > Duration::from_secs(kick_conf.cammed_down_timeout) - { - let user = cam.user_id.to_user(&ctx.http).await.unwrap(); - tracing::warn!( - "User {} has been cammed down for {} seconds", - user.name, - status.time_last_cam_change.elapsed().as_secs() - ); - - // let guild = cam.guild_id.to_guild_cached(&ctx.cache).unwrap(); - let guild_id = cam.guild_id; - tracing::error!("about to disconnect {:?}", cam.user_id); - - // WARN: Disconnect the user - // FIXME: Should this not be it's own function? - // let dc_res = disconnect_member(ctx.clone(), *cam, guild).await; - let dc_res1 = ( - server_defeafen_member(ctx.clone(), *cam, guild_id).await, - "deafen", - ); - let dc_res2 = ( - server_mute_member(ctx.clone(), *cam, guild_id).await, - "mute", - ); - - for (dc_res, state) in vec![dc_res1, dc_res2] { - match dc_res { - Ok(_) => { - tracing::error!( - "User {} has been violated: {}", - user.name, - state - ); - if state == "deafen" && kick_conf.send_msg_deafen - || state == "mute" && kick_conf.send_msg_mute - || state == "disconnect" && kick_conf.send_msg_dc - { - let channel = ChannelId::new(kick_conf.channel_id); - let _ = channel - .send_message( - &ctx.http, - CreateMessage::default().content({ - format!( - "{} {}: {}", - user.mention(), - kick_conf.dc_message, - state - ) - }), - ) - .await; - // cam_status.remove(&cam.key()); - } - cam_status.remove(&cam.key()); - // if state == "disconnect" { - // cam_status.remove(&cam.key()); - // } - }, - Err(err) => { - tracing::error!("Error violating user: {}", err); - }, - } - } - } - } - } - } else { - new_cams.push(cam); - } - } - let res: i32 = new_cams - .iter() - .map(|x| { - if cam_status.insert(x.key(), **x).is_some() { - 0 - } else { - 1 - } - }) - .sum(); - - tracing::warn!("num new cams: {}", res); - tracing::warn!( - "Sleeping for {} seconds", - config.get_video_status_poll_interval() - ); - tokio::time::sleep(Duration::from_secs(config.get_video_status_poll_interval())).await; - } - }); -} - -#[allow(dead_code)] -async fn disconnect_member( - ctx: Arc, - cam: MyVoiceUserInfo, - guild: GuildId, -) -> Result { - guild - .edit_member( - &ctx.http, - cam.user_id, - EditMember::default().disconnect_member(), - ) - .await -} - -async fn server_defeafen_member( - ctx: Arc, - cam: MyVoiceUserInfo, - guild: GuildId, -) -> Result { - guild - .edit_member(&ctx.http, cam.user_id, EditMember::default().deafen(true)) - .await -} - -async fn server_mute_member( - ctx: Arc, - cam: MyVoiceUserInfo, - guild: GuildId, -) -> Result { - guild - .edit_member(&ctx.http, cam.user_id, EditMember::default().mute(true)) - .await -} +// #[allow(dead_code)] +// async fn disconnect_member( +// ctx: Arc, +// cam: CamPollEvent, +// guild: GuildId, +// ) -> Result { +// guild +// .edit_member(&ctx, cam.user_id, EditMember::default().disconnect_member()) +// .await +// } + +// async fn server_defeafen_member( +// ctx: Arc, +// cam: CamPollEvent, +// guild: GuildId, +// ) -> Result { +// guild +// .edit_member(&ctx, cam.user_id, EditMember::default().deafen(true)) +// .await +// } + +// async fn server_mute_member( +// ctx: Arc, +// cam: CamPollEvent, +// guild: GuildId, +// ) -> Result { +// guild +// .edit_member(&ctx, cam.user_id, EditMember::default().mute(true)) +// .await +// } /// Returns a string describing the difference between two voice states. pub async fn voice_state_diff_str( diff --git a/crack-core/src/handlers/track_end.rs b/crack-core/src/handlers/track_end.rs index 6b1bbd4eb..4a3456b6c 100644 --- a/crack-core/src/handlers/track_end.rs +++ b/crack-core/src/handlers/track_end.rs @@ -5,17 +5,20 @@ use ::serenity::{ http::Http, model::id::GuildId, }; +use serenity::all::CacheHttp; use songbird::{input::AuxMetadata, tracks::TrackHandle, Call, Event, EventContext, EventHandler}; use std::{sync::Arc, time::Duration}; use tokio::sync::Mutex; use crate::{ - commands::{doplay_utils::enqueue_track_pgwrite_asdf, forget_skip_votes, MyAuxMetadata}, + commands::{forget_skip_votes, play_utils::enqueue_track_pgwrite_asdf, MyAuxMetadata}, db::PlayLog, errors::{verify, CrackedError}, guild::operations::GuildSettingsOperations, - interface::{build_nav_btns, create_queue_embed}, - messaging::messages::SPOTIFY_AUTH_FAILED, + messaging::{ + interface::{create_nav_btns, create_queue_embed}, + messages::SPOTIFY_AUTH_FAILED, + }, sources::spotify::{Spotify, SPOTIFY}, utils::{calculate_num_pages, forget_queue_message, send_now_playing}, Data, Error, @@ -29,6 +32,7 @@ pub struct TrackEndHandler { pub data: Data, pub cache: Arc, pub http: Arc, + // pub cache_http: impl CacheHttp, pub call: Arc>, } @@ -36,7 +40,7 @@ pub struct ModifyQueueHandler { pub guild_id: GuildId, pub data: Data, pub http: Arc, - pub _cache: Arc, + pub cache: Arc, pub call: Arc>, } @@ -45,19 +49,12 @@ pub struct ModifyQueueHandler { impl EventHandler for TrackEndHandler { async fn act(&self, _ctx: &EventContext<'_>) -> Option { tracing::error!("TrackEndHandler"); - let autoplay = { - self.data - .guild_cache_map - .lock() - .unwrap() - .entry(self.guild_id) - .or_default() - .autoplay - }; + let autoplay = self.data.get_autoplay(self.guild_id).await; + tracing::error!("Autoplay: {}", autoplay); let (autopause, volume) = { - let settings = self.data.guild_settings_map.read().unwrap().clone(); + let settings = self.data.guild_settings_map.read().await.clone(); let autopause = settings .get(&self.guild_id) .map(|guild_settings| guild_settings.autopause) @@ -92,7 +89,7 @@ impl EventHandler for TrackEndHandler { Err(e) => tracing::warn!("Error forgetting skip votes: {}", e), }; - let music_channel = self.data.get_music_channel(self.guild_id); + let music_channel = self.data.get_music_channel(self.guild_id).await; let (chan_id, _chan_name, MyAuxMetadata::Data(metadata), cur_position) = { let (sb_chan_id, my_metadata, cur_pos) = { @@ -162,12 +159,13 @@ impl EventHandler for TrackEndHandler { return None; }, }; + let cache_http = (&self.cache, self.http.as_ref()); let tracks = enqueue_track_pgwrite_asdf( self.data.database_pool.as_ref().unwrap(), self.guild_id, chan_id, UserId::new(1), - &self.cache, + cache_http, &self.call, &query, ) @@ -220,7 +218,7 @@ impl EventHandler for TrackEndHandler { { Ok(message) => { self.data.add_msg_to_cache(self.guild_id, message); - tracing::warn!("Sent now playing message"); + tracing::info!("Sent now playing message"); }, Err(e) => tracing::warn!("Error sending now playing message: {}", e), } @@ -246,18 +244,18 @@ async fn extract_track_metadata(track: &TrackHandle) -> Result<(MyAuxMetadata, D #[async_trait] impl EventHandler for ModifyQueueHandler { async fn act(&self, _ctx: &EventContext<'_>) -> Option { - let (queue, vol) = { + let queue = { let handler = self.call.lock().await; - let queue = handler.queue().current_queue().clone(); - let settings = self.data.guild_settings_map.read().unwrap().clone(); - let vol = settings - .get(&self.guild_id) - .map(|guild_settings| guild_settings.volume); - (queue, vol) + handler.queue().current_queue() + }; + let vol = { + let guild_settings = self.data.get_guild_settings(self.guild_id).await; + guild_settings.map(|x| x.volume) }; vol.map(|vol| queue.first().map(|track| track.set_volume(vol).unwrap())); - update_queue_messages(&self.http, &self.data, &queue, self.guild_id).await; + let cache_http = (&self.cache, self.http.as_ref()); + update_queue_messages(&cache_http, &self.data, &queue, self.guild_id).await; None } @@ -266,12 +264,12 @@ impl EventHandler for ModifyQueueHandler { /// This function goes through all the active "queue" messages that are still /// being updated and updates them with the current. pub async fn update_queue_messages( - http: &Http, + cache_http: &impl CacheHttp, data: &Data, tracks: &[TrackHandle], guild_id: GuildId, ) { - let cache_map = data.guild_cache_map.lock().unwrap().clone(); + let cache_map = data.guild_cache_map.lock().await; let mut messages = match cache_map.get(&guild_id) { Some(cache) => cache.queue_messages.clone(), @@ -281,18 +279,18 @@ pub async fn update_queue_messages( for (message, page_lock) in messages.iter_mut() { // has the page size shrunk? let num_pages = calculate_num_pages(tracks); - let page = *page_lock.read().unwrap(); + let page = *page_lock.read().await; let page_val = usize::min(page, num_pages - 1); - *page_lock.write().unwrap() = page_val; + *page_lock.write().await = page_val; let embed = create_queue_embed(tracks, page_val).await; let edit_message = message .edit( - &http, + cache_http, EditMessage::new() .embed(embed) - .components(build_nav_btns(page_val, num_pages)), + .components(create_nav_btns(page_val, num_pages)), ) .await; diff --git a/crack-core/src/handlers/voice.rs b/crack-core/src/handlers/voice.rs index 6c9e7eece..26e282170 100644 --- a/crack-core/src/handlers/voice.rs +++ b/crack-core/src/handlers/voice.rs @@ -1,22 +1,14 @@ -use std::sync::Arc; -// use std::{mem, slice}; - -use songbird::{Call, CoreEvent}; -use tokio::fs::File; -use tokio::io::AsyncWriteExt as TokAsyncWriteExt; // for write_all() - +use serenity::all::{Cache, CacheHttp, Http}; use serenity::async_trait; -use serenity::prelude::RwLock; - use serenity::client::EventHandler; - +use serenity::prelude::RwLock; +use serenity::{client::Context as SerenityContext, model::gateway::Ready}; use songbird::{ model::payload::{ClientDisconnect, Speaking}, Event, EventContext, EventHandler as VoiceEventHandler, }; - -use serenity::{client::Context as SerenityContext, model::gateway::Ready}; -use std::time::{SystemTime, UNIX_EPOCH}; +use songbird::{Call, CoreEvent, TrackEvent}; +use std::{mem, slice, sync::Arc}; use crate::errors::CrackedError; @@ -39,59 +31,47 @@ impl EventHandler for Handler { // type Value = Vec; // } +// 10MB (10s not powers of 2 since it gets stored on disk) +const DEFAULT_BUFFER_SIZE: usize = 100_000_000; + pub struct Receiver { pub data: Arc>>, + pub cache: Option>, + pub http: Arc, + buf_size: usize, } impl Receiver { - pub fn new(arc: Arc>>) -> Self { - // Copy of the global audio buffer with RWLock - Self { data: arc } + pub fn new(data: Arc>>, ctx: Option) -> Self { + Self { + data, + cache: ctx.clone().map(|x| x.cache().cloned()).unwrap_or_default(), + http: ctx + .map(|x| x.http.clone()) + .unwrap_or(Arc::new(Http::new(""))), + buf_size: DEFAULT_BUFFER_SIZE, + } } - // FIXME - #[allow(dead_code)] + // Insert a buffer from the audio stream to the handlers internal buffer. + // Clear it every so often to bound the memory usage. async fn insert(&self, buf: &[u8]) { - let insert_lock = { - // While data is a RwLock, it's recommended that you always open the lock as read. - // This is mainly done to avoid Deadlocks for having a possible writer waiting for multiple - // readers to close. - self.data.write().await - //let data_read = self.data.write().await; - - //data_read - // data, instead the reference is cloned. - // We wrap every value on in an Arc, as to keep the data lock open for the least time possible, - // to again, avoid deadlocking it. - // data_read - // .get::() - // .expect("Expected AudioBuffer.") - // .clone() - }; - - // Just like with client.data in main, we want to keep write locks open the least time - // possible, so we wrap them on a block so they get automatically closed at the end. - { - // The HashMap of CommandCounter is wrapped in an RwLock; since we want to write to it, we will - // open the lock in write mode. - // let mut buff = insert_lock.write().await; - let mut buff = insert_lock; //.clone(); - - println!("AudioBuffer size: {}", buff.len()); - // And we write the amount of times the command has been called to it. - if buff.len() > 100000000 { - let mut out_file = File::create(format!( - "file_{:?}.out", - SystemTime::now().duration_since(UNIX_EPOCH).unwrap() - )) - .await - .unwrap(); - out_file.write_all(&buff).await.unwrap(); - buff.clear(); - } - // - buff.extend_from_slice(buf); + let mut lock = self.data.write().await; + + let n = lock.len(); + tracing::trace!("AudioBuffer size: {}", n); + if n > self.buf_size { + // let mut out_file = File::create(format!( + // "file_{:?}.out", + // SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + // )) + // .await + // .unwrap(); + // use tokio::io::AsyncWriteExt as TokAsyncWriteExt; // for write_all() + // out_file.write_all(&buff).await.unwrap(); + lock.clear(); } + lock.extend_from_slice(buf); } } @@ -124,6 +104,10 @@ impl VoiceEventHandler for Receiver { ssrc, speaking, ); + + let user_id = user_id.unwrap().0.to_be_bytes(); + self.data.write().await.extend_from_slice(&user_id); + // You can implement logic here which reacts to a user starting // or stopping speaking, and to map their SSRC to User ID. tracing::warn!( @@ -140,32 +124,24 @@ impl VoiceEventHandler for Receiver { // FIXME: update this to the new library // An event which fires for every received audio packet, // containing the decoded data. - // if let Some(audio) = data.audio { - // // FIXME: Can we not do an unsafe? - // let slice_u8: &[u8] = unsafe { - // slice::from_raw_parts( - // audio.as_ptr() as *const u8, - // audio.len() * mem::size_of::(), - // ) - // }; - // // self.insert(slice_u8); - // self.insert(slice_u8).await; - - // println!( - // "Audio packet's first 5 samples: {:?}", - // audio.get(..5.min(audio.len())) - // ); - // println!( - // "Audio packet sequence {:05} has {:04} bytes (decompressed from {}), SSRC {}", - // data.packet.sequence.0, - // audio.len() * std::mem::size_of::(), - // data.packet.payload.len(), - // data.packet.ssrc, - // ); - // } else { - // println!("RTP packet, but no audio. Driver may not be configured to decode."); - // } - tracing::trace!("RTP packet received: {:?}", data.packet); + let ssrc = data.rtp().get_ssrc(); + let n = data.packet.len(); + let (beg, end) = data.packet.split_at(n - data.payload_end_pad); + // Can we not do an unsafe?... + // Seven months later... We need to do an unsafe because we are not moving + // this memory, but reinterpreting the ptr as a slice of a different size. + // This is I believe analogous to `new_type *new_obj = *(new_type *)(void*)&obj;` + // in C. + let slice_u8: &[u8] = unsafe { + slice::from_raw_parts(beg.as_ptr(), beg.len() * mem::size_of::()) + }; + self.insert(slice_u8).await; + + // println!( + // "Audio packet's first 5 samples: {:?}", + // data.packet.get(..5.min(beg.len())) + // ); + // tracing::trace!("RTP packet received: {:?}", data.packet); }, Ctx::RtcpPacket(data) => { // An event which fires for every received rtcp packet, @@ -178,10 +154,31 @@ impl VoiceEventHandler for Receiver { // You will typically need to map the User ID to their SSRC; observed when // first speaking. - tracing::warn!("Client disconnected: user {:?}", user_id); + let user_name = tracing::warn!("Client disconnected: user {:?}", user_id); }, - _ => { + Ctx::Track(track_data) => { + // An event which fires when a new track starts playing. + if track_data.is_empty() { + return None; + } + tracing::warn!("{:?}", track_data); + for &(track_state, track_handle) in track_data.iter() { + tracing::warn!( + "Track started: {:?} (handle: {:?})", + track_state, + track_handle, + ); + } + }, + Ctx::VoiceTick(_) + | Ctx::DriverConnect(_) + | Ctx::DriverReconnect(_) + | Ctx::DriverDisconnect(_) => { // We won't be registering this struct for any more event classes. + tracing::warn!("Event not handled: {:?}", ctx); + }, + _ => { + // This should not happen. unimplemented!() }, } @@ -190,113 +187,70 @@ impl VoiceEventHandler for Receiver { } } +/// Registers the voice handlers for a call instance for the bot. +/// These are kept per guild. pub async fn register_voice_handlers( buffer: Arc>>, handler_lock: Arc>, + ctx: SerenityContext, ) -> Result<(), CrackedError> { // NOTE: this skips listening for the actual connection result. let mut handler = handler_lock.lock().await; - // .map_err(|e| { - // tracing::error!("Error locking handler: {:?}", e); - // CrackedError::RSpotifyLockError(format!("{e:?}")) - // })?; + // allocating memory, need to drop this when y??? handler.add_global_event( CoreEvent::SpeakingStateUpdate.into(), - Receiver::new(buffer.clone()), + Receiver::new(buffer.clone(), Some(ctx.clone())), ); - // handler.add_global_event( - // CoreEvent::SpeakingStateUpdate.into(), - // Receiver::new(buffer.clone()), - // ); + handler.add_global_event( + TrackEvent::End.into(), + Receiver::new(buffer.clone(), Some(ctx.clone())), + ); - handler.add_global_event(CoreEvent::RtpPacket.into(), Receiver::new(buffer.clone())); + handler.add_global_event( + CoreEvent::RtpPacket.into(), + Receiver::new(buffer.clone(), Some(ctx.clone())), + ); - handler.add_global_event(CoreEvent::RtcpPacket.into(), Receiver::new(buffer.clone())); + handler.add_global_event( + CoreEvent::RtcpPacket.into(), + Receiver::new(buffer.clone(), Some(ctx.clone())), + ); handler.add_global_event( CoreEvent::ClientDisconnect.into(), - Receiver::new(buffer.clone()), + Receiver::new(buffer.clone(), Some(ctx.clone())), ); Ok(()) } -// #[command] -// #[only_in(guilds)] -// async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { -// let connect_to = match args.single::() { -// Ok(id) => ChannelId(id), -// Err(_) => { -// check_msg( -// msg.reply(ctx, "Requires a valid voice channel ID be given") -// .await, -// ); - -// return Ok(()); -// } -// }; - -// let guild = msg.guild(&ctx.cache).unwrap(); -// let guild_id = guild.id; - -// let manager = songbird::get(ctx) -// .await -// .expect("Songbird Voice client placed in at initialisation.") -// .clone(); - -// let (handler_lock, conn_result) = manager.join(guild_id, connect_to).await; - -// { -// // Open the data lock in write mode, so keys can be inserted to it. -// let mut data = ctx.data.write().await; - -// // So, we have to insert the same type to it. -// data.insert::(Arc::new(RwLock::new(Vec::new()))); -// } - -// if let Ok(_) = conn_result { -// // NOTE: this skips listening for the actual connection result. -// let mut handler = handler_lock.lock().await; - -// handler.add_global_event( -// CoreEvent::SpeakingStateUpdate.into(), -// Receiver::new(ctx.data.clone()), -// ); - -// handler.add_global_event( -// CoreEvent::SpeakingUpdate.into(), -// Receiver::new(ctx.data.clone()), -// ); - -// handler.add_global_event( -// CoreEvent::VoicePacket.into(), -// Receiver::new(ctx.data.clone()), -// ); - -// handler.add_global_event( -// CoreEvent::RtcpPacket.into(), -// Receiver::new(ctx.data.clone()), -// ); +#[cfg(test)] +mod test { + use super::*; + use serenity_voice_model::id::UserId as VoiceUserId; + use songbird::model::{payload::Speaking, SpeakingState}; + + #[tokio::test] + async fn test_receiver() { + let buffer = Arc::new(RwLock::new(Vec::new())); + let receiver = Receiver::new(buffer.clone(), None); + let want = VoiceUserId(0xAA); + + let speaking = Speaking { + delay: Some(0), + speaking: SpeakingState::MICROPHONE, + ssrc: 0, + user_id: Some(want), + }; -// handler.add_global_event( -// CoreEvent::ClientDisconnect.into(), -// Receiver::new(ctx.data.clone()), -// ); -// // } + let ctx = EventContext::SpeakingStateUpdate(speaking); + let _ = receiver.act(&ctx).await; + let buf = receiver.data.read().await.clone(); -// check_msg( -// msg.channel_id -// .say(&ctx.http, &format!("Joined {}", connect_to.mention())) -// .await, -// ); -// } else { -// check_msg( -// msg.channel_id -// .say(&ctx.http, "Error joining the channel") -// .await, -// ); -// } + let user_id = u64::from_be_bytes(buf.as_slice().try_into().unwrap()); + let got = VoiceUserId(user_id); -// Ok(()) -// } + assert_eq!(want, got); + } +} diff --git a/crack-core/src/handlers/voice_chat_stats.rs b/crack-core/src/handlers/voice_chat_stats.rs new file mode 100644 index 000000000..99860e086 --- /dev/null +++ b/crack-core/src/handlers/voice_chat_stats.rs @@ -0,0 +1,307 @@ +use crate::{ + commands::{deafen_internal, mute_internal}, + errors::CrackedError, + BotConfig, CamKickConfig, +}; +use ::serenity::builder::CreateMessage; +use colored::Colorize; +use poise::serenity_prelude::{self as serenity, Channel, Mentionable, UserId}; +use serenity::{model::id::GuildId, ChannelId, Context as SerenityContext}; +use std::{ + cmp::{Eq, PartialEq}, + collections::{HashMap, HashSet}, + sync::Arc, +}; +use tokio::time::{Duration, Instant}; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Enum for the Camera status. +enum CamStatus { + On, + Off, +} + +/// Implement Display for the Camera status enum. +impl std::fmt::Display for CamStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CamStatus::On => write!(f, "On"), + CamStatus::Off => write!(f, "Off"), + } + } +} + +/// Implement From bool for the Camera status enum. +impl From for CamStatus { + fn from(status: bool) -> Self { + if status { + CamStatus::On + } else { + CamStatus::Off + } + } +} + +#[derive(Debug, Clone, Copy)] +/// Struct for the our derived Camera change event. +struct CamPollEvent { + user_id: UserId, + guild_id: GuildId, + chan_id: ChannelId, + status: CamStatus, + last_change: Instant, +} + +impl CamPollEvent { + /// Returns the key for the Camera change event. + fn key(&self) -> (UserId, ChannelId) { + (self.user_id, self.chan_id) + } +} + +/// Check the camera status of a user and enforce the rules if necessary. +async fn check_and_enforce_cams( + cur_cam: CamPollEvent, + new_cam: &CamPollEvent, + cam_states: &mut HashMap<(UserId, ChannelId), CamPollEvent>, + config_map: &HashMap, + //status_changes: &mut Vec, + ctx: Arc, +) -> Result<(), CrackedError> { + let kick_conf = config_map + .get(&cur_cam.chan_id.get()) + .ok_or(CrackedError::Other("Channel not found"))?; + tracing::trace!("kick_conf: {}", format!("{:?}", kick_conf).blue()); + if cur_cam.status != new_cam.status { + let cam_event = CamPollEvent { + last_change: Instant::now(), + ..*new_cam + }; + + cam_states.insert(cam_event.key(), cam_event); + } else { + tracing::trace!("cur: {}, prev: {}", cur_cam.status, new_cam.status); + tracing::trace!( + "elapsed: {:?}, timeout: {}", + cur_cam.last_change.elapsed(), + kick_conf.timeout + ); + if cur_cam.status == CamStatus::Off + && cur_cam.last_change.elapsed() > Duration::from_secs(kick_conf.timeout) + { + let user = match new_cam.user_id.to_user(&ctx).await { + Ok(user) => user, + Err(err) => { + tracing::error!("Error getting user: {err}"); + return Err(CrackedError::Other("Error getting user")); + }, + }; + tracing::info!( + "User {} has been cammed down for {} seconds", + user.name, + cur_cam.last_change.elapsed().as_secs() + ); + + // let guild = cam.guild_id.to_guild_cached(&ctx.cache).unwrap(); + let guild_id = new_cam.guild_id; + tracing::info!("about to deafen {:?}", new_cam.user_id); + + if false { + run_cam_enforcement(ctx, new_cam, guild_id, user, kick_conf, cam_states).await; + } + } + }; + Ok(()) +} + +/// Run the camera enforcement rules. +async fn run_cam_enforcement( + ctx: Arc, + new_cam: &CamPollEvent, + guild_id: GuildId, + user: ::serenity::model::prelude::User, + kick_conf: &&CamKickConfig, + cam_states: &mut HashMap<(UserId, ChannelId), CamPollEvent>, +) { + // WARN: Disconnect the user + // FIXME: Should this not be it's own function? + // let dc_res = disconnect_member(ctx.clone(), *cam, guild).await; + let dc_res1 = ( + deafen_internal(ctx.clone(), guild_id, user.clone(), true).await, + "deafen", + ); + let dc_res2 = ( + mute_internal(ctx.clone(), user.clone(), guild_id, true).await, + "deafen", + ); + // let dc_res1 = ( + // server_defeafen_member(ctx.clone(), *new_cam, guild_id).await, + // "deafen", + // ); + // let dc_res2 = ( + // server_mute_member(ctx.clone(), *new_cam, guild_id).await, + // "mute", + // ); + + for (dc_res, state) in vec![dc_res1, dc_res2] { + match dc_res { + Ok(_) => { + tracing::error!("User {} has been violated: {}", user.name, state); + if state == "deafen" && kick_conf.msg_on_deafen + || state == "mute" && kick_conf.msg_on_mute + || state == "disconnect" && kick_conf.msg_on_dc + { + let channel = ChannelId::new(kick_conf.chan_id); + let _ = channel + .send_message( + &ctx, + CreateMessage::default().content({ + format!("{} {}: {}", user.mention(), kick_conf.dc_msg, state) + }), + ) + .await; + } + cam_states.remove(&new_cam.key()); + }, + Err(err) => { + tracing::error!("Error violating user: {}", err); + }, + } + } +} + +/// Check the camera statuses of all the users in voice channels per +/// guild and if there's rules aroun camera usage, enforce them. +async fn check_camera_status( + ctx: Arc, + guild_id: GuildId, +) -> (Vec, String) { + let (voice_states, guild_name) = match guild_id.to_guild_cached(&ctx) { + Some(guild) => (guild.voice_states.clone(), guild.name.clone()), + // Err(err) => { + // tracing::error!("{err}"); + None => { + // let partial_guild = ctx.http().get_guild(guild_id).await.unwrap(); + tracing::error!("Guild not found {guild_id}."); + return (vec![], "".to_string()); + }, + }; + + let mut cams = Vec::new(); + let mut output: String = format!("{}\n", guild_name.bright_green()); + + for (user_id, voice_state) in voice_states { + if let Some(chan_id) = voice_state.channel_id { + let user = match user_id.to_user(&ctx).await { + Ok(user) => user, + Err(err) => { + tracing::error!("Error getting user: {err}"); + continue; + }, + }; + let channel_name = match chan_id.to_channel(&ctx).await { + Ok(chan) => match chan { + Channel::Guild(chan) => chan.name, + Channel::Private(chan) => chan.name(), + _ => String::from("Unknown"), + }, + Err(err) => { + tracing::error!( + r#"Error getting channel name for channel + {chan_id} in guild {guild_name}: {err}"#, + ); + "Missing Access".to_string() + }, + }; + let status = CamStatus::from(voice_state.self_video); + let last_change = Instant::now(); + + let info = CamPollEvent { + user_id, + guild_id, + chan_id, + status, + last_change, + }; + + cams.push(info); + output.push_str(&format!( + "{}|{}|{}|{}|{}|{}\n", + guild_name, &user.name, &user.id, &channel_name, &chan_id, status, + )); + } + } + // tracing::warn!("{}", output.bright_cyan()); + (cams, output) +} + +/// The main loop that checks the camera status of all the users in voice channels +pub async fn cam_status_loop( + ctx: Arc, + config: Arc, + guilds: Vec, +) { + tokio::spawn(async move { + tracing::info!("Starting camera status check loop"); + let configs = config.cam_kick.clone().unwrap_or_default(); + let conf_guilds = configs.iter().map(|x| x.guild_id).collect::>(); + + // This HashMap is used to keep track of the camera status of all the users in voice. + // channels. It gets initialized empty here and then is updated every iteration of the loop. + let mut cur_cams: HashMap<(UserId, ChannelId), CamPollEvent> = + HashMap::<(UserId, ChannelId), CamPollEvent>::new(); + // This is + let channels: HashMap = configs + .iter() + .map(|x| (x.chan_id, x)) + .collect::>(); + + tracing::trace!("conf_guilds: {}", format!("{:?}", conf_guilds).green()); + loop { + // We clone Context again here, because Arc is owned, so it moves to the + // new function. + tracing::error!("Checking camera status for {} guilds", guilds.len()); + // Go through all the guilds we have cached and check the camera status + // for all the users we can see in voice channels. + let mut output = String::new(); + let mut new_cams = vec![]; + for guild_id in &guilds { + let (add_new_cams, add_output) = + check_camera_status(Arc::clone(&ctx), *guild_id).await; + new_cams.extend(add_new_cams); + output.push_str(&add_output); + } + + //let total_active_cams = cams.len(); + let mut new_cams = Vec::<&CamPollEvent>::new(); + //let mut status_changes = Vec::::new(); + + for new_cam in new_cams.iter_mut() { + if let Some(status) = cur_cams.get(&new_cam.key()) { + let _ = check_and_enforce_cams( + *status, + new_cam, + &mut cur_cams, + &channels, + //&mut status_changes, + Arc::clone(&ctx), + ) + .await; + } else { + cur_cams.insert(new_cam.key(), **new_cam); + } + } + let res: i32 = new_cams + .iter() + .map(|x| Into::::into(cur_cams.insert(x.key(), **x).is_none())) + .sum(); + + tracing::warn!("{}", output); + tracing::warn!("num new cams: {}", res); + tracing::warn!( + "Sleeping for {} seconds", + config.get_video_status_poll_interval() + ); + tokio::time::sleep(Duration::from_secs(config.get_video_status_poll_interval())).await; + } + }); +} diff --git a/crack-core/src/http_utils.rs b/crack-core/src/http_utils.rs index ab8489045..e1c5888e6 100644 --- a/crack-core/src/http_utils.rs +++ b/crack-core/src/http_utils.rs @@ -1,40 +1,116 @@ -use crate::errors::CrackedError; -use serenity::all::{ChannelId, GuildId, Http, UserId}; - use once_cell::sync::Lazy; use reqwest::Client; +use std::future::Future; + +use crate::errors::CrackedError; +use crate::messaging::message::CrackedMessage; +use serenity::all::{ + CacheHttp, ChannelId, CreateEmbed, CreateMessage, GuildId, Http, Message, UserId, +}; + +/// Parameter structure for functions that send messages to a channel. +pub struct SendMessageParams { + pub channel: ChannelId, + pub as_embed: bool, + pub ephemeral: bool, + pub reply: bool, + pub msg: CrackedMessage, +} + +/// Extension trait for CacheHttp to add some utility functions. +pub trait CacheHttpExt { + fn cache(&self) -> Option; + fn http(&self) -> Option<&Http>; + fn get_bot_id(&self) -> impl Future> + Send; + fn user_id_to_username_or_default(&self, user_id: UserId) -> String; + fn channel_id_to_guild_name( + &self, + channel_id: ChannelId, + ) -> impl Future> + Send; + fn send_channel_message( + &self, + params: SendMessageParams, + ) -> impl Future> + Send; +} + +/// Implement the CacheHttpExt trait for any type that implements CacheHttp. +impl CacheHttpExt for T { + fn cache(&self) -> Option { + Some(self) + } + + fn http(&self) -> Option<&Http> { + Some(self.http()) + } + + async fn get_bot_id(&self) -> Result { + get_bot_id(self).await + } + + fn user_id_to_username_or_default(&self, user_id: UserId) -> String { + cache_to_username_or_default(self, user_id) + } + + async fn channel_id_to_guild_name( + &self, + channel_id: ChannelId, + ) -> Result { + get_guild_name(self, channel_id).await + } + /// Sends a message to a channel. + #[cfg(not(tarpaulin_include))] + async fn send_channel_message( + &self, + params: SendMessageParams, + ) -> Result { + let channel = params.channel; + let content = format!("{}", params.msg); + let msg = if params.as_embed { + let embed = CreateEmbed::default().description(content); + CreateMessage::new().add_embed(embed) + } else { + CreateMessage::new().content(content) + }; + channel.send_message(self, msg).await.map_err(Into::into) + } +} + +/// This is a hack to get around the fact that we can't use async in statics. Is it? static CLIENT: Lazy = Lazy::new(|| { - println!("Creating a new client..."); // Optional: for demonstration + println!("Creating a new reqwest client..."); + tracing::info!("Creating a new reqwest client..."); reqwest::ClientBuilder::new() .use_rustls_tls() .build() .expect("Failed to build reqwest client") }); +/// Get a reference to the lazy, static, global reqwest client. pub fn get_client() -> &'static Client { &CLIENT } +/// Initialize the static, global reqwest client. pub async fn init_http_client() -> Result<(), CrackedError> { let client = get_client().clone(); let res = client.get("https://httpbin.org/ip").send().await?; tracing::info!("HTTP client initialized successfully: {:?}", res); Ok(()) } - -// /// Get a new reqwest client with consistent settings. -// pub fn new_reqwest_client() -> &'static Client { -// &CLIENT -// } - /// Get the bot's user ID. #[cfg(not(tarpaulin_include))] -pub async fn get_bot_id(http: &Http) -> Result { +pub async fn get_bot_id(cache_http: impl CacheHttp) -> Result { let tune_titan_id = UserId::new(1124707756750934159); let rusty_bot_id = UserId::new(1111844110597374042); let cracktunes_id = UserId::new(1115229568006103122); - let bot_id = http.get_current_user().await?.id; + let bot_id = match cache_http.cache() { + Some(cache) => cache.current_user().id, + None => { + tracing::warn!("cache_http.cache() returned None"); + return Err(CrackedError::Other("cache_http.cache() returned None")); + }, + }; // If the bot is tune titan or rusty bot, return cracktunes ID if bot_id == tune_titan_id || bot_id == rusty_bot_id { @@ -46,24 +122,18 @@ pub async fn get_bot_id(http: &Http) -> Result { /// Get the username of a user from their user ID, returns "Unknown" if an error occurs. #[cfg(not(tarpaulin_include))] -pub fn cache_to_username_or_default(cache: &serenity::all::Cache, user_id: UserId) -> String { - match cache.user(user_id) { - Some(x) => x.name.clone(), - None => { - tracing::warn!("cache.user returned None"); - "Unknown".to_string() +pub fn cache_to_username_or_default(cache_http: impl CacheHttp, user_id: UserId) -> String { + // let asdf = cache.cache()?.user(user_id); + match cache_http.cache() { + Some(cache) => match cache.user(user_id) { + Some(x) => x.name.clone(), + None => { + tracing::warn!("cache.user returned None"); + "Unknown".to_string() + }, }, - } -} - -/// Get the username of a user from their user ID, returns "Unknown" if an error occurs. -#[cfg(not(tarpaulin_include))] -//#[allow(dead_code)] -pub async fn http_to_username_or_default(http: &Http, user_id: UserId) -> String { - match http.get_user(user_id).await { - Ok(x) => x.name, - Err(e) => { - tracing::error!("http.get_user error: {}", e); + None => { + tracing::warn!("cache_http.cache() returned None"); "Unknown".to_string() }, } @@ -85,30 +155,33 @@ pub async fn resolve_final_url(url: &str) -> Result { /// Gets the guild_name for a channel_id. #[cfg(not(tarpaulin_include))] -pub async fn get_guild_name(http: &Http, channel_id: ChannelId) -> Result { +pub async fn get_guild_name( + cache_http: &impl CacheHttp, + channel_id: ChannelId, +) -> Result { channel_id - .to_channel(http) + .to_channel(cache_http) .await? .guild() .map(|x| x.guild_id) .ok_or(CrackedError::NoGuildForChannelId(channel_id))? - .to_partial_guild(http) + .to_partial_guild(cache_http) .await .map(|x| x.name) - .map_err(|e| e.into()) + .map_err(Into::into) } // Get the guild name from the guild id and an http client. #[cfg(not(tarpaulin_include))] -pub async fn get_guild_name_from_guild_id( - http: &Http, +pub async fn guild_name_from_guild_id( + cache_http: impl CacheHttp, guild_id: GuildId, ) -> Result { guild_id - .to_partial_guild(http) + .to_partial_guild(cache_http) .await .map(|x| x.name) - .map_err(|e| e.into()) + .map_err(Into::into) } #[cfg(test)] diff --git a/crack-core/src/lib.rs b/crack-core/src/lib.rs index fbb53d82b..6b6fb0d97 100644 --- a/crack-core/src/lib.rs +++ b/crack-core/src/lib.rs @@ -1,29 +1,32 @@ use crate::handlers::event_log::LogEntry; -use chrono::DateTime; -use chrono::Utc; -use db::PlayLog; -use db::TrackReaction; +use chrono::{DateTime, Utc}; +use commands::play_utils::TrackReadyData; +use commands::MyAuxMetadata; +#[cfg(feature = "crack-gpt")] +use crack_gpt::GptContext; +use db::worker_pool::MetadataMsg; +use db::{PlayLog, TrackReaction}; use errors::CrackedError; use guild::settings::get_log_prefix; -use guild::settings::GuildSettings; -use guild::settings::GuildSettingsMapParam; +use guild::settings::{GuildSettings, GuildSettingsMapParam}; use guild::settings::{ DEFAULT_DB_URL, DEFAULT_LOG_PREFIX, DEFAULT_PREFIX, DEFAULT_VIDEO_STATUS_POLL_INTERVAL, DEFAULT_VOLUME_LEVEL, }; -use poise::serenity_prelude::GuildId; use serde::{Deserialize, Serialize}; -use serenity::all::Message; -use std::fs; -use std::fs::File; -use std::io::Write; -use std::path::Path; -use std::sync::RwLock; +use serenity::all::{ChannelId, GuildId, Message}; +use songbird::Call; use std::{ collections::{BTreeMap, HashMap, HashSet}, fmt::Display, - sync::{Arc, Mutex}, + fs, + fs::File, + future::Future, + io::Write, + path::Path, + sync::{Arc, Mutex as SyncMutex, RwLock as SyncRwLock}, }; +use tokio::sync::{mpsc::Sender, Mutex, RwLock}; pub mod commands; pub mod connection; @@ -32,52 +35,50 @@ pub mod errors; pub mod guild; pub mod handlers; pub mod http_utils; -pub mod interface; pub mod messaging; pub mod metrics; pub mod sources; +#[cfg(test)] +pub mod test; pub mod utils; pub type Error = Box; pub type Context<'a> = poise::Context<'a, Data, Error>; -pub use Result; - -pub trait CrackContext<'a> { - fn add_msg_to_cache(&self, guild_id: GuildId, msg: Message) -> Option; -} - -impl<'a> CrackContext<'a> for Context<'a> { - fn add_msg_to_cache(&self, guild_id: GuildId, msg: Message) -> Option { - self.data().add_msg_to_cache(guild_id, msg) - } -} +pub type ArcTRwLock = Arc>; +pub type ArcTMutex = Arc>; +pub type ArcRwMap = Arc>>; +pub type ArcTRwMap = Arc>>; +pub type ArcMutDMap = Arc>>; +pub type CrackedResult = std::result::Result; /// Checks if we're in a prefix context or not. pub fn is_prefix(ctx: Context) -> bool { matches!(ctx, Context::Prefix(_)) } +/// Struct for the cammed down kicking configuration. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct CamKickConfig { - pub cammed_down_timeout: u64, + pub timeout: u64, pub guild_id: u64, - pub channel_id: u64, - pub dc_message: String, - pub send_msg_deafen: bool, - pub send_msg_mute: bool, - pub send_msg_dc: bool, + pub chan_id: u64, + pub dc_msg: String, + pub msg_on_deafen: bool, + pub msg_on_mute: bool, + pub msg_on_dc: bool, } +/// Default for the CamKickConfig. impl Default for CamKickConfig { fn default() -> Self { Self { - cammed_down_timeout: 0, + timeout: 0, guild_id: 0, - channel_id: 0, - dc_message: "You have been violated for being cammed down for too long.".to_string(), - send_msg_deafen: false, - send_msg_mute: false, - send_msg_dc: false, + chan_id: 0, + dc_msg: "You have been violated for being cammed down for too long.".to_string(), + msg_on_deafen: false, + msg_on_mute: false, + msg_on_dc: false, } } } @@ -86,16 +87,13 @@ impl Default for CamKickConfig { impl Display for CamKickConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut result = String::new(); - result.push_str(&format!( - "cammed_down_timeout: {:?}\n", - self.cammed_down_timeout - )); + result.push_str(&format!("timeout: {:?}\n", self.timeout)); result.push_str(&format!("guild_id: {:?}\n", self.guild_id)); - result.push_str(&format!("channel_id: {:?}\n", self.channel_id)); - result.push_str(&format!("dc_message: {:?}\n", self.dc_message)); - result.push_str(&format!("deafen: {}\n", self.send_msg_deafen)); - result.push_str(&format!("mute: {}\n", self.send_msg_mute)); - result.push_str(&format!("dc: {}\n", self.send_msg_dc)); + result.push_str(&format!("chan_id: {:?}\n", self.chan_id)); + result.push_str(&format!("dc_msg: {:?}\n", self.dc_msg)); + result.push_str(&format!("msg_on_deafen: {}\n", self.msg_on_deafen)); + result.push_str(&format!("msg_on_mute: {}\n", self.msg_on_mute)); + result.push_str(&format!("msg_on_dc: {}\n", self.msg_on_dc)); write!(f, "{}", result) } @@ -288,26 +286,41 @@ impl PhoneCodeData { /// User data, which is stored and accessible in all command invocations #[derive(Serialize, Deserialize, Clone)] pub struct DataInner { - #[serde(skip)] - pub phone_data: PhoneCodeData, pub up_prefix: &'static str, pub bot_settings: BotConfig, - // TODO: Make this a HashMap, pointing to a settings struct containiong + // TODO?: Make this a HashMap, pointing to a settings struct containing // user priviledges, etc pub authorized_users: HashSet, - pub guild_settings_map: Arc>>, - #[serde(skip)] - pub guild_msg_cache_ordered: Arc>>, + // + // Non-serializable below here. What did I even decide to make this Serializable for? + // I doubt it's doing anything, most fields aren't. + // #[serde(skip)] - pub guild_cache_map: Arc>>, + pub phone_data: PhoneCodeData, #[serde(skip)] pub event_log: EventLog, #[serde(skip)] + pub event_log_async: EventLogAsync, + #[serde(skip)] + pub db_channel: Option>, + #[serde(skip)] pub database_pool: Option, #[serde(skip)] pub http_client: reqwest::Client, - // #[serde(skip, default = "default_topgg_client")] - // pub topgg_client: topgg::Client, + // Synchronous settings and caches. These are going away. + pub guild_settings_map_non_async: + Arc>>, + #[serde(skip)] + pub guild_msg_cache_ordered: Arc>>, + + // Async access fields, will switch entirely to these + #[serde(skip)] + pub guild_settings_map: Arc>>, + #[serde(skip)] + pub guild_cache_map: Arc>>, + #[serde(skip)] + #[cfg(feature = "crack-gpt")] + pub gpt_ctx: Arc>>, } // /// Get the default topgg client @@ -333,6 +346,8 @@ impl std::fmt::Debug for DataInner { result.push_str(&format!("guild_cache_map: {:?}\n", self.guild_cache_map)); result.push_str(&format!("event_log: {:?}\n", self.event_log)); result.push_str(&format!("database_pool: {:?}\n", self.database_pool)); + #[cfg(feature = "crack-gpt")] + result.push_str(&format!("gpt_context: {:?}\n", self.gpt_ctx)); result.push_str(&format!("http_client: {:?}\n", self.http_client)); result.push_str("topgg_client: \n"); write!(f, "{}", result) @@ -340,7 +355,7 @@ impl std::fmt::Debug for DataInner { } impl DataInner { - /// Set the bot settings for the data + /// Set the bot settings for the data. pub fn with_bot_settings(&self, bot_settings: BotConfig) -> Self { Self { bot_settings, @@ -348,7 +363,7 @@ impl DataInner { } } - /// Set the database pool for the data + /// Set the database pool for the data. pub fn with_database_pool(&self, database_pool: sqlx::PgPool) -> Self { Self { database_pool: Some(database_pool), @@ -356,7 +371,24 @@ impl DataInner { } } - /// Set the guild settings map for the data + /// Set the channel for the database pool communication. + pub fn with_db_channel(&self, db_channel: Sender) -> Self { + Self { + db_channel: Some(db_channel), + ..self.clone() + } + } + + /// Set the GPT context for the data. + #[cfg(feature = "crack-gpt")] + pub fn with_gpt_ctx(&self, gpt_ctx: GptContext) -> Self { + Self { + gpt_ctx: Arc::new(RwLock::new(Some(gpt_ctx))), + ..self.clone() + } + } + + /// Set the guild settings map for the data. pub fn with_guild_settings_map(&self, guild_settings: GuildSettingsMapParam) -> Self { Self { guild_settings_map: guild_settings, @@ -367,10 +399,10 @@ impl DataInner { /// General log for events that the bot reveices from Discord. #[derive(Clone, Debug)] -pub struct EventLog(pub Arc>); +pub struct EventLog(pub Arc>); impl std::ops::Deref for EventLog { - type Target = Arc>; + type Target = Arc>; fn deref(&self) -> &Self::Target { &self.0 @@ -383,6 +415,42 @@ impl std::ops::DerefMut for EventLog { } } +/// General log for events that the bot reveices from Discord. +#[derive(Clone, Debug)] +pub struct EventLogAsync(pub ArcTMutex); + +impl std::ops::Deref for EventLogAsync { + type Target = ArcTMutex; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for EventLogAsync { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Default for EventLogAsync { + fn default() -> Self { + let log_path = format!("{}/events2.log", get_log_prefix()); + let _ = fs::create_dir_all(Path::new(&log_path).parent().unwrap()); + let log_file = match File::create(log_path) { + Ok(f) => f, + Err(e) => { + eprintln!("Error creating log file: {}", e); + // FIXME: Maybe use io::null()? + // I went down this path with sink and it was a mistake. + File::create("/dev/null") + .expect("Should be able to have a file object to write too.") + }, + }; + Self(Arc::new(tokio::sync::Mutex::new(log_file))) + } +} + impl Default for EventLog { fn default() -> Self { let log_path = format!("{}/events.log", get_log_prefix()); @@ -397,7 +465,63 @@ impl Default for EventLog { .expect("Should be able to have a file object to write too.") }, }; - Self(Arc::new(Mutex::new(log_file))) + Self(Arc::new(SyncMutex::new(log_file))) + } +} + +impl EventLogAsync { + /// Create a new EventLog, calls default + pub fn new() -> Self { + Self::default() + } + + /// Write an object to the log file without a note async. + pub async fn write_log_obj_async( + &self, + name: &str, + obj: &T, + ) -> Result<(), Error> { + self.write_log_obj_note_async(name, None, obj).await + } + + /// Write an object to the log file with a note. + pub async fn write_log_obj_note_async( + &self, + name: &str, + notes: Option<&str>, + obj: &T, + ) -> Result<(), Error> { + let entry = LogEntry { + name: name.to_string(), + notes: notes.unwrap_or("").to_string(), + event: obj, + }; + let mut buf = serde_json::to_vec(&entry).unwrap(); + let _ = buf.write(&[b'\n']); + let buf: &[u8] = buf.as_slice(); + self.lock() + .await + .write_all(buf) + .map_err(|e| CrackedError::IO(e).into()) + } + + /// Write an object to the log file. + pub async fn write_obj(&self, obj: &T) -> Result<(), Error> { + let mut buf = serde_json::to_vec(obj).unwrap(); + let _ = buf.write(&[b'\n']); + let buf: &[u8] = buf.as_slice(); + self.lock() + .await + .write_all(buf) + .map_err(|e| CrackedError::IO(e).into()) + } + + /// Write a buffer to the log file. + pub async fn write(self, buf: &[u8]) -> Result<(), Error> { + self.lock() + .await + .write_all(buf) + .map_err(|e| CrackedError::IO(e).into()) } } @@ -457,17 +581,29 @@ impl EventLog { impl Default for DataInner { fn default() -> Self { // let topgg_token = std::env::var("TOPGG_TOKEN").unwrap_or_default(); + // let runtime = tokio::runtime::Builder::new_multi_thread() + // .worker_threads(4) + // .enable_all() + // .build() + // .unwrap(); + // let rt_handle = Arc::new(RwLock::new(Some(runtime.handle().clone()))); Self { + // rt_handle, phone_data: PhoneCodeData::default(), //PhoneCodeData::load().unwrap(), up_prefix: "R", bot_settings: Default::default(), authorized_users: Default::default(), guild_settings_map: Arc::new(RwLock::new(HashMap::new())), guild_cache_map: Arc::new(Mutex::new(HashMap::new())), - guild_msg_cache_ordered: Arc::new(Mutex::new(BTreeMap::new())), + guild_settings_map_non_async: Arc::new(SyncRwLock::new(HashMap::new())), + guild_msg_cache_ordered: Arc::new(SyncMutex::new(BTreeMap::new())), event_log: EventLog::default(), + event_log_async: EventLogAsync::default(), database_pool: None, http_client: http_utils::get_client().clone(), + db_channel: None, + #[cfg(feature = "crack-gpt")] + gpt_ctx: Arc::new(RwLock::new(None)), // topgg_client: topgg::Client::new(topgg_token), } } @@ -491,6 +627,19 @@ impl std::ops::Deref for Data { } impl Data { + /// Insert a guild into the guild settings map. + pub async fn insert_guild( + &self, + guild_id: GuildId, + guild_settings: GuildSettings, + ) -> Result { + self.guild_settings_map + .write() + .await + .insert(guild_id, guild_settings) + .ok_or(CrackedError::FailedToInsert) + } + /// Create a new Data, calls default pub async fn downvote_track( &self, @@ -530,7 +679,11 @@ impl Data { } /// Remove and return a message from the cache based on the guild_id and timestamp. - pub fn remove_msg_from_cache(&self, guild_id: GuildId, ts: DateTime) -> Option { + pub async fn remove_msg_from_cache( + &self, + guild_id: GuildId, + ts: DateTime, + ) -> Option { let mut guild_msg_cache_ordered = self.guild_msg_cache_ordered.lock().unwrap(); guild_msg_cache_ordered .get_mut(&guild_id) @@ -539,42 +692,92 @@ impl Data { .remove(&ts) } - /// Get the guild settings for a guild (read only) - pub fn get_guild_settings(&self, guild_id: GuildId) -> Option { - self.guild_settings_map - .read() - .unwrap() - .get(&guild_id) - .cloned() - } - - pub fn add_guild_settings(&self, guild_id: GuildId, settings: GuildSettings) { + /// Add the guild settings for a guild. + pub async fn add_guild_settings(&self, guild_id: GuildId, settings: GuildSettings) { self.guild_settings_map .write() - .unwrap() + .await .insert(guild_id, settings); } - // /// Get the guild settings for a guild (read only) - // pub fn get_guild_settings_mut(&self, guild_id: GuildId) -> Option<&mut GuildSettings> { - // let mut asdf = self.guild_settings_map.write().unwrap().clone(); - // let qwer = asdf.get_mut(&guild_id); - // qwer - // } - /// Set the guild settings for a guild and return a new copy. pub fn with_guild_settings_map(&self, guild_settings: GuildSettingsMapParam) -> Self { Self(Arc::new(self.0.with_guild_settings_map(guild_settings))) } +} + +/// Trait to extend the Context struct with additional convenience functionality. +pub trait ContextExt { + /// Send a message to tell the worker pool to do a db write when it feels like it. + fn send_track_metadata_write_msg(&self, ready_track: &TrackReadyData); + /// Return the call that the bot is currently in, if it is in one. + fn get_call(&self) -> impl Future>, CrackedError>>; + /// Add a message to the cache + fn add_msg_to_cache_nonasync(&self, guild_id: GuildId, msg: Message) -> Option; + /// Gets the channel id that the bot is currently playing in for a given guild. + fn get_active_channel_id(&self, guild_id: GuildId) -> impl Future>; +} + +/// Implement the ContextExt trait for the Context struct. +impl ContextExt for Context<'_> { + /// Send a message to tell the worker pool to do a db write when it feels like it. + fn send_track_metadata_write_msg(&self, ready_track: &TrackReadyData) { + let username = ready_track.username.clone(); + let MyAuxMetadata::Data(aux_metadata) = ready_track.metadata.clone(); + let user_id = ready_track.user_id; + let guild_id = self.guild_id().unwrap(); + let channel_id = self.channel_id(); + match &self.data().db_channel { + Some(channel) => { + let write_data: MetadataMsg = MetadataMsg { + user_id, + aux_metadata, + username, + guild_id, + channel_id, + }; + if let Err(e) = channel.try_send(write_data) { + tracing::error!("Error sending metadata to db_channel: {}", e); + } + }, + None => {}, + } + } + + /// Return the call that the bot is currently in, if it is in one. + async fn get_call(&self) -> Result>, CrackedError> { + let guild_id = self.guild_id().ok_or(CrackedError::NoGuildId)?; + let manager = songbird::get(self.serenity_context()) + .await + .ok_or(CrackedError::NotConnected)?; + manager.get(guild_id).ok_or(CrackedError::NotConnected) + } - // /// Get the guild settings for a guild (mutable) - // pub fn get_guild_settings_mut(&self, guild_id: GuildId) -> Option<&mut GuildSettings> { - // self.guild_settings_map.write().unwrap().get_mut(&guild_id) - // } + /// Add a message to the cache + fn add_msg_to_cache_nonasync(&self, guild_id: GuildId, msg: Message) -> Option { + self.data().add_msg_to_cache(guild_id, msg) + } + + /// Gets the channel id that the bot is currently playing in for a given guild. + async fn get_active_channel_id(&self, guild_id: GuildId) -> Option { + let serenity_context = self.serenity_context(); + let manager = songbird::get(serenity_context) + .await + .expect("Failed to get songbird manager") + .clone(); + + let call_lock = manager.get(guild_id)?; + let call = call_lock.lock().await; + + let channel_id = call.current_channel()?; + let serenity_channel_id = ChannelId::new(channel_id.0.into()); + + Some(serenity_channel_id) + } } #[cfg(test)] -mod test { +mod lib_test { use super::*; #[test] @@ -605,13 +808,14 @@ mod test { #[test] fn test_display_cam_kick_config() { let cam_kick = CamKickConfig::default(); - let want = "cammed_down_timeout: 0\nguild_id: 0\nchannel_id: 0\ndc_message: \"You have been violated for being cammed down for too long.\"\ndeafen: false\nmute: false\ndc: false\n"; + // let want = "timeout: 0\nguild_id: 0\nchan_id: 0\ndc_msg: \"You have been violated for being cammed down for too long.\"\nmsg_on_deafen: false\nmsg_on_mute: false\nmsg_on_dc: false\n"; + let want = "timeout: 0\nguild_id: 0\nchan_id: 0\ndc_msg: \"You have been violated for being cammed down for too long.\"\nmsg_on_deafen: false\nmsg_on_mute: false\nmsg_on_dc: false\n"; assert_eq!(cam_kick.to_string(), want); } use serde_json::json; - #[test] - fn test_with_data_inner() { + #[tokio::test] + async fn test_with_data_inner() { let data = DataInner::default(); let new_data = data.with_bot_settings(BotConfig::default()); assert_eq!(json!(new_data.bot_settings), json!(BotConfig::default())); @@ -623,6 +827,6 @@ mod test { let guild_settings = GuildSettingsMapParam::default(); let new_data = new_data.with_guild_settings_map(guild_settings); - assert!(new_data.guild_settings_map.read().unwrap().is_empty()); + assert!(new_data.guild_settings_map.read().await.is_empty()); } } diff --git a/crack-core/src/messaging/help.rs b/crack-core/src/messaging/help.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crack-core/src/messaging/help.rs @@ -0,0 +1 @@ + diff --git a/crack-core/src/interface.rs b/crack-core/src/messaging/interface.rs similarity index 74% rename from crack-core/src/interface.rs rename to crack-core/src/messaging/interface.rs index 8c720ee8d..a0ccd5a64 100644 --- a/crack-core/src/interface.rs +++ b/crack-core/src/messaging/interface.rs @@ -1,19 +1,22 @@ -/// Contains functions for creating embeds and other messages which are used -/// to communicate with the user. +use super::messages::REQUESTED_BY; use crate::errors::CrackedError; use crate::messaging::messages::{ QUEUE_NOTHING_IS_PLAYING, QUEUE_NOW_PLAYING, QUEUE_NO_SONGS, QUEUE_NO_SRC, QUEUE_NO_TITLE, QUEUE_PAGE, QUEUE_PAGE_OF, QUEUE_UP_NEXT, }; -use crate::utils::calculate_num_pages; use crate::utils::EMBED_PAGE_SIZE; +use crate::utils::{calculate_num_pages, send_embed_response_poise}; use crate::Context as CrackContext; +use crate::{guild::settings::DEFAULT_LYRICS_PAGE_SIZE, utils::create_paged_embed}; use crate::{ messaging::message::CrackedMessage, utils::{ get_footer_info, get_human_readable_timestamp, get_requesting_user, get_track_metadata, }, }; +/// Contains functions for creating embeds and other messages which are used +/// to communicate with the user. +use lyric_finder::LyricResult; use poise::CreateReply; use serenity::all::UserId; use serenity::{ @@ -32,9 +35,9 @@ pub fn requesting_user_to_string(user_id: UserId) -> String { } } -/// Builds a page of the queue. +/// Creates a page of the queue. #[cfg(not(tarpaulin_include))] -async fn build_queue_page(tracks: &[TrackHandle], page: usize) -> String { +async fn create_queue_page(tracks: &[TrackHandle], page: usize) -> String { let start_idx = EMBED_PAGE_SIZE * page; let queue: Vec<&TrackHandle> = tracks .iter() @@ -69,7 +72,7 @@ async fn build_queue_page(tracks: &[TrackHandle], page: usize) -> String { description } -/// Builds the queue embed. +/// Creates a queue embed. pub async fn create_queue_embed(tracks: &[TrackHandle], page: usize) -> CreateEmbed { let (description, thumbnail) = if !tracks.is_empty() { let metadata = get_track_metadata(&tracks[0]).await; @@ -103,7 +106,7 @@ pub async fn create_queue_embed(tracks: &[TrackHandle], page: usize) -> CreateEm CreateEmbed::default() .thumbnail(thumbnail) .field(QUEUE_NOW_PLAYING, &description, false) - .field(QUEUE_UP_NEXT, build_queue_page(tracks, page).await, false) + .field(QUEUE_UP_NEXT, create_queue_page(tracks, page).await, false) .footer(CreateEmbedFooter::new(format!( "{} {} {} {}", QUEUE_PAGE, @@ -127,13 +130,13 @@ pub async fn create_now_playing_embed(track: &TrackHandle) -> CreateEmbed { let channel_field: (&'static str, String, bool) = match requesting_user { Ok(user_id) => ( - "Requested By", + REQUESTED_BY, format!(">>> {}", requesting_user_to_string(user_id)), true, ), Err(error) => { tracing::error!("error getting requesting user: {:?}", error); - ("Requested By", ">>> N/A".to_string(), true) + (REQUESTED_BY, ">>> N/A".to_string(), true) }, }; @@ -183,11 +186,20 @@ pub async fn create_search_results_reply(results: Vec) -> CreateRep #[cfg(not(tarpaulin_include))] pub async fn create_lyrics_embed( ctx: CrackContext<'_>, - track: String, - artists: String, - lyric: String, + lyric_res: LyricResult, ) -> Result<(), CrackedError> { - use crate::{guild::settings::DEFAULT_LYRICS_PAGE_SIZE, utils::create_paged_embed}; + let (track, artists, lyric) = match lyric_res { + LyricResult::Some { + track, + artists, + lyric, + } => (track, artists, lyric), + LyricResult::None => ( + "Unknown".to_string(), + "Unknown".to_string(), + "No lyrics found!".to_string(), + ), + }; create_paged_embed( ctx, @@ -200,7 +212,7 @@ pub async fn create_lyrics_embed( } /// Builds a single navigation button for the queue. -pub fn build_single_nav_btn(label: &str, is_disabled: bool) -> CreateButton { +pub fn create_single_nav_btn(label: &str, is_disabled: bool) -> CreateButton { CreateButton::new(label.to_string().to_ascii_lowercase()) .label(label) .style(ButtonStyle::Primary) @@ -209,16 +221,41 @@ pub fn build_single_nav_btn(label: &str, is_disabled: bool) -> CreateButton { } /// Builds the four navigation buttons for the queue. -pub fn build_nav_btns(page: usize, num_pages: usize) -> Vec { +pub fn create_nav_btns(page: usize, num_pages: usize) -> Vec { let (cant_left, cant_right) = (page < 1, page >= num_pages - 1); vec![CreateActionRow::Buttons(vec![ - build_single_nav_btn("<<", cant_left), - build_single_nav_btn("<", cant_left), - build_single_nav_btn(">", cant_right), - build_single_nav_btn(">>", cant_right), + create_single_nav_btn("<<", cant_left), + create_single_nav_btn("<", cant_left), + create_single_nav_btn(">", cant_right), + create_single_nav_btn(">>", cant_right), ])] } +/// Sends a message to the user indicating that the search failed. +pub async fn send_search_failed(ctx: CrackContext<'_>) -> Result<(), CrackedError> { + let guild_id = ctx.guild_id().unwrap(); + let embed = CreateEmbed::default() + .description(format!( + "{}", + CrackedError::Other("Something went wrong while parsing your query!") + )) + .footer(CreateEmbedFooter::new("Search failed!")); + let msg = send_embed_response_poise(ctx, embed).await?; + ctx.data().add_msg_to_cache(guild_id, msg); + Ok(()) +} + +/// Sends a message to the user indicating that no query was provided. +pub async fn send_no_query_provided(ctx: CrackContext<'_>) -> Result<(), CrackedError> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let embed = CreateEmbed::default() + .description(format!("{}", CrackedError::Other("No query provided!"))) + .footer(CreateEmbedFooter::new("No query provided!")); + let msg = send_embed_response_poise(ctx, embed).await?; + ctx.data().add_msg_to_cache(guild_id, msg); + Ok(()) +} + #[cfg(test)] mod test { #[test] diff --git a/crack-core/src/messaging/message.rs b/crack-core/src/messaging/message.rs index ab5339474..99347a133 100644 --- a/crack-core/src/messaging/message.rs +++ b/crack-core/src/messaging/message.rs @@ -6,7 +6,7 @@ use ::serenity::builder::CreateEmbed; use crack_osint::virustotal::VirusTotalApiResponse; use poise::serenity_prelude::{self as serenity, UserId}; -use crate::messaging::messages::*; +use crate::{errors::CrackedError, messaging::messages::*}; const RELEASES_LINK: &str = "https://github.com/cycle-five/cracktunes/releases"; const REPO_LINK: &str = "https://github.com/cycle-five/cracktunes/"; @@ -22,12 +22,18 @@ pub enum CrackedMessage { channel_name: String, }, CountryName(String), + ChannelSizeSet { + id: serenity::ChannelId, + name: String, + size: u32, + }, ChannelDeleted { channel_id: serenity::ChannelId, channel_name: String, }, Clear, Clean(i32), + CrackedError(CrackedError), DomainInfo(String), Error, ErrorHttp(serenity::http::HttpError), @@ -101,50 +107,58 @@ pub enum CrackedMessage { channel_id: serenity::ChannelId, channel_name: String, }, - UserAuthorized { - user_id: UserId, - user_name: String, + id: UserId, + mention: Mention, guild_id: serenity::GuildId, guild_name: String, }, UserDeauthorized { - user_id: UserId, - user_name: String, + id: UserId, + mention: Mention, guild_id: serenity::GuildId, guild_name: String, }, UserTimeout { - user: String, - user_id: String, + id: UserId, + mention: Mention, timeout_until: String, }, UserKicked { - user_id: UserId, + mention: Mention, + id: UserId, }, UserBanned { - user: String, - user_id: UserId, + mention: Mention, + id: UserId, }, UserUnbanned { - user: String, - user_id: UserId, + mention: Mention, + id: UserId, }, UserMuted { - user: String, - user_id: UserId, + mention: Mention, + id: UserId, }, UserUnmuted { - user: String, - user_id: UserId, + mention: Mention, + id: UserId, }, UserDeafened { - user: String, - user_id: UserId, + mention: Mention, + id: UserId, + }, + UserDeafenedFail { + mention: Mention, + id: UserId, }, UserUndeafened { - user: String, - user_id: UserId, + mention: Mention, + id: UserId, + }, + UserUndeafenedFail { + mention: Mention, + id: UserId, }, Version { current: String, @@ -177,6 +191,9 @@ impl Display for CrackedMessage { Self::CountryName(name) => f.write_str(name), Self::Clear => f.write_str(CLEARED), Self::Clean(n) => f.write_str(&format!("{} {}!", CLEANED, n)), + Self::ChannelSizeSet { id, name, size } => { + f.write_str(&format!("{} {} {} {}", CHANNEL_SIZE_SET, name, id, size)) + }, Self::ChannelDeleted { channel_id, channel_name, @@ -184,6 +201,7 @@ impl Display for CrackedMessage { "{} {} {}", CHANNEL_DELETED, channel_id, channel_name )), + Self::CrackedError(err) => f.write_str(&format!("{}", err)), Self::DomainInfo(info) => f.write_str(info), Self::Error => f.write_str(ERROR), Self::ErrorHttp(err) => f.write_str(&format!("{}", err)), @@ -261,50 +279,49 @@ impl Display for CrackedMessage { CATEGORY_CREATED, channel_id, channel_name )), Self::UserAuthorized { - user_id, - user_name, + id, + mention, guild_id, guild_name, } => f.write_str(&format!( "{}\n User: {} ({}) Guild: {} ({})", - AUTHORIZED, user_name, user_id, guild_name, guild_id + AUTHORIZED, mention, id, guild_name, guild_id )), Self::UserDeauthorized { - user_id, - user_name, + id, + mention, guild_id, guild_name, } => f.write_str(&format!( "{}\n User: {} ({}) Guild: {} ({})", - DEAUTHORIZED, user_name, user_id, guild_name, guild_id + DEAUTHORIZED, mention, id, guild_name, guild_id )), Self::UserTimeout { - user: _, - user_id, + mention, + id, timeout_until, } => f.write_str(&format!( - "User timed out: {} for {}", - user_id, timeout_until + "{TIMEOUT}\n{mention} ({id})\n{UNTIL}: {timeout_until}" )), - Self::UserKicked { user_id } => f.write_str(&format!("{} {}", KICKED, user_id)), - Self::UserBanned { user, user_id } => { - f.write_str(&format!("{} {} {}", BANNED, user, user_id)) - }, - Self::UserUnbanned { user, user_id } => { - f.write_str(&format!("{} {} {}", UNBANNED, user, user_id)) + Self::UserKicked { mention, id } => f.write_str(&format!("{KICKED}\n{mention} ({id})")), + Self::UserBanned { mention, id } => f.write_str(&format!("{BANNED}\n{mention} ({id})")), + Self::UserUnbanned { mention, id } => { + f.write_str(&format!("{UNBANNED}\n{mention} ({id})")) }, - Self::UserUndeafened { user, user_id } => { - f.write_str(&format!("{} {} {}", UNDEAFENED, user, user_id)) + Self::UserUndeafened { mention, id } => { + f.write_str(&format!("{} {} {}", UNDEAFENED, mention, id)) }, - Self::UserDeafened { user, user_id } => { - f.write_str(&format!("{} {} {}", DEAFENED, user, user_id)) + Self::UserDeafened { mention, id } => { + f.write_str(&format!("{DEAFENED}\n{mention}({id})")) }, - Self::UserMuted { user, user_id } => { - f.write_str(&format!("{} {} {}", MUTED, user, user_id)) + Self::UserDeafenedFail { mention, id } => { + f.write_str(&format!("{DEAFENED_FAIL}\n{mention} ({id})")) }, - Self::UserUnmuted { user, user_id } => { - f.write_str(&format!("{} {} {}", UNMUTED, user, user_id)) + Self::UserUndeafenedFail { mention, id } => { + f.write_str(&format!("{UNDEAFENED_FAIL}\n{mention} ({id})")) }, + Self::UserMuted { mention, id } => f.write_str(&format!("{MUTED}\n{mention} {id}")), + Self::UserUnmuted { mention, id } => f.write_str(&format!("{UNMUTED}\n{mention} {id}")), Self::Version { current, hash } => f.write_str(&format!( "{} [{}]({}/tag/v{})\n{}({}/latest)\n{}({}/tree/{})", VERSION, diff --git a/crack-core/src/messaging/messages.rs b/crack-core/src/messaging/messages.rs index 155f6b649..0e50f7ea7 100644 --- a/crack-core/src/messaging/messages.rs +++ b/crack-core/src/messaging/messages.rs @@ -5,14 +5,19 @@ pub const AUTOPLAY_OFF: &str = "🤖 Autoplay OFF!"; pub const AUTOPLAY_ON: &str = "🤖 Autoplay ON!"; pub const CLEARED: &str = "🗑️ Cleared!"; pub const CLEANED: &str = "🗑️ Messages Cleaned: "; +pub const CHANNEL_SIZE_SET: &str = "🗑️ Channel size set!"; pub const CHANNEL_DELETED: &str = "🗑️ Deleted channel!"; pub const AUTHORIZED: &str = "✅ User has been authorized."; pub const DEAUTHORIZED: &str = "❌ User has been deauthorized."; pub const BANNED: &str = "Banned"; pub const UNBANNED: &str = "Unbanned"; -pub const DEAFENED: &str = "Deafened"; +// Use the unicode emoji for the check mark +pub const EMOJI_HEADPHONES: &str = "🎧"; +pub const DEAFENED: &str = "User deafened."; +pub const DEAFENED_FAIL: &str = "User failed to be deafened."; pub const UNDEAFENED: &str = "Undeafened"; +pub const UNDEAFENED_FAIL: &str = "User failed to be undeafened."; pub const MUTED: &str = "Muted"; pub const UNMUTED: &str = "Unmuted"; @@ -27,12 +32,16 @@ pub const DOMAIN_FORM_TITLE: &str = "Manage sources"; pub const ERROR: &str = "Fatality! Something went wrong ☹️"; pub const FAIL_ALREADY_HERE: &str = "⚠️ I'm already here!"; pub const FAIL_ANOTHER_CHANNEL: &str = "⚠️ I'm already connected to"; +pub const FAIL_AUDIO_STREAM_RUSTY_YTDL_METADATA: &str = + "⚠️ Failed to fetch metadata from rusty_ytdl!"; pub const FAIL_AUTHOR_DISCONNECTED: &str = "⚠️ You are not connected to"; ///? pub const FAIL_AUTHOR_NOT_FOUND: &str = "⚠️ Could not find you in any voice channel!"; -pub const FAIL_INVALID_TOPGG_TOKEN: &str = "⚠️ Invalid top.gg token!"; pub const FAIL_LOOP: &str = "⚠️ Failed to toggle loop!"; pub const FAIL_EMPTY_VECTOR: &str = "⚠️ Empty vector not allowed!"; +pub const FAIL_INSERT: &str = "⚠️ Failed to insert!"; +pub const FAIL_INVALID_TOPGG_TOKEN: &str = "⚠️ Invalid top.gg token!"; +pub const FAIL_INVALID_PERMS: &str = "⚠️ Invalid permissions!!"; pub const FAIL_MINUTES_PARSING: &str = "⚠️ Invalid formatting for 'minutes'"; pub const FAIL_NO_SONG_ON_INDEX: &str = "⚠️ There is no queued song on that index!"; pub const FAIL_NO_SONGBIRD: &str = "⚠️ Failed to get songbird!"; @@ -43,6 +52,7 @@ pub const FAIL_NOT_IMPLEMENTED: &str = "⚠️ Function is not implemented!"; pub const FAIL_NOTHING_PLAYING: &str = "🔈 Nothing is playing!"; pub const FAIL_REMOVE_RANGE: &str = "⚠️ `until` needs to be higher than `index`!"; pub const FAIL_SECONDS_PARSING: &str = "⚠️ Invalid formatting for 'seconds'"; +pub const FAIL_TO_SET_CHANNEL_SIZE: &str = "⚠️ Failed to set channel size!"; pub const FAIL_WRONG_CHANNEL: &str = "⚠️ We are not in the same voice channel!"; pub const FAIL_PARSE_TIME: &str = "⚠️ Failed to parse time, speak English much?"; pub const FAIL_PLAYLIST_FETCH: &str = "⚠️ Failed to fetch playlist!"; @@ -63,6 +73,7 @@ pub const NO_DATABASE_POOL: &str = "⚠️ No Database Pool Found!"; pub const NO_GUILD_CACHED: &str = "⚠️ No Cached Guild Found!"; pub const NO_GUILD_ID: &str = "⚠️ No GuildId Found!"; pub const NO_GUILD_SETTINGS: &str = "⚠️ No GuildSettings Found!"; +pub const NO_USER_AUTOPLAY: &str = "(auto)"; pub const ONETWOFT: &str = "https://12ft.io/"; pub const PAGINATION_COMPLETE: &str = "🔚 Dynamic message timed out! Run the command again to see updates."; @@ -98,10 +109,12 @@ pub const QUEUE_UP_NEXT: &str = "⌛ Up next"; pub const REMOVED_QUEUE_MULTIPLE: &str = "❌ Removed multiple tracks from queue!"; pub const REMOVED_QUEUE: &str = "❌ Removed from queue"; pub const RESUMED: &str = "▶️ Resumed!"; +pub const REQUESTED_BY: &str = "Requested by"; pub const ROLE_CREATED: &str = "📝 Created role!"; pub const ROLE_DELETED: &str = "🗑️ Deleted role!"; pub const ROLE_NOT_FOUND: &str = "⚠️ Role not found!"; pub const PREMIUM: &str = "👑 Premium status now"; +pub const PROGRESS: &str = "Progress"; pub const SCAN_QUEUED: &str = "🔍 Scan queued! Use"; pub const SEARCHING: &str = "🔎 Searching..."; pub const SEEKED: &str = "⏩ Seeked current track to"; @@ -117,6 +130,8 @@ pub const SPOTIFY_INVALID_QUERY: &str = "⚠️ **Could not find any tracks with that link!**\nAre you sure that is a valid Spotify URL?"; pub const SPOTIFY_PLAYLIST_FAILED: &str = "⚠️ **Failed to fetch playlist!**\nIt's likely that this playlist is either private or a personalized playlist generated by Spotify, like your daylist."; pub const STOPPED: &str = "⏹️ Stopped!"; +pub const TIMEOUT: &str = "⏱️ User Timed Out!"; +pub const UNTIL: &str = "Until"; pub const TRACK_DURATION: &str = "Track duration: "; pub const TRACK_NOT_FOUND: &str = "⚠️ **Could not play track!**\nYour request yielded no results."; pub const TRACK_INAPPROPRIATE: &str = "⚠️ **Could not play track!**\nThe video you requested may be inappropriate for some users, so sign-in is required."; @@ -124,6 +139,7 @@ pub const TRACK_TIME_TO_PLAY: &str = "Estimated time until play: "; pub const TEXT_CHANNEL_CREATED: &str = "📝 Created text channel!"; pub const CATEGORY_CREATED: &str = "📝 Created category!"; pub const UNAUTHORIZED_USER: &str = "⚠️ You are not authorized to use this command!"; +pub const UNKNOWN_LIT: &str = "Unknown"; pub const WAYBACK_SNAPSHOT: &str = "Wayback snapshot for"; pub const KICKED: &str = "Kicked"; pub const VERSION_LATEST: &str = "Find the latest version [here]"; diff --git a/crack-core/src/messaging/mod.rs b/crack-core/src/messaging/mod.rs index 88cfd7a97..156c69975 100644 --- a/crack-core/src/messaging/mod.rs +++ b/crack-core/src/messaging/mod.rs @@ -1,2 +1,4 @@ +pub mod help; +pub mod interface; pub mod message; pub mod messages; diff --git a/crack-core/src/sources/mod.rs b/crack-core/src/sources/mod.rs index d1b0de1bb..1bc34d490 100644 --- a/crack-core/src/sources/mod.rs +++ b/crack-core/src/sources/mod.rs @@ -1,3 +1,4 @@ pub mod rusty_ytdl; pub mod spotify; +pub mod youtube; pub mod ytdl; diff --git a/crack-core/src/sources/rusty_ytdl.rs b/crack-core/src/sources/rusty_ytdl.rs index b220e6c4c..3ce7dcf6a 100644 --- a/crack-core/src/sources/rusty_ytdl.rs +++ b/crack-core/src/sources/rusty_ytdl.rs @@ -1,15 +1,75 @@ -use std::{fmt::Display, time::Duration}; - -use crate::{commands::QueryType, errors::CrackedError, http_utils}; +use crate::{commands::play_utils::QueryType, errors::CrackedError, http_utils}; +use bytes::Buf; +use bytes::BytesMut; +use rusty_ytdl::stream::Stream; use rusty_ytdl::{ search::{Playlist, SearchOptions, SearchResult, YouTube}, Video, VideoInfo, }; -use songbird::input::AuxMetadata; +use serenity::async_trait; +use songbird::input::{AudioStream, AudioStreamError, AuxMetadata, Compose, Input, YoutubeDl}; +use std::io::{self, Read, Seek, SeekFrom}; +use std::pin::Pin; +use std::sync::Arc; +use std::{fmt::Display, time::Duration}; +use symphonia::core::io::MediaSource; +use tokio::sync::RwLock; + +use super::ytdl::HANDLE; +/// Hacky, why did I do this? `AsString` +pub trait AsString { + fn as_string(&self) -> String; +} + +/// Implement the `AsString` trait for the `SearchResult` enum. +impl AsString for SearchResult { + fn as_string(&self) -> String { + match self { + SearchResult::Video(video) => video.title.clone(), + SearchResult::Playlist(playlist) => playlist.name.clone(), + SearchResult::Channel(channel) => channel.name.clone(), + } + } +} + +/// Implement the `AsString` trait for the `VideoInfo` struct. +impl AsString for VideoInfo { + fn as_string(&self) -> String { + self.video_details.title.clone() + } +} + +/// Implement the `AsString` trait for the `Playlist` struct. +impl AsString for Playlist { + fn as_string(&self) -> String { + self.name.clone() + } +} + +/// Implement the `AsString` trait for the `YouTube` struct. +impl AsString for YouTube { + fn as_string(&self) -> String { + "YouTube".to_string() + } +} + +/// Implement the `AsString` trait for the `YoutubeDl` struct. +impl AsString for YoutubeDl { + fn as_string(&self) -> String { + "YoutubeDl".to_string() + } +} + +/// Implement the `AsString` trait for the `RustyYoutubeClient` struct. +impl AsString for RustyYoutubeClient { + fn as_string(&self) -> String { + self.to_string() + } +} -/// Out strucut to wrap the rusty-ytdl search instance -//TODO expand to go beyond search #[derive(Clone, Debug)] +/// Our strucut to wrap the rusty-ytdl search instance +//TODO expand to go beyond search pub struct RustyYoutubeClient { pub rusty_ytdl: YouTube, pub client: reqwest::Client, @@ -27,9 +87,30 @@ impl Display for RustyYoutubeClient { #[derive(Clone, Debug)] pub struct RustyYoutubeSearch { - rusty_ytdl: RustyYoutubeClient, - metadata: Option, - query: QueryType, + pub rusty_ytdl: RustyYoutubeClient, + pub metadata: Option, + pub query: QueryType, +} + +/// More general struct to wrap the search instances. Name this better. +#[derive(Clone, Debug)] +pub struct FastYoutubeSearch { + pub query: QueryType, + pub client: reqwest::Client, + pub ytdl: either::Either, + pub metadata: Option, +} + +impl Display for FastYoutubeSearch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + r#"FastYT: Query: {:?} + ytdl: {:?}"#, + self.query.build_query(), + either::for_both!(&self.ytdl, ytdl => ytdl.as_string()), + ) + } } impl Display for RustyYoutubeSearch { @@ -97,7 +178,10 @@ impl RustyYoutubeClient { pub fn video_info_to_aux_metadata(video: &VideoInfo) -> AuxMetadata { let mut metadata = AuxMetadata::default(); - tracing::warn!("{:?}", video.video_details); + tracing::info!( + "video_info_to_aux_metadata: {:?}", + video.video_details.title + ); let details = &video.video_details; metadata.artist = None; metadata.album = None; @@ -126,7 +210,7 @@ impl RustyYoutubeClient { // ..Default::default() // }; // let video = Video::new_with_options(&url, vid_options)?; - let video = Video::new(&url).unwrap(); + let video = Video::new(&url)?; video.get_basic_info().await.map_err(|e| e.into()) } @@ -155,6 +239,192 @@ impl RustyYoutubeClient { } } +impl From for Input { + fn from(val: RustyYoutubeSearch) -> Self { + Input::Lazy(Box::new(val)) + } +} + +#[async_trait] +impl Compose for RustyYoutubeSearch { + fn create(&mut self) -> Result>, AudioStreamError> { + Err(AudioStreamError::Unsupported) + } + + async fn create_async( + &mut self, + ) -> Result>, AudioStreamError> { + let query_str = self + .query + .build_query() + .unwrap_or("Rick Astley Never Gonna Give You Up".to_string()); + let search_res = self + .rusty_ytdl + .one_shot(query_str) + .await? + .ok_or_else(|| CrackedError::AudioStreamRustyYtdlMetadata)?; + let search_video = match search_res { + SearchResult::Video(video) => video, + SearchResult::Playlist(playlist) => { + let video = playlist.videos.first().unwrap(); + video.clone() + }, + _ => { + return Err(Into::into(CrackedError::AudioStreamRustyYtdlMetadata)); + }, + }; + Video::new(&search_video.url) + .map_err(CrackedError::from)? + .stream() + .await + .map(|input| { + // let stream = AsyncAdapterStream::new(input, 64 * 1024); + let stream = Box::into_pin(input).into_media_source(); + + AudioStream { + input: Box::new(stream) as Box, + hint: None, + } + }) + .map_err(|e| AudioStreamError::from(CrackedError::from(e))) + } + + fn should_create_async(&self) -> bool { + true + } + + async fn aux_metadata(&mut self) -> Result { + if let Some(meta) = self.metadata.as_ref() { + return Ok(meta.clone()); + } + + self.rusty_ytdl + .one_shot(self.query.build_query().unwrap()) + .await?; + + self.metadata + .clone() + .ok_or_else(|| AudioStreamError::from(CrackedError::AudioStreamRustyYtdlMetadata)) + } +} + +pub trait StreamExt { + fn into_media_source(self: Pin>) -> MediaSourceStream; +} + +impl StreamExt for dyn Stream + Sync + Send { + fn into_media_source(self: Pin>) -> MediaSourceStream + where + Self: Sync + Send + 'static, + { + MediaSourceStream { + stream: self, + buffer: Arc::new(RwLock::new(BytesMut::new())), + position: Arc::new(RwLock::new(0)), + } + } +} + +pub struct MediaSourceStream { + stream: Pin>, + buffer: Arc>, + position: Arc>, +} + +impl MediaSourceStream { + async fn read_async(&mut self, buf: &mut [u8]) -> io::Result { + let opt_bytes = if self.buffer.read().await.is_empty() { + either::Left( + self.stream + .chunk() + .await + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?, + ) + } else { + either::Right(()) + }; + + let chunk = match opt_bytes { + either::Left(Some(chunk)) => Some(chunk), + either::Left(None) => return Ok(0), // End of stream + either::Right(_) => None, + }; + + let mut buffer = self.buffer.write().await; + let mut position = self.position.write().await; + + if let Some(chunk) = chunk { + buffer.extend_from_slice(&chunk); + } + + let len = std::cmp::min(buf.len(), buffer.len()); + buf[..len].copy_from_slice(&buffer[..len]); + buffer.advance(len); + *position += len as u64; + + Ok(len) + } +} + +impl Read for MediaSourceStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + // Get the current tokio runtime + let handle = HANDLE.lock().unwrap().clone().unwrap(); + tokio::task::block_in_place(move || handle.block_on(async { self.read_async(buf).await })) + } +} + +impl Seek for MediaSourceStream { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + match pos { + SeekFrom::End(offset) => { + let len = self.byte_len().ok_or(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid seek position", + ))?; + let new_position = len as i64 + offset; + if new_position < 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid seek position", + )); + } + let mut position = self.position.blocking_write(); + *position = new_position as u64; + Ok(*position) + }, + SeekFrom::Start(offset) => { + let mut position = self.position.blocking_write(); + *position = offset; + Ok(*position) + }, + SeekFrom::Current(offset) => { + let mut position = self.position.blocking_write(); + let new_position = (*position as i64) + offset; + if new_position < 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid seek position", + )); + } + *position = new_position as u64; + Ok(*position) + }, + } + } +} + +impl MediaSource for MediaSourceStream { + fn is_seekable(&self) -> bool { + false + } + + fn byte_len(&self) -> Option { + // Some(self.stream.content_length() as u64) + Some(0) + } +} + #[cfg(test)] mod test { use crate::http_utils; @@ -169,32 +439,28 @@ mod test { let ytdl = crate::sources::rusty_ytdl::RustyYoutubeClient::new_with_client(client).unwrap(); let ytdl = Arc::new(ytdl); let playlist = ytdl.one_shot("The Night Chicago Died".to_string()).await; - if playlist.is_err() { - assert!(playlist - .unwrap_err() - .to_string() - .contains("Your IP is likely being blocked")); - } else { - let playlist_val = playlist.unwrap().unwrap(); - let metadata = - crate::sources::rusty_ytdl::RustyYoutubeClient::search_result_to_aux_metadata( - &playlist_val, - ); - println!("{:?}", metadata); + match playlist { + Ok(Some(playlist)) => { + let metadata = + crate::sources::rusty_ytdl::RustyYoutubeClient::search_result_to_aux_metadata( + &playlist, + ); + println!("{:?}", metadata); + }, + Ok(None) => { + assert!(false) + }, + Err(e) => { + assert!(e.to_string().contains("Your IP is likely being blocked")) + }, } } #[tokio::test] async fn test_rusty_ytdl() { - // let url = "https://www.youtube.com/watch?v=6n3pFFPSlW4".to_string(); let searches = vec!["the night chicago died", "Oh Shit I'm Feeling It"]; - // let client = reqwest::ClientBuilder::new() - // .use_rustls_tls() - // .build() - // .unwrap(); let rusty_ytdl = YouTube::new().unwrap(); - // let mut all_res = Vec::new(); for search in searches { let res = rusty_ytdl.search_one(search.to_string(), None).await; println!("{res:?}"); @@ -205,7 +471,6 @@ mod test { .to_string() .contains("Your IP is likely being blocked") ); - // all_res.push(res.unwrap().clone()); } } @@ -226,7 +491,6 @@ mod test { .unwrap(); let ytdl = crate::sources::rusty_ytdl::RustyYoutubeClient::new_with_client(client).unwrap(); let ytdl = Arc::new(ytdl); - // let mut all_res = Vec::new(); for search in searches { let res = ytdl.one_shot(search.to_string()).await; assert!( @@ -236,7 +500,6 @@ mod test { .to_string() .contains("Your IP is likely being blocked") ); - // all_res.push(res.unwrap().clone()); } } @@ -265,57 +528,6 @@ mod test { res_all.push(res); } - // assert!(res_all.len() == 5); - println!("{:?}", res_all); } - // #[tokio::test] - // async fn test_ytdl_parallel() { - // // let url = "https://www.youtube.com/watch?v=6n3pFFPSlW4".to_string(); - // let searches = vec![ - // "The Night Chicago Died".to_string(), - // "The Devil Went Down to Georgia".to_string(), - // "Hit That The Offspring".to_string(), - // "Nightwish I Wish I had an Angel".to_string(), - // "Oh Shit I'm Feeling It".to_string(), - // ]; - // let ytdl = crate::sources::rusty_ytdl::MyRustyYoutubeDl::new(None).unwrap(); - // let ytdl = Arc::new(ytdl); - // use tokio::task::JoinSet; - - // { - // let ytdl2 = ytdl.clone(); - // let mut futures = Vec::with_capacity(searches.len()); - // for search in searches { - // let fut = ytdl2.clone().one_shot(search); - // futures.push(fut); - // } - - // let mut set = JoinSet::new(); - - // for fut in futures { - // set.spawn(fut); - // } - - // let mut results = Vec::with_capacity(futures.len()); - // while let Some(res) = set.join_next().await { - // let out = &mut res.unwrap().unwrap(); - // results.append(out); - // } - - // assert!(results.len() == 5); - // println!("{:?}", results); - // } - // println!("{:?}", ytdl) - // // for search in searches { - // // let join_handle = - // // tokio::spawn(async move { ytdl.clone().one_shot(search.to_string()) }); - // // handles.push(join_handle); - // // } - - // // for handle in handles { - // // results.push(handle.await.unwrap()) - // // } - // //tokio::join!(all_res); - // } } diff --git a/crack-core/src/sources/spotify.rs b/crack-core/src/sources/spotify.rs index f7094f488..69703ff79 100644 --- a/crack-core/src/sources/spotify.rs +++ b/crack-core/src/sources/spotify.rs @@ -1,5 +1,5 @@ use crate::{ - commands::QueryType, + commands::play_utils::QueryType, errors::CrackedError, messaging::messages::{SPOTIFY_INVALID_QUERY, SPOTIFY_PLAYLIST_FAILED}, }; @@ -14,7 +14,12 @@ use rspotify::{ }, ClientCredsSpotify, ClientResult, Config, Credentials, }; -use std::{env, str::FromStr, time::Duration}; +use std::{ + env, + ops::{Deref, DerefMut}, + str::FromStr, + time::Duration, +}; use tokio::sync::Mutex; lazy_static! { @@ -69,6 +74,7 @@ impl FromStr for MediaType { } } +#[derive(Debug, Clone)] pub struct ParsedSpotifyUrl { media_type: MediaType, media_id: String, @@ -76,7 +82,25 @@ pub struct ParsedSpotifyUrl { type SpotifyCreds = Credentials; +#[derive(Debug, Clone)] +pub struct SpotifyPlaylist(FullPlaylist); + +impl Deref for SpotifyPlaylist { + type Target = FullPlaylist; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SpotifyPlaylist { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + /// Spotify source. +#[derive(Debug, Clone)] pub struct Spotify {} /// Implementation of Spotify source. @@ -489,7 +513,11 @@ impl From for SpotifyTrack { #[cfg(test)] mod test { + use crate::commands::MyAuxMetadata; + use super::*; + use rspotify::model::{FullTrack, SimplifiedAlbum}; + use std::collections::HashMap; // // Mock ClientCredsSpotify // struct MockClientCredsSpotify {} @@ -563,4 +591,46 @@ mod test { // .unwrap(); // assert_eq!(tracks.len(), 50); // } + #[test] + fn test_from_spotify_track() { + let track = SpotifyTrack::new(FullTrack { + id: None, + name: "asdf".to_string(), + artists: vec![], + album: SimplifiedAlbum { + album_type: None, + album_group: None, + artists: vec![], + available_markets: vec![], + external_urls: HashMap::new(), + href: None, + id: None, + images: vec![], + name: "zxcv".to_string(), + release_date: Some("2012".to_string()), + release_date_precision: None, + restrictions: None, + }, + track_number: 0, + disc_number: 0, + explicit: false, + external_urls: HashMap::new(), + href: None, + preview_url: None, + popularity: 0, + is_playable: None, + linked_from: None, + restrictions: None, + external_ids: HashMap::new(), + is_local: false, + available_markets: vec![], + duration: chrono::TimeDelta::new(60, 0).unwrap(), + }); + let res = MyAuxMetadata::from_spotify_track(&track); + let metadata = res.metadata(); + assert_eq!(metadata.title, Some("asdf".to_string())); + assert_eq!(metadata.artist, Some("".to_string())); + assert_eq!(metadata.album, Some("zxcv".to_string())); + assert_eq!(metadata.duration.unwrap().as_secs(), 60); + } } diff --git a/crack-core/src/sources/youtube.rs b/crack-core/src/sources/youtube.rs new file mode 100644 index 000000000..59b921000 --- /dev/null +++ b/crack-core/src/sources/youtube.rs @@ -0,0 +1,195 @@ +use crate::commands::play_utils::QueryType; +use crate::sources::rusty_ytdl::RustyYoutubeSearch; +use crate::{ + commands::MyAuxMetadata, errors::CrackedError, sources::rusty_ytdl::RustyYoutubeClient, +}; +use songbird::input::{AuxMetadata, Compose, Input as SongbirdInput, YoutubeDl}; + +/// Get the source and metadata from a video link. Return value is a vector due +/// to this being used in a method that also handles the interactive search so +/// it can return multiple metadatas. +pub async fn video_info_to_source_and_metadata( + client: reqwest::Client, + url: String, +) -> Result<(SongbirdInput, Vec), CrackedError> { + let rytdl = RustyYoutubeClient::new_with_client(client.clone())?; + let video_info = rytdl.get_video_info(url.clone()).await?; + let metadata = RustyYoutubeClient::video_info_to_aux_metadata(&video_info); + let my_metadata = MyAuxMetadata::Data(metadata.clone()); + + // let ytdl = YoutubeDl::new(client, url); + let rusty_search = RustyYoutubeSearch { + rusty_ytdl: rytdl, + metadata: Some(metadata.clone()), + query: QueryType::VideoLink(url), + }; + Ok((rusty_search.into(), vec![my_metadata])) +} + +/// Search youtube for a query and return the source (playable) +/// and metadata. +pub async fn search_query_to_source_and_metadata( + client: reqwest::Client, + query: String, +) -> Result<(SongbirdInput, Vec), CrackedError> { + tracing::warn!("search_query_to_source_and_metadata: {:?}", query); + + let metadata = { + let rytdl = RustyYoutubeClient::new_with_client(client.clone())?; + // let rytdl = RustyYoutubeClient::new()?; + tracing::warn!("search_query_to_source_and_metadata: {:?}", rytdl); + let results = rytdl.one_shot(query.clone()).await?; + tracing::warn!("search_query_to_source_and_metadata: {:?}", results); + // FIXME: Fallback to yt-dlp + let result = match results { + Some(r) => r, + None => return Err(CrackedError::EmptySearchResult), + }; + let metadata = &RustyYoutubeClient::search_result_to_aux_metadata(&result); + metadata.clone() + }; + + let source_url = match metadata.clone().source_url { + Some(url) => url.clone(), + None => "".to_string(), + }; + let ytdl = YoutubeDl::new(client, source_url); + let my_metadata = MyAuxMetadata::Data(metadata); + + Ok((ytdl.into(), vec![my_metadata])) + // Ok((ytdl.into(), vec![MyAuxMetadata::Data(metadata)])) +} + +/// Search youtube for a query and return the source (playable) +/// and metadata. +pub async fn search_query_to_source_and_metadata_rusty( + client: reqwest::Client, + query: QueryType, +) -> Result<(SongbirdInput, Vec), CrackedError> { + tracing::warn!("search_query_to_source_and_metadata_rusty: {:?}", query); + let rytdl = RustyYoutubeClient::new_with_client(client.clone())?; + + let metadata = { + // let rytdl = RustyYoutubeClient::new()?; + tracing::warn!("search_query_to_source_and_metadata_rusty: {:?}", rytdl); + let results = rytdl + .one_shot( + query + .build_query() + .ok_or(CrackedError::Other("No query given"))?, + ) + .await?; + tracing::warn!("search_query_to_source_and_metadata_rusty: {:?}", results); + // FIXME: Fallback to yt-dlp + let result = match results { + Some(r) => r, + None => return Err(CrackedError::EmptySearchResult), + }; + let metadata = &RustyYoutubeClient::search_result_to_aux_metadata(&result); + metadata.clone() + }; + + let rusty_search = RustyYoutubeSearch { + rusty_ytdl: rytdl, + metadata: Some(metadata.clone()), + query, + }; + + Ok((rusty_search.into(), vec![MyAuxMetadata::Data(metadata)])) +} + +/// Search youtube for a query and return the source (playable) +/// and metadata using the yt-dlp command line tool. +pub async fn search_query_to_source_and_metadata_ytdl( + client: reqwest::Client, + query: String, +) -> Result<(SongbirdInput, Vec), CrackedError> { + let query = if query.starts_with("ytsearch:") { + query + } else { + format!("ytsearch:{}", query) + }; + let mut ytdl = YoutubeDl::new(client, query); + let metadata = ytdl.aux_metadata().await?; + let my_metadata = MyAuxMetadata::Data(metadata); + + Ok((ytdl.into(), vec![my_metadata])) +} + +/// Build a query from AuxMetadata. +pub fn build_query_aux_metadata(aux_metadata: &AuxMetadata) -> String { + format!( + "{} - {}", + aux_metadata.track.clone().unwrap_or_default(), + aux_metadata.artist.clone().unwrap_or_default(), + ) +} + +#[cfg(test)] +mod test { + + use super::*; + + #[tokio::test] + async fn test_get_track_source_and_metadata() { + let query_type = QueryType::Keywords("hello".to_string()); + let res = query_type.get_track_source_and_metadata().await; + assert!(res.is_ok()); + } + + #[tokio::test] + async fn test_get_track_source_and_metadata_ytdl() { + let query_type = QueryType::Keywords("hello".to_string()); + let res = query_type.get_track_source_and_metadata().await; + assert!(res.is_ok()); + } + + #[tokio::test] + async fn test_get_track_source_and_metadata_video_link() { + let query_type = + QueryType::VideoLink("https://www.youtube.com/watch?v=6n3pFFPSlW4".to_string()); + let res = query_type.get_track_source_and_metadata().await; + assert!(res.is_ok()); + } + + #[tokio::test] + async fn test_get_track_source_and_metadata_playlist_link() { + let query_type = QueryType::PlaylistLink( + "https://www.youtube.com/playlist?list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI".to_string(), + ); + let res = query_type.get_track_source_and_metadata().await; + assert!(res.is_ok()); + } + + #[tokio::test] + async fn test_get_track_source_and_metadata_keyword_list() { + let query_type = QueryType::KeywordList(vec!["hello".to_string(), "world".to_string()]); + let res = query_type.get_track_source_and_metadata().await; + match res { + Ok(_) => assert!(true), + Err(e) => { + let phrase = "Your IP is likely being blocked by Youtube"; + assert!(e.to_string().contains(phrase)); + }, + } + } + + #[tokio::test] + async fn test_build_query_aux_metadata() { + let aux_metadata = AuxMetadata { + artist: Some("hello".to_string()), + track: Some("world".to_string()), + ..Default::default() + }; + let res = build_query_aux_metadata(&aux_metadata); + assert_eq!(res, "world - hello"); + } + + #[tokio::test] + async fn test_video_info_to_source_and_metadata() { + let client = reqwest::Client::new(); + let url = "https://www.youtube.com/watch?v=6n3pFFPSlW4".to_string(); + let res = video_info_to_source_and_metadata(client, url).await; + assert!(res.is_ok()); + } +} diff --git a/crack-core/src/sources/ytdl.rs b/crack-core/src/sources/ytdl.rs index 57580b814..94918eff6 100644 --- a/crack-core/src/sources/ytdl.rs +++ b/crack-core/src/sources/ytdl.rs @@ -1,7 +1,10 @@ use crate::errors::CrackedError; use std::fmt::Display; use tokio::process::Command; +use tokio::runtime::Handle; +use once_cell::sync::Lazy; // 1.5.2 +pub static HANDLE: Lazy>> = Lazy::new(Default::default); const YOUTUBE_DL_COMMAND: &str = "yt-dlp"; #[derive(Clone, Debug)] diff --git a/crack-core/src/utils.rs b/crack-core/src/utils.rs index 5869bd666..aa3495920 100644 --- a/crack-core/src/utils.rs +++ b/crack-core/src/utils.rs @@ -1,11 +1,11 @@ #[cfg(feature = "crack-metrics")] use crate::metrics::COMMAND_EXECUTIONS; use crate::{ - commands::{music::doplay::RequestingUser, MyAuxMetadata}, + commands::{music::doplay::RequestingUser, play_utils::QueryType, MyAuxMetadata}, db::Playlist, - interface::create_now_playing_embed, - interface::{build_nav_btns, requesting_user_to_string}, + guild::settings::DEFAULT_PREMIUM, messaging::{ + interface::{create_nav_btns, create_now_playing_embed, requesting_user_to_string}, message::CrackedMessage, messages::{ INVITE_LINK_TEXT_SHORT, INVITE_URL, PLAYLIST_EMPTY, PLAYLIST_LIST_EMPTY, QUEUE_PAGE, @@ -15,10 +15,12 @@ use crate::{ Context as CrackContext, CrackedError, Data, Error, }; use ::serenity::{ - all::{ChannelId, GuildId, Interaction, UserId}, - builder::CreateEmbed, + all::{ + CacheHttp, ChannelId, ComponentInteractionDataKind, CreateSelectMenu, CreateSelectMenuKind, + CreateSelectMenuOption, EmbedField, GuildId, Interaction, UserId, + }, builder::{ - CreateEmbedAuthor, CreateEmbedFooter, CreateInteractionResponse, + CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateInteractionResponse, CreateInteractionResponseMessage, EditInteractionResponse, EditMessage, }, futures::StreamExt, @@ -36,6 +38,7 @@ use songbird::{input::AuxMetadata, tracks::TrackHandle}; use std::sync::Arc; use std::{ cmp::{max, min}, + collections::HashMap, fmt::Write, ops::Add, time::Duration, @@ -48,6 +51,27 @@ use songbird::Call; pub const EMBED_PAGE_SIZE: usize = 6; +pub fn interaction_to_guild_id(interaction: &Interaction) -> Option { + match interaction { + Interaction::Command(int) => int.guild_id, + Interaction::Component(int) => int.guild_id, + Interaction::Modal(int) => int.guild_id, + Interaction::Autocomplete(int) => int.guild_id, + Interaction::Ping(_) => None, + _ => None, + } +} + +/// Convert a duration to a string. +pub fn duration_to_string(duration: Duration) -> String { + let mut secs = duration.as_secs(); + let hours = secs / 3600; + secs %= 3600; + let minutes = secs / 60; + secs %= 60; + format!("{:02}:{:02}:{:02}", hours, minutes, secs) +} + /// Create and sends an log message as an embed. /// FIXME: The avatar_url won't always be available. How do we best handle this? pub async fn build_log_embed( @@ -90,7 +114,7 @@ pub async fn build_log_embed_thumb( pub async fn send_log_embed_thumb( guild_name: &str, channel: &serenity::ChannelId, - http: &Arc, + cache_http: &impl CacheHttp, id: &str, title: &str, description: &str, @@ -99,7 +123,7 @@ pub async fn send_log_embed_thumb( let embed = build_log_embed_thumb(guild_name, title, id, description, avatar_url).await?; channel - .send_message(http, CreateMessage::new().embed(embed)) + .send_message(cache_http, CreateMessage::new().embed(embed)) .await .map_err(Into::into) } @@ -108,7 +132,7 @@ pub async fn send_log_embed_thumb( #[cfg(not(tarpaulin_include))] pub async fn send_log_embed( channel: &serenity::ChannelId, - http: &Arc, + http: &impl CacheHttp, title: &str, description: &str, avatar_url: &str, @@ -121,34 +145,6 @@ pub async fn send_log_embed( .map_err(Into::into) } -/// Parameter structure for functions that send messages to a channel. -pub struct SendMessageParams { - pub channel: ChannelId, - // pub http: &Arc, - pub as_embed: bool, - pub ephemeral: bool, - pub reply: bool, - pub msg: CrackedMessage, -} - -/// Sends a message to a channel. -#[cfg(not(tarpaulin_include))] -pub async fn send_channel_message( - http: Arc<&Http>, - params: SendMessageParams, -) -> Result { - let channel = params.channel; - // let http = params.http; - let content = format!("{}", params.msg); - let msg = if params.as_embed { - let embed = CreateEmbed::default().description(content); - CreateMessage::new().add_embed(embed) - } else { - CreateMessage::new().content(content) - }; - channel.send_message(http, msg).await.map_err(Into::into) -} - /// Creates an embed from a CrackedMessage and sends it as an embed. #[cfg(not(tarpaulin_include))] pub async fn send_response_poise( @@ -156,8 +152,16 @@ pub async fn send_response_poise( message: CrackedMessage, as_embed: bool, ) -> Result { + use ::serenity::all::Colour; + + let color = match message { + CrackedMessage::CrackedError(_) => Colour::RED, + _ => Colour::BLUE, + }; if as_embed { - let embed = CreateEmbed::default().description(format!("{message}")); + let embed = CreateEmbed::default() + .color(color) + .description(format!("{message}")); send_embed_response_poise(ctx, embed).await } else { send_nonembed_response_poise(ctx, format!("{message}")).await @@ -170,9 +174,7 @@ pub async fn send_response_poise_text( ctx: CrackContext<'_>, message: CrackedMessage, ) -> Result { - let message_str = format!("{message}"); - - send_embed_response_str(ctx, message_str).await + send_response_poise(ctx, message, false).await } /// Create an embed to send as a response. @@ -204,15 +206,13 @@ pub async fn edit_response_poise( let embed = CreateEmbed::default().description(format!("{message}")); match get_interaction_new(ctx) { - Some(interaction) => { - edit_embed_response(&ctx.serenity_context().http, &interaction, embed).await - }, + Some(interaction) => edit_embed_response(&ctx, &interaction, embed).await, None => send_embed_response_poise(ctx, embed).await, } } pub async fn edit_response( - http: &Arc, + http: &impl CacheHttp, interaction: &CommandOrMessageInteraction, message: CrackedMessage, ) -> Result { @@ -221,7 +221,7 @@ pub async fn edit_response( } pub async fn edit_response_text( - http: &Arc, + http: &impl CacheHttp, interaction: &CommandOrMessageInteraction, content: &str, ) -> Result { @@ -229,24 +229,6 @@ pub async fn edit_response_text( edit_embed_response(http, interaction, embed).await } -/// Sends a reply response as an embed. -#[cfg(not(tarpaulin_include))] -pub async fn send_embed_response_str( - ctx: CrackContext<'_>, - message_str: String, -) -> Result { - ctx.send( - CreateReply::default() - .embed(CreateEmbed::new().description(message_str)) - .reply(true), - ) - .await - .unwrap() - .into_message() - .await - .map_err(Into::into) -} - /// Send the current track information as an ebmed to the given channel. #[cfg(not(tarpaulin_include))] pub async fn send_now_playing( @@ -288,17 +270,150 @@ pub async fn send_now_playing( .map_err(|e| e.into()) } +async fn build_embed_fields(elems: Vec) -> Vec { + tracing::warn!("num elems: {:?}", elems.len()); + let mut fields = vec![]; + // let tmp = "".to_string(); + for elem in elems.into_iter() { + let title = elem.title.unwrap_or_default(); + let link = elem.source_url.unwrap_or_default(); + let duration = elem.duration.unwrap_or_default(); + let elem = format!("({}) - {}", link, duration_to_string(duration)); + fields.push(EmbedField::new(format!("[{}]", title), elem, true)); + } + fields +} + +#[cfg(not(tarpaulin_include))] +/// Interactive youtube search and selection. +pub async fn yt_search_select( + ctx: SerenityContext, + channel_id: ChannelId, + metadata: Vec, +) -> Result { + let res = metadata.iter().map(|x| { + let title = x.title.clone().unwrap_or_default(); + let link = x.source_url.clone().unwrap_or_default(); + let duration = x.duration.unwrap_or_default(); + let elem = format!("{}: {}", duration_to_string(duration), title); + let len = min(elem.len(), 99); + let elem = elem[..len].to_string(); + tracing::warn!("elem: {}", elem); + (elem, link) + }); + let rev_map = res + .clone() + .map(|(elem, link)| (link, elem)) + .collect::>(); + // Ask the user for its favorite animal + let m = channel_id + .send_message( + &ctx, + CreateMessage::new().content("Search results").select_menu( + CreateSelectMenu::new( + "song_select", + CreateSelectMenuKind::String { + options: res + .map(|(x, y)| CreateSelectMenuOption::new(x, y)) + .collect(), + }, + ) + .custom_id("song_select") + .placeholder("Select Song to Play"), + ), + ) + .await?; + + // Wait for the user to make a selection + // This uses a collector to wait for an incoming event without needing to listen for it + // manually in the EventHandler. + let interaction = match m + .await_component_interaction(&ctx.shard) + .timeout(Duration::from_secs(60 * 3)) + .await + { + Some(x) => x, + None => { + m.reply(&ctx, "Timed out").await.unwrap(); + return Err(CrackedError::Other("Timed out").into()); + }, + }; + + // data.values contains the selected value from each select menus. We only have one menu, + // so we retrieve the first + let url = match &interaction.data.kind { + ComponentInteractionDataKind::StringSelect { values } => &values[0], + _ => panic!("unexpected interaction data kind"), + }; + + tracing::error!("url: {}", url); + + let qt = QueryType::VideoLink(url.to_string()); + tracing::error!("url: {:?}", qt); + + // Acknowledge the interaction and edit the message + let res = interaction + .create_response( + &ctx, + CreateInteractionResponse::UpdateMessage( + CreateInteractionResponseMessage::default().content(CrackedMessage::SongQueued { + title: rev_map.get(url).unwrap().to_string(), + url: url.to_owned(), + }), + ), + ) + .await + .map_err(|e| e.into()) + .map(|_| qt); + + m.delete(&ctx).await.unwrap(); + res +} + +/// Send the search results to the user. +pub async fn send_search_response( + ctx: CrackContext<'_>, + guild_id: GuildId, + user_id: UserId, + query: String, + res: Vec, +) -> Result { + use poise::serenity_prelude::Mentionable; + let author = ctx.author_member().await.unwrap(); + let name = if DEFAULT_PREMIUM { + author.mention().to_string() + } else { + author.display_name().to_string() + }; + + let now_time_str = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + let fields = build_embed_fields(res).await; + let author = CreateEmbedAuthor::new(name); + let title = format!("Search results for: {}", query); + let footer = CreateEmbedFooter::new(format!("{} * {} * {}", user_id, guild_id, now_time_str)); + let embed = CreateEmbed::new() + .author(author) + .title(title) + .footer(footer) + .fields(fields.into_iter().map(|f| (f.name, f.value, f.inline))); + + send_embed_response_poise(ctx, embed).await +} + /// Sends a reply response with an embed. #[cfg(not(tarpaulin_include))] pub async fn send_embed_response_poise( ctx: CrackContext<'_>, embed: CreateEmbed, ) -> Result { + let is_prefix = crate::is_prefix(ctx); + let is_ephemeral = !is_prefix; + let is_reply = is_prefix; ctx.send( CreateReply::default() .embed(embed) - .ephemeral(false) - .reply(true), + .ephemeral(is_ephemeral) + .reply(is_reply), ) .await? .into_message() @@ -328,12 +443,7 @@ pub async fn send_embed_response_prefix( ctx: CrackContext<'_>, embed: CreateEmbed, ) -> Result { - ctx.send(CreateReply::default().embed(embed)) - .await - .unwrap() - .into_message() - .await - .map_err(Into::into) + send_embed_response_poise(ctx, embed).await } pub async fn send_embed_response( @@ -344,8 +454,10 @@ pub async fn send_embed_response( match interaction { CommandOrMessageInteraction::Command(int) => { tracing::warn!("CommandOrMessageInteraction::Command"); - create_response_interaction(&ctx.serenity_context().http, int, embed, false).await + create_response_interaction(&ctx, &Interaction::Command(int.clone()), embed, false) + .await }, + // Under what circusmtances does this get called? CommandOrMessageInteraction::Message(_interaction) => { tracing::warn!("CommandOrMessageInteraction::Message"); ctx.channel_id() @@ -357,7 +469,7 @@ pub async fn send_embed_response( } pub async fn edit_reponse_interaction( - http: &Arc, + http: &impl CacheHttp, interaction: &Interaction, embed: CreateEmbed, ) -> Result { @@ -384,10 +496,10 @@ pub async fn edit_reponse_interaction( } } -/// Create a response to an interaction. +/// Create (and send) a response to an interaction. #[cfg(not(tarpaulin_include))] pub async fn create_response_interaction( - http: &Arc, + cache_http: &impl CacheHttp, interaction: &Interaction, embed: CreateEmbed, _defer: bool, @@ -411,18 +523,18 @@ pub async fn create_response_interaction( let res = CreateInteractionResponse::Message( CreateInteractionResponseMessage::new().embed(embed.clone()), ); - let message = int.get_response(http).await; + let message = int.get_response(cache_http.http()).await; match message { Ok(message) => { message .clone() - .edit(http, EditMessage::default().embed(embed.clone())) + .edit(cache_http, EditMessage::default().embed(embed.clone())) .await?; Ok(message) }, Err(_) => { - int.create_response(http, res).await?; - let message = int.get_response(http).await?; + int.create_response(cache_http, res).await?; + let message = int.get_response(cache_http.http()).await?; Ok(message) }, } @@ -431,13 +543,14 @@ pub async fn create_response_interaction( | Interaction::Component(..) | Interaction::Modal(..) | Interaction::Autocomplete(..) => Err(CrackedError::Other("not implemented")), - _ => todo!(), + _ => unimplemented!(), } } /// Defers a response to an interaction. +/// TODO: use a macro to reduce code here? pub async fn defer_response_interaction( - http: &Arc, + http: impl CacheHttp, interaction: &Interaction, embed: CreateEmbed, ) -> Result<(), CrackedError> { @@ -483,14 +596,35 @@ pub async fn defer_response_interaction( } } +/// Edit the embed response of the given message. +#[cfg(not(tarpaulin_include))] +pub async fn edit_embed_response2( + ctx: CrackContext<'_>, + embed: CreateEmbed, + mut msg: Message, +) -> Result { + match get_interaction(ctx) { + Some(interaction) => interaction + .edit_response(ctx, EditInteractionResponse::new().add_embed(embed)) + .await + .map_err(Into::into), + None => msg + .edit(ctx, EditMessage::new().embed(embed)) + .await + .map(|_| msg) + .map_err(Into::into), + } +} + +/// WHY ARE THERE TWO OF THESE? pub async fn edit_embed_response( - http: &Arc, + http: &impl CacheHttp, interaction: &CommandOrMessageInteraction, embed: CreateEmbed, ) -> Result { match interaction { CommandOrMessageInteraction::Command(int) => { - edit_reponse_interaction(http, int, embed).await + edit_reponse_interaction(http, &Interaction::Command(int.clone()), embed).await }, CommandOrMessageInteraction::Message(msg) => match msg { Some(_msg) => { @@ -527,18 +661,19 @@ pub async fn edit_embed_response_poise( ) -> Result { match get_interaction_new(ctx) { Some(interaction1) => match interaction1 { - CommandOrMessageInteraction::Command(interaction2) => match interaction2 { - Interaction::Command(interaction3) => { - tracing::warn!("CommandInteraction"); - interaction3 - .edit_response( - &ctx.serenity_context().http, - EditInteractionResponse::new().content(" ").embed(embed), - ) - .await - .map_err(Into::into) - }, - _ => Err(CrackedError::Other("not implemented")), + CommandOrMessageInteraction::Command(interaction2) => { + // match interaction2 { + // Interaction::Command(interaction3) => { + // tracing::warn!("CommandInteraction"); + interaction2 + .edit_response( + &ctx.serenity_context().http, + EditInteractionResponse::new().content(" ").embed(embed), + ) + .await + .map_err(Into::into) + // }, + // _ => Err(CrackedError::Other("not implemented")), }, CommandOrMessageInteraction::Message(_) => send_embed_response_poise(ctx, embed).await, }, @@ -670,12 +805,13 @@ pub fn calculate_num_pages(tracks: &[T]) -> usize { max(1, num_pages) } +/// Forget the current cache of queue messages we need to update. pub async fn forget_queue_message( data: &Data, message: &Message, guild_id: GuildId, ) -> Result<(), CrackedError> { - let mut cache_map = data.guild_cache_map.lock().unwrap().clone(); + let mut cache_map = data.guild_cache_map.lock().await; let cache = cache_map .get_mut(&guild_id) @@ -764,7 +900,7 @@ pub async fn create_paged_embed( .description(page_getter(0)) .footer(CreateEmbedFooter::new(format!("Page {}/{}", 1, num_pages))), ) - .components(build_nav_btns(0, num_pages)), + .components(create_nav_btns(0, num_pages)), ) .await?; reply.into_message().await? @@ -801,7 +937,7 @@ pub async fn create_paged_embed( *page_wlock + 1, num_pages )))]) - .components(build_nav_btns(*page_wlock, num_pages)), + .components(create_nav_btns(*page_wlock, num_pages)), ), ) .await?; @@ -949,7 +1085,7 @@ pub fn check_interaction(result: Result<(), Error>) { } pub enum CommandOrMessageInteraction { - Command(Interaction), + Command(CommandInteraction), Message(Option>), } @@ -960,7 +1096,7 @@ pub fn get_interaction(ctx: CrackContext<'_>) -> Option { // CommandOrAutocompleteInteraction::Command(x) => Some(x.clone()), // CommandOrAutocompleteInteraction::Autocomplete(_) => None, // }, - // Context::Prefix(_ctx) => None, //Some(ctx.msg.interaction.clone().into()), + // CrackContext::Prefix(prefix_ctx) => Some(prefix_ctx.msg.interaction.into()), CrackContext::Prefix(_ctx) => None, } } @@ -968,9 +1104,9 @@ pub fn get_interaction(ctx: CrackContext<'_>) -> Option { pub fn get_interaction_new(ctx: CrackContext<'_>) -> Option { match ctx { CrackContext::Application(app_ctx) => { - Some(CommandOrMessageInteraction::Command(Interaction::Command( + Some(CommandOrMessageInteraction::Command( app_ctx.interaction.clone(), - ))) + )) // match app_ctx.interaction { // CommandOrAutocompleteInteraction::Command(x) => Some( // CommandOrMessageInteraction::Command(Interaction::Command(x.clone())), @@ -984,16 +1120,6 @@ pub fn get_interaction_new(ctx: CrackContext<'_>) -> Option) -> Option { -// match ctx { -// Context::Application(app_ctx) => match app_ctx.interaction { -// ApplicationCommandOrMessageInteraction::ApplicationCommand(x) => Some(x.clone().into()), -// ApplicationCommandOrMessageInteraction::Autocomplete(_) => None, -// }, -// Context::Prefix(ctx) => ctx.msg.interaction.clone().map(|x| x.into()), -// } -// } - /// Get the user id from a context. pub fn get_user_id(ctx: &CrackContext) -> serenity::UserId { match ctx { @@ -1002,31 +1128,6 @@ pub fn get_user_id(ctx: &CrackContext) -> serenity::UserId { } } -// /// Get the channel id from a context. -// pub fn get_channel_id(ctx: &CrackContext) -> serenity::ChannelId { -// match ctx { -// CrackContext::Application(ctx) => ctx.interaction.channel_id, -// CrackContext::Prefix(ctx) => ctx.msg.channel_id, -// } -// } - -// pub async fn summon_short(ctx: CrackContext<'_>) -> Result<(), FrameworkError> { -// match ctx { -// CrackContext::Application(_ctx) => { -// tracing::warn!("summoning via slash command"); -// // summon().slash_action.unwrap()(ctx).await -// // FIXME -// Ok(()) -// } -// CrackContext::Prefix(_ctx) => { -// tracing::warn!("summoning via prefix command"); -// // summon().prefix_action.unwrap()(ctx).await -// // FIXME -// Ok(()) -// } -// } -// } - pub async fn handle_error( ctx: CrackContext<'_>, interaction: &CommandOrMessageInteraction, @@ -1039,7 +1140,7 @@ pub async fn handle_error( #[cfg(feature = "crack-metrics")] pub fn count_command(command: &str, is_prefix: bool) { - tracing::warn!("counting command: {}", command); + tracing::warn!("counting command: {}, {}", command, is_prefix); match COMMAND_EXECUTIONS .get_metric_with_label_values(&[command, if is_prefix { "prefix" } else { "slash" }]) { @@ -1050,31 +1151,14 @@ pub fn count_command(command: &str, is_prefix: bool) { tracing::error!("Failed to get metric: {}", e); }, }; - #[cfg(not(feature = "crack-metrics"))] - tracing::warn!("crack-metrics feature not enabled"); } #[cfg(not(feature = "crack-metrics"))] pub fn count_command(command: &str, is_prefix: bool) { - tracing::warn!("counting command: {}, {}", command, is_prefix); -} - -/// Gets the channel id that the bot is currently playing in for a given guild. -pub async fn get_current_voice_channel_id( - ctx: &SerenityContext, - guild_id: serenity::GuildId, -) -> Option { - let manager = songbird::get(ctx) - .await - .expect("Failed to get songbird manager") - .clone(); - - let call_lock = manager.get(guild_id)?; - let call = call_lock.lock().await; - - let channel_id = call.current_channel()?; - let serenity_channel_id = serenity::ChannelId::new(channel_id.0.into()); - - Some(serenity_channel_id) + tracing::warn!( + "crack-metrics feature not enabled!\ncommand: {}, {}", + command, + is_prefix + ); } pub fn get_guild_name(ctx: &SerenityContext, guild_id: serenity::GuildId) -> Option { @@ -1087,7 +1171,7 @@ mod test { use ::serenity::{all::Button, builder::CreateActionRow}; - use crate::interface::build_single_nav_btn; + use crate::messaging::interface::create_single_nav_btn; use super::*; @@ -1122,7 +1206,7 @@ mod test { #[test] fn test_build_single_nav_btn() { - let creat_btn = build_single_nav_btn("<<", true); + let creat_btn = create_single_nav_btn("<<", true); let s = serde_json::to_string_pretty(&creat_btn).unwrap(); println!("s: {}", s); let btn = serde_json::from_str::