From 711cbe7ea8d12c9698ec19e7c66eca02b7a22459 Mon Sep 17 00:00:00 2001 From: Fridella <43757589+fdnt7@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:52:30 +0700 Subject: [PATCH] =?UTF-8?q?=E2=8F=AF=EF=B8=8F=20Playback=20Functionality?= =?UTF-8?q?=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bumped to `0.8.0`, Fixed confirmation cancellation, Added more unit tests * Utilise `LazyLock` & Update Lavalink * Implemented playback commands * Handle player voice connection updates * Handled unrecognised connections, track stuck & exception, fixed `/remove` index correction * Implement response deferrals, Updated dependencies * Fixed youtube source not working * Use lyra-music's twilight, Optimisations & Initial playback controller development --- Cargo.lock | 1013 ++++++++--------- flake.lock | 299 ++++- flake.nix | 2 + lavalink/application.yml | 12 +- lyra/Cargo.toml | 57 +- lyra/src/client.rs | 5 +- lyra/src/command/check.rs | 202 ++-- lyra/src/command/declare.rs | 77 +- lyra/src/command/macros.rs | 137 ++- lyra/src/command/model.rs | 17 +- lyra/src/command/model/ctx.rs | 110 +- lyra/src/command/model/ctx/autocomplete.rs | 12 +- lyra/src/command/model/ctx/command_data.rs | 21 +- lyra/src/command/model/ctx/menu.rs | 20 +- lyra/src/command/model/ctx/message.rs | 81 +- lyra/src/command/model/ctx/modal.rs | 20 +- lyra/src/command/poll.rs | 69 +- lyra/src/command/require.rs | 159 ++- lyra/src/command/util.rs | 62 +- lyra/src/component/config/access.rs | 23 +- lyra/src/component/config/access/clear.rs | 22 +- lyra/src/component/config/access/edit.rs | 12 +- lyra/src/component/config/access/mode.rs | 10 +- lyra/src/component/config/access/view.rs | 2 +- lyra/src/component/config/now_playing.rs | 3 +- lyra/src/component/connection.rs | 62 +- lyra/src/component/connection/join.rs | 113 +- lyra/src/component/connection/leave.rs | 21 +- lyra/src/component/playback.rs | 141 ++- lyra/src/component/playback/back.rs | 43 + lyra/src/component/playback/jump.rs | 27 + lyra/src/component/playback/jump/backward.rs | 58 + lyra/src/component/playback/jump/first.rs | 47 + lyra/src/component/playback/jump/forward.rs | 67 ++ lyra/src/component/playback/jump/to.rs | 121 ++ lyra/src/component/playback/play_pause.rs | 37 + lyra/src/component/playback/restart.rs | 30 + lyra/src/component/playback/seek.rs | 17 + lyra/src/component/playback/seek/backward.rs | 55 + lyra/src/component/playback/seek/forward.rs | 68 ++ lyra/src/component/playback/seek/to.rs | 96 ++ lyra/src/component/playback/skip.rs | 35 + lyra/src/component/queue.rs | 144 ++- lyra/src/component/queue/clear.rs | 22 +- lyra/src/component/queue/fair_queue.rs | 8 +- lyra/src/component/queue/move.rs | 55 +- lyra/src/component/queue/play.rs | 36 +- lyra/src/component/queue/remove.rs | 45 +- lyra/src/component/queue/remove_range.rs | 45 +- lyra/src/component/queue/repeat.rs | 41 +- lyra/src/component/queue/shuffle.rs | 10 +- lyra/src/component/tuning.rs | 12 +- lyra/src/component/tuning/equaliser.rs | 1 + lyra/src/component/tuning/equaliser/preset.rs | 1 + lyra/src/component/tuning/filter/pitch.rs | 4 +- lyra/src/component/tuning/filter/pitch/set.rs | 2 +- lyra/src/component/tuning/speed.rs | 20 +- lyra/src/component/tuning/volume/down.rs | 9 +- lyra/src/component/tuning/volume/set.rs | 8 +- .../component/tuning/volume/toggle_mute.rs | 6 +- lyra/src/component/tuning/volume/up.rs | 22 +- lyra/src/core/const.rs | 91 +- lyra/src/core/model.rs | 57 +- lyra/src/core/model/emoji.rs | 50 + lyra/src/core/model/interaction.rs | 222 +++- lyra/src/core/traced.rs | 10 +- lyra/src/error.rs | 81 +- lyra/src/error/command.rs | 332 +++--- lyra/src/error/command/check.rs | 83 +- lyra/src/error/command/declare.rs | 26 +- lyra/src/error/command/require.rs | 15 + lyra/src/error/command/util.rs | 17 +- lyra/src/error/component.rs | 1 + lyra/src/error/component/connection.rs | 24 +- lyra/src/error/component/playback.rs | 8 + lyra/src/error/component/queue.rs | 16 +- lyra/src/error/gateway.rs | 13 +- lyra/src/error/lavalink.rs | 2 + lyra/src/gateway/guild.rs | 15 +- lyra/src/gateway/interaction.rs | 176 +-- lyra/src/gateway/model.rs | 20 +- lyra/src/gateway/shard.rs | 3 +- lyra/src/gateway/voice.rs | 26 +- lyra/src/lavalink.rs | 8 +- lyra/src/lavalink/model.rs | 191 +++- lyra/src/lavalink/model/connection.rs | 14 +- lyra/src/lavalink/model/pitch.rs | 8 +- lyra/src/lavalink/model/queue.rs | 136 +-- lyra/src/lavalink/model/queue_indexer.rs | 4 +- lyra/src/lavalink/track.rs | 120 +- lyra/src/main.rs | 5 + lyra/src/runner.rs | 40 +- lyra_ext/Cargo.toml | 15 +- lyra_ext/src/as_grapheme.rs | 56 +- lyra_ext/src/image/limit_file_size.rs | 83 +- lyra_ext/src/iter/chunked_range.rs | 360 +++--- lyra_ext/src/iter/multi_interleave.rs | 206 ++-- lyra_ext/src/lib.rs | 3 + lyra_ext/src/num.rs | 13 + lyra_ext/src/pretty/duration_display.rs | 104 +- lyra_ext/src/pretty/flags_display.rs | 10 +- lyra_ext/src/pretty/join.rs | 28 +- lyra_ext/src/pretty/truncate.rs | 81 +- lyra_ext/src/rgb_hex.rs | 54 +- lyra_ext/src/time.rs | 3 +- lyra_ext/src/time/iso8601.rs | 10 + lyra_ext/src/time/rfc3339.rs | 10 - lyra_ext/src/time/track_timestamp.rs | 460 ++++++++ lyra_ext/src/time/unix.rs | 6 +- lyra_proc/Cargo.toml | 11 +- lyra_proc/src/command.rs | 59 +- lyra_proc/src/config_access.rs | 14 +- lyra_proc/src/lib.rs | 9 +- scripts/get-lavalink | 12 + 114 files changed, 4869 insertions(+), 2528 deletions(-) create mode 100644 lyra/src/component/playback/back.rs create mode 100644 lyra/src/component/playback/jump.rs create mode 100644 lyra/src/component/playback/jump/backward.rs create mode 100644 lyra/src/component/playback/jump/first.rs create mode 100644 lyra/src/component/playback/jump/forward.rs create mode 100644 lyra/src/component/playback/jump/to.rs create mode 100644 lyra/src/component/playback/play_pause.rs create mode 100644 lyra/src/component/playback/restart.rs create mode 100644 lyra/src/component/playback/seek.rs create mode 100644 lyra/src/component/playback/seek/backward.rs create mode 100644 lyra/src/component/playback/seek/forward.rs create mode 100644 lyra/src/component/playback/seek/to.rs create mode 100644 lyra/src/component/playback/skip.rs create mode 100644 lyra/src/core/model/emoji.rs create mode 100644 lyra/src/error/command/require.rs create mode 100644 lyra/src/error/component/playback.rs create mode 100644 lyra_ext/src/num.rs create mode 100644 lyra_ext/src/time/iso8601.rs delete mode 100644 lyra_ext/src/time/rfc3339.rs create mode 100644 lyra_ext/src/time/track_timestamp.rs create mode 100755 scripts/get-lavalink diff --git a/Cargo.lock b/Cargo.lock index 538f22b..eca398d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.8.11" @@ -24,7 +30,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", "once_cell", "version_check", "zerocopy", @@ -62,9 +67,9 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" [[package]] name = "approx" @@ -112,7 +117,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -143,9 +148,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -159,22 +164,11 @@ dependencies = [ "generic-array", ] -[[package]] -name = "brotli" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - [[package]] name = "brotli-decompressor" -version = "2.5.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -188,9 +182,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" [[package]] name = "byteorder" @@ -198,17 +192,23 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "camino" -version = "1.1.7" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] @@ -238,13 +238,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.98" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -286,6 +286,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -300,9 +309,9 @@ checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" [[package]] name = "const_panic" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b" +checksum = "7782af8f90fe69a4bb41e460abe1727d493403d8b2cc43201a3a3e906b24379f" dependencies = [ "const_panic_proc_macros", "typewit", @@ -310,13 +319,13 @@ dependencies = [ [[package]] name = "const_panic_proc_macros" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395398edf7e8b0a812e905c150c438520ab077c4f9f931556302c48dd4e8ada9" +checksum = "e15066a3ad67370e5311151e6c2574f51e80bd9a88ad8af5823d4f4ca4a230e0" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.77", "unicode-xid", ] @@ -332,15 +341,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -415,9 +424,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.14.4" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -425,27 +434,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.14.4" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] name = "darling_macro" -version = "0.14.4" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] @@ -461,6 +470,20 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -489,33 +512,33 @@ dependencies = [ [[package]] name = "derive_builder" -version = "0.13.1" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f59169f400d8087f238c5c0c7db6a28af18681717f3b623227d92f397e938c7" +checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.13.1" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ec317cc3e7ef0928b0ca6e4a634a4d6c001672ae210438cf114a83e56b018d" +checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" dependencies = [ "darling", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] name = "derive_builder_macro" -version = "0.13.1" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870368c3fb35b8031abb378861d4460f573b92238ec2152c927a21f77e3e0127" +checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" dependencies = [ "derive_builder_core", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] @@ -550,9 +573,9 @@ dependencies = [ [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ "serde", ] @@ -586,9 +609,14 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.3" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] [[package]] name = "eyre" @@ -608,9 +636,9 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fdeflate" @@ -623,13 +651,13 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -640,7 +668,7 @@ checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -725,7 +753,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -773,20 +801,6 @@ dependencies = [ "thread_local", ] -[[package]] -name = "generator" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186014d53bc231d0090ef8d6f03e0920c54d85a5ed22f4f2f74315ec56cf83fb" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows 0.54.0", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -810,14 +824,14 @@ dependencies = [ [[package]] name = "getset" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" dependencies = [ - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] @@ -838,11 +852,11 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "git2" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", "libgit2-sys", "log", @@ -857,9 +871,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", "bytes", @@ -886,22 +900,13 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ "hashbrown", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.5.0" @@ -960,9 +965,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", @@ -970,12 +975,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http", "http-body", "pin-project-lite", @@ -983,15 +988,15 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -1018,7 +1023,7 @@ dependencies = [ "hyper", "hyper-util", "rustls 0.22.4", - "rustls-native-certs", + "rustls-native-certs 0.7.3", "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", @@ -1027,16 +1032,16 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http", "hyper", "hyper-util", - "rustls 0.23.9", - "rustls-native-certs", + "rustls 0.23.13", + "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -1045,9 +1050,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" dependencies = [ "bytes", "futures-channel", @@ -1081,12 +1086,12 @@ dependencies = [ [[package]] name = "image" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "color_quant", "gif", "num-traits", @@ -1104,23 +1109,14 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.2.6" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -1138,9 +1134,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] @@ -1165,17 +1161,17 @@ dependencies = [ [[package]] name = "lavalink-rs" -version = "0.12.0" -source = "git+https://gitlab.com/vicky5124/lavalink-rs?rev=56fd9191#56fd919119c7ae0ce3172012578ce8afbf79513c" +version = "0.13.0" +source = "git+https://github.com/lyra-music/lavalink-rs?branch=lyra#fb9498b5d54213ece85097bd608e235a7afdb98e" dependencies = [ "arc-swap", "bytes", - "dashmap", + "dashmap 5.5.3", "futures", "http", "http-body-util", "hyper", - "hyper-rustls 0.27.2", + "hyper-rustls 0.27.3", "hyper-util", "lavalink_rs_macros", "oneshot", @@ -1198,29 +1194,29 @@ checksum = "426ef2204d4fa6806fa6a00fdd747f7312645fcd30cf9208a5bdfeec6f3b9a41" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin", ] [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libgit2-sys" -version = "0.16.2+1.7.2" +version = "0.17.0+1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" dependencies = [ "cc", "libc", @@ -1236,9 +1232,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -1247,9 +1243,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.18" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" dependencies = [ "cc", "libc", @@ -1284,40 +1280,26 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" - -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "pin-utils", - "scoped-tls", - "tracing", - "tracing-subscriber", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lyra" -version = "0.7.1" +version = "0.8.0" dependencies = [ "aho-corasick", "anyhow", - "bitflags 2.5.0", + "bitflags 2.6.0", "color-eyre", "const-str", "const_panic", - "dashmap", + "dashmap 6.1.0", "dotenvy", "dotenvy_macro", "futures", "fuzzy-matcher", - "itertools 0.13.0", + "itertools", "lavalink-rs", "linkify", "log", @@ -1350,13 +1332,14 @@ dependencies = [ [[package]] name = "lyra_ext" -version = "0.7.1" +version = "0.8.0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "const-str", - "heck 0.5.0", + "heck", "image", "kmeans_colors", + "mock_instant", "palette", "rayon", "regex", @@ -1367,13 +1350,13 @@ dependencies = [ [[package]] name = "lyra_proc" -version = "0.7.1" +version = "0.8.0" dependencies = [ - "heck 0.4.1", - "itertools 0.12.1", + "heck", + "itertools", "quote", "serde", - "syn 2.0.66", + "syn 2.0.77", "toml", ] @@ -1398,9 +1381,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" @@ -1410,23 +1393,33 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1438,6 +1431,12 @@ dependencies = [ "libm", ] +[[package]] +name = "mock_instant" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcebb6db83796481097dedc7747809243cc81d9ed83e6a938b76d4ea0b249cf" + [[package]] name = "nom" version = "7.1.3" @@ -1520,16 +1519,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_threads" version = "0.1.7" @@ -1556,12 +1545,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oneshot" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071d1cf3298ad8e543dca18217d198cb6a3884443d204757b9624b935ef09fa0" -dependencies = [ - "loom", -] +checksum = "e296cf87e61c9cfc1a61c3c63a0f7f286ed4554e0e22be84e8a38e1d264a2a29" [[package]] name = "openssl-probe" @@ -1610,9 +1596,15 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1631,9 +1623,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1674,7 +1666,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -1726,7 +1718,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.4", ] [[package]] @@ -1737,57 +1729,58 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.21.1", + "toml_edit", ] [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.109", - "version_check", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ + "proc-macro-error-attr2", "proc-macro2", "quote", - "version_check", + "syn 2.0.77", ] [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -1844,32 +1837,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -1883,13 +1867,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -1900,9 +1884,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "relative-path" @@ -1920,7 +1904,7 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] @@ -1947,9 +1931,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682" +checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" dependencies = [ "futures", "futures-timer", @@ -1959,9 +1943,9 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d" +checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" dependencies = [ "cfg-if", "glob", @@ -1971,7 +1955,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.66", + "syn 2.0.77", "unicode-ident", ] @@ -1983,37 +1967,26 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.22.4" @@ -2023,52 +1996,56 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls" -version = "0.23.9" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.2", + "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls-native-certs" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" dependencies = [ - "base64 0.21.7", + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", ] [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64 0.22.1", "rustls-pki-types", @@ -2076,25 +2053,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", @@ -2115,42 +2082,26 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -2159,9 +2110,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -2178,9 +2129,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -2197,22 +2148,23 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -2236,18 +2188,30 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2261,9 +2225,9 @@ dependencies = [ [[package]] name = "sha1_smol" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" [[package]] name = "sha2" @@ -2285,6 +2249,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2330,6 +2300,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -2341,12 +2314,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -2368,20 +2335,19 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.3" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ - "itertools 0.12.1", "nom", "unicode_categories", ] [[package]] name = "sqlx" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2392,11 +2358,10 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" dependencies = [ - "ahash", "atoi", "byteorder", "bytes", @@ -2409,6 +2374,7 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", + "hashbrown", "hashlink", "hex", "indexmap", @@ -2417,8 +2383,8 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", + "rustls 0.23.13", + "rustls-pemfile", "serde", "serde_json", "sha2", @@ -2434,26 +2400,26 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" dependencies = [ "dotenvy", "either", - "heck 0.4.1", + "heck", "hex", "once_cell", "proc-macro2", @@ -2465,7 +2431,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", + "syn 2.0.77", "tempfile", "tokio", "url", @@ -2473,13 +2439,13 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" dependencies = [ "atoi", - "base64 0.21.7", - "bitflags 2.5.0", + "base64 0.22.1", + "bitflags 2.6.0", "byteorder", "bytes", "crc", @@ -2515,13 +2481,13 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" dependencies = [ "atoi", - "base64 0.21.7", - "bitflags 2.5.0", + "base64 0.22.1", + "bitflags 2.6.0", "byteorder", "crc", "dotenvy", @@ -2553,9 +2519,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" dependencies = [ "atoi", "flume", @@ -2568,10 +2534,10 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", "tracing", "url", - "urlencoding", ] [[package]] @@ -2587,15 +2553,15 @@ dependencies = [ [[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 = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -2610,9 +2576,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -2621,48 +2587,49 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.30.12" +version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" dependencies = [ "cfg-if", "core-foundation-sys", "libc", "ntapi", "once_cell", - "windows 0.52.0", + "windows", ] [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -2721,9 +2688,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -2736,31 +2703,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -2780,16 +2746,16 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.9", + "rustls 0.23.13", "rustls-pki-types", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -2805,7 +2771,7 @@ dependencies = [ "futures-util", "log", "rustls 0.22.4", - "rustls-native-certs", + "rustls-native-certs 0.7.3", "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", @@ -2814,9 +2780,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -2827,9 +2793,9 @@ dependencies = [ [[package]] name = "tokio-websockets" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b069bad86dda43d908b4221fe04fe49d2ed8e0a24d319a5c6a8d250e76fe15b" +checksum = "988c6e20955aa5043e0822cb27093ebaabb430a126cda0223824b6d65ea900c1" dependencies = [ "base64 0.21.7", "bytes", @@ -2839,7 +2805,7 @@ dependencies = [ "http", "httparse", "ring", - "rustls-native-certs", + "rustls-native-certs 0.7.3", "rustls-pki-types", "sha1_smol", "simdutf8", @@ -2851,47 +2817,36 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.14", + "toml_edit", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.14" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.12", + "winnow", ] [[package]] @@ -2911,15 +2866,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -2941,7 +2896,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -3023,17 +2978,15 @@ dependencies = [ [[package]] name = "twilight" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f917ea79e3c1cb325a5cd060e8d3b9f6872ae124a3e1e1192c0e17b3b0a20132" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" [[package]] name = "twilight-cache-inmemory" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a997fdd14e32ebbe80ee87887720e756efed0a7dbf97bd1170eb7c9a3956378b" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" dependencies = [ - "bitflags 2.5.0", - "dashmap", + "bitflags 2.6.0", + "dashmap 5.5.3", "serde", "twilight-model", "twilight-util", @@ -3042,10 +2995,9 @@ dependencies = [ [[package]] name = "twilight-gateway" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3edd60e2b95ec0d651c475dacad8a75e4780d900f8f2ea0f4c215182d920fc" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "fastrand", "flate2", "futures-core", @@ -3063,8 +3015,7 @@ dependencies = [ [[package]] name = "twilight-gateway-queue" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d30e99f204d3803b47c679214ba9e9527cfb1dd0b0deccbe821f7e4682d9a2" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" dependencies = [ "tokio", "tracing", @@ -3073,10 +3024,9 @@ dependencies = [ [[package]] name = "twilight-http" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a03a8133c9ae23510aa6bbe81ca7c4d5bc6fc45c52075d350fd0578b71397413" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" dependencies = [ - "brotli", + "brotli-decompressor", "fastrand", "http", "http-body-util", @@ -3096,8 +3046,7 @@ dependencies = [ [[package]] name = "twilight-http-ratelimiting" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f814c51752ba6838d3b32e82638176de9679e9a2a084a65251b7879281bdc6" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" dependencies = [ "tokio", "tracing", @@ -3106,8 +3055,7 @@ dependencies = [ [[package]] name = "twilight-interactions" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc671758050eefe63e2b87f6af2fc1807ac3ac373bbafff1ae1a4cc4af6cb722" +source = "git+https://github.com/lyra-music/twilight-interactions?branch=lyra#068a8459d4529134d4c655d6e823037e6b697d44" dependencies = [ "twilight-interactions-derive", "twilight-model", @@ -3116,19 +3064,17 @@ dependencies = [ [[package]] name = "twilight-interactions-derive" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c07ac7c9d2d086291765ceecafcd52ce6dc33ab0ade02e06509eb416d88470" +source = "git+https://github.com/lyra-music/twilight-interactions?branch=lyra#068a8459d4529134d4c655d6e823037e6b697d44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] name = "twilight-mention" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc35d15fbaf51d2a4a0ca961e746bca9a01f26f338f99d9134c15b6561b4c5b" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" dependencies = [ "twilight-model", ] @@ -3136,10 +3082,9 @@ dependencies = [ [[package]] name = "twilight-model" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eaf296e6aa8784699046bb1ceb33f3c684a59c4657c2b159eb4ec1e1ce44a49" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "serde", "serde-value", "serde_repr", @@ -3149,10 +3094,9 @@ dependencies = [ [[package]] name = "twilight-standby" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e968572b4e0c2b0e7a05b4b31b1cbc767c7ec0df0407410045ae8c267af6e1fd" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" dependencies = [ - "dashmap", + "dashmap 5.5.3", "futures-core", "tokio", "tracing", @@ -3162,8 +3106,7 @@ dependencies = [ [[package]] name = "twilight-util" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8a5a519ea63090a9e009974bf25e8cb3be9284da7248011ed37377d4180295" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" dependencies = [ "twilight-model", "twilight-validate", @@ -3172,8 +3115,7 @@ dependencies = [ [[package]] name = "twilight-validate" version = "0.16.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d71bf2b683421a1674080e821eef108a35cef841ed73562f835bc5cdd478b49" +source = "git+https://github.com/lyra-music/twilight?branch=lyra#f58b5402cc17a53e68312cc5969fde81fdfb006a" dependencies = [ "twilight-model", ] @@ -3207,9 +3149,9 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" @@ -3222,9 +3164,9 @@ dependencies = [ [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" [[package]] name = "unicode-segmentation" @@ -3234,9 +3176,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" [[package]] name = "unicode_categories" @@ -3252,9 +3194,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -3287,9 +3229,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "9.0.0-beta.2" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "107dc53b443fe8cc380798abb75ad6b7038281165109afea1f1b28bb47047ed5" +checksum = "c32e7318e93a9ac53693b6caccfb05ff22e04a44c7cf8a279051f24c09da286f" dependencies = [ "anyhow", "cargo_metadata", @@ -3305,9 +3247,9 @@ dependencies = [ [[package]] name = "vergen-git2" -version = "1.0.0-beta.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8875c5d71074bb67118774e3d795ab6fe77c3ae3161cb54e19104cabc49487f1" +checksum = "a62c52cd2b2b8b7ec75fc20111b3022ac3ff83e4fc14b9497cfcfd39c54f9c67" dependencies = [ "anyhow", "derive_builder", @@ -3320,9 +3262,9 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ebfba72ba904559f25f41ea1512335b5a46459084258cea0857549d9645187" +checksum = "e06bee42361e43b60f363bad49d63798d0f42fb1768091812270eca00c784720" dependencies = [ "anyhow", "derive_builder", @@ -3332,9 +3274,9 @@ dependencies = [ [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "want" @@ -3359,9 +3301,12 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "weezl" @@ -3371,11 +3316,11 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall", "wasite", ] @@ -3407,18 +3352,8 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.5", -] - -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.5", + "windows-core", + "windows-targets 0.52.6", ] [[package]] @@ -3427,26 +3362,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", -] - -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result", - "windows-targets 0.52.5", -] - -[[package]] -name = "windows-result" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" -dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -3464,7 +3380,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -3484,18 +3409,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -3506,9 +3431,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -3518,9 +3443,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -3530,15 +3455,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -3548,9 +3473,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -3560,9 +3485,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -3572,9 +3497,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -3584,46 +3509,38 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ff33f391015ecab21cd092389215eb265ef9496a9a07b6bee7d3529831deda" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -3640,9 +3557,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" dependencies = [ "zune-core", ] diff --git a/flake.lock b/flake.lock index 2b1bf47..a2bc184 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,47 @@ "devenv", "flake-compat" ], + "git-hooks": [ + "devenv", + "pre-commit-hooks" + ], + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1724232775, + "narHash": "sha256-6u2DycIEgrgNYlLxyGqdFVmBNiKIitnQKJ1pbRP5oko=", + "owner": "cachix", + "repo": "cachix", + "rev": "03b6cb3f953097bff378fb8b9ea094bd091a4ec7", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "cachix", + "type": "github" + } + }, + "cachix_2": { + "inputs": { + "devenv": "devenv_3", + "flake-compat": [ + "devenv", + "cachix", + "devenv", + "flake-compat" + ], "nixpkgs": [ + "devenv", + "cachix", "devenv", "nixpkgs" ], "pre-commit-hooks": [ + "devenv", + "cachix", "devenv", "pre-commit-hooks" ] @@ -34,18 +70,18 @@ "inputs": { "cachix": "cachix", "flake-compat": "flake-compat_2", - "nix": "nix_2", + "nix": "nix_3", "nixpkgs": [ "nixpkgs" ], - "pre-commit-hooks": "pre-commit-hooks" + "pre-commit-hooks": "pre-commit-hooks_2" }, "locked": { - "lastModified": 1717917954, - "narHash": "sha256-2p4dWAywA85r/4zDBQt8dLUErIVSitdDMEow5xxpU0g=", + "lastModified": 1726063457, + "narHash": "sha256-VtMnPqbP2MLJSjEqmGEUb+cTG1dthc+Bfch2/McymbI=", "owner": "cachix", "repo": "devenv", - "rev": "8dab95bb60b6ece337450d80cc67515429652876", + "rev": "39bf6ce569103c9390d37322daa59468c31b3ce7", "type": "github" }, "original": { @@ -56,15 +92,53 @@ }, "devenv_2": { "inputs": { + "cachix": "cachix_2", "flake-compat": [ "devenv", "cachix", "flake-compat" ], + "nix": "nix_2", + "nixpkgs": [ + "devenv", + "cachix", + "nixpkgs" + ], + "pre-commit-hooks": [ + "devenv", + "cachix", + "git-hooks" + ] + }, + "locked": { + "lastModified": 1723156315, + "narHash": "sha256-0JrfahRMJ37Rf1i0iOOn+8Z4CLvbcGNwa2ChOAVrp/8=", + "owner": "cachix", + "repo": "devenv", + "rev": "ff5eb4f2accbcda963af67f1a1159e3f6c7f5f91", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "devenv_3": { + "inputs": { + "flake-compat": [ + "devenv", + "cachix", + "devenv", + "cachix", + "flake-compat" + ], "nix": "nix", "nixpkgs": "nixpkgs", "poetry2nix": "poetry2nix", "pre-commit-hooks": [ + "devenv", + "cachix", "devenv", "cachix", "pre-commit-hooks" @@ -93,11 +167,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1717827974, - "narHash": "sha256-ixopuTeTouxqTxfMuzs6IaRttbT8JqRW5C9Q/57WxQw=", + "lastModified": 1726116637, + "narHash": "sha256-tU2GhwU887mPg6C4c2k+CEBAnKY6R0tSeQYtoqjZmLM=", "owner": "nix-community", "repo": "fenix", - "rev": "ab655c627777ab5f9964652fe23bbb1dfbd687a8", + "rev": "96a04a213838c5001619ad57400c5a176fa040b1", "type": "github" }, "original": { @@ -138,6 +212,28 @@ "type": "github" } }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "devenv", + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1712014858, + "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -157,15 +253,12 @@ } }, "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "type": "github" }, "original": { @@ -196,10 +289,28 @@ "type": "github" } }, + "libgit2": { + "flake": false, + "locked": { + "lastModified": 1697646580, + "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", + "owner": "libgit2", + "repo": "libgit2", + "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", + "type": "github" + }, + "original": { + "owner": "libgit2", + "repo": "libgit2", + "type": "github" + } + }, "nix": { "inputs": { "flake-compat": "flake-compat", "nixpkgs": [ + "devenv", + "cachix", "devenv", "cachix", "devenv", @@ -225,6 +336,8 @@ "nix-github-actions": { "inputs": { "nixpkgs": [ + "devenv", + "cachix", "devenv", "cachix", "devenv", @@ -249,10 +362,14 @@ "nix_2": { "inputs": { "flake-compat": [ + "devenv", + "cachix", "devenv", "flake-compat" ], "nixpkgs": [ + "devenv", + "cachix", "devenv", "nixpkgs" ], @@ -273,6 +390,34 @@ "type": "github" } }, + "nix_3": { + "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], + "flake-parts": "flake-parts", + "libgit2": "libgit2", + "nixpkgs": "nixpkgs_2", + "nixpkgs-23-11": "nixpkgs-23-11", + "nixpkgs-regression": "nixpkgs-regression_3", + "pre-commit-hooks": "pre-commit-hooks" + }, + "locked": { + "lastModified": 1725980365, + "narHash": "sha256-uDwWyizzlQ0HFzrhP6rVp2+2NNA+/TM5zT32dR8GUlg=", + "owner": "domenkozar", + "repo": "nix", + "rev": "1e61e9f40673f84c3b02573145492d8af581bec5", + "type": "github" + }, + "original": { + "owner": "domenkozar", + "ref": "devenv-2.24", + "repo": "nix", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1692808169, @@ -289,6 +434,22 @@ "type": "github" } }, + "nixpkgs-23-11": { + "locked": { + "lastModified": 1717159533, + "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", + "type": "github" + } + }, "nixpkgs-regression": { "locked": { "lastModified": 1643052045, @@ -321,23 +482,55 @@ "type": "github" } }, + "nixpkgs-regression_3": { + "locked": { + "lastModified": 1643052045, + "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + } + }, "nixpkgs-stable": { "locked": { - "lastModified": 1710695816, - "narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=", + "lastModified": 1720386169, + "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "614b4613980a522ba49f0d194531beddbb7220d3", + "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-23.11", + "ref": "nixos-24.05", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_2": { + "locked": { + "lastModified": 1717432640, + "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { "locked": { "lastModified": 1716977621, "narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=", @@ -358,6 +551,8 @@ "flake-utils": "flake-utils", "nix-github-actions": "nix-github-actions", "nixpkgs": [ + "devenv", + "cachix", "devenv", "cachix", "devenv", @@ -382,9 +577,44 @@ "inputs": { "flake-compat": [ "devenv", - "flake-compat" + "nix" ], "flake-utils": "flake-utils_2", + "gitignore": [ + "devenv", + "nix" + ], + "nixpkgs": [ + "devenv", + "nix", + "nixpkgs" + ], + "nixpkgs-stable": [ + "devenv", + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1712897695, + "narHash": "sha256-nMirxrGteNAl9sWiOhoN5tIHyjBbVi5e2tgZUgZlK3Y=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "40e6053ecb65fcbf12863338a6dcefb3f55f1bf8", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "pre-commit-hooks_2": { + "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], "gitignore": "gitignore", "nixpkgs": [ "devenv", @@ -393,11 +623,11 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1713775815, - "narHash": "sha256-Wu9cdYTnGQQwtT20QQMg7jzkANKQjwBD9iccfGKkfls=", + "lastModified": 1725513492, + "narHash": "sha256-tyMUA6NgJSvvQuzB7A1Sf8+0XCHyfSPRx/b00o6K0uo=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "2ac4dcbf55ed43f3be0bae15e181f08a57af24a4", + "rev": "7570de7b9b504cfe92025dd1be797bf546f66528", "type": "github" }, "original": { @@ -410,18 +640,18 @@ "inputs": { "devenv": "devenv", "fenix": "fenix", - "nixpkgs": "nixpkgs_2", - "systems": "systems_3" + "nixpkgs": "nixpkgs_3", + "systems": "systems_2" } }, "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1717583671, - "narHash": "sha256-+lRAmz92CNUxorqWusgJbL9VE1eKCnQQojglRemzwkw=", + "lastModified": 1725985110, + "narHash": "sha256-0HKj+JI6rtxaE6Kzcd6HyFNbEFJRsLy5DoNgVF1pyRM=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "48bbdd6a74f3176987d5c809894ac33957000d19", + "rev": "bcc708992104c2059f310fbc3ac00bfc377f9ea8", "type": "github" }, "original": { @@ -460,21 +690,6 @@ "repo": "default", "type": "github" } - }, - "systems_3": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index cc2047d..7680f58 100644 --- a/flake.nix +++ b/flake.nix @@ -51,12 +51,14 @@ act sqlx-cli pgcli + cargo-edit ]; # https://devenv.sh/scripts/ # scripts.hello.exec = ""; enterShell = '' + ./scripts/get-lavalink ''; # https://devenv.sh/tests/ diff --git a/lavalink/application.yml b/lavalink/application.yml index 45faf2d..6e07308 100644 --- a/lavalink/application.yml +++ b/lavalink/application.yml @@ -6,7 +6,11 @@ plugins: allowDirectPlaylistIds: true # Whether just playlist IDs can match. If false, only complete URLs will be loaded. # The clients to use for track loading. See below for a list of valid clients. # Clients are queried in the order they are given (left-to-right) - clients: ["MUSIC", "ANDROID", "WEB"] + clients: + - MUSIC + - ANDROID_TESTSUITE + - WEB + - TVHTML5EMBEDDED lavasrc: providers: # Custom providers for track loading. This is the default - "dzisrc:%ISRC%" # Deezer ISRC provider @@ -29,11 +33,11 @@ plugins: lavalink: plugins: - - dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.0.1" + - dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.2.0" snapshot: false - dependency: "com.github.topi314.lavasearch:lavasearch-plugin:1.0.0" snapshot: false - - dependency: "dev.lavalink.youtube:youtube-plugin:1.1.0" + - dependency: "dev.lavalink.youtube:youtube-plugin:1.7.2" snapshot: false server: sources: @@ -43,7 +47,7 @@ lavalink: twitch: true vimeo: true http: true - local: false + local: false bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU. diff --git a/lyra/Cargo.toml b/lyra/Cargo.toml index 060391b..35663ea 100644 --- a/lyra/Cargo.toml +++ b/lyra/Cargo.toml @@ -2,7 +2,7 @@ name = "lyra" readme = "../README.md" description = "A featureful, self-hostable Discord music bot." -version = "0.7.1" +version = "0.8.0" edition = "2021" license = "GPL-3.0" repository = "https://github.com/lyra-music/lyra" @@ -11,7 +11,7 @@ build = "build.rs" [build-dependencies] anyhow = "1" -vergen-git2 = { version = "1.0.0-beta.2", features = [ +vergen-git2 = { version = "1.0.0", features = [ "build", "cargo", "rustc", @@ -19,75 +19,68 @@ vergen-git2 = { version = "1.0.0-beta.2", features = [ ] } [lints.rust] -# dead_code = "allow" unsafe_op_in_unsafe_fn = "forbid" -# unsafe_code = "forbid" +# dead_code = "allow" [lints.clippy] multiple_unsafe_ops_per_block = "forbid" undocumented_unsafe_blocks = "forbid" enum_glob_use = "forbid" unwrap_used = "forbid" +try_err = "forbid" pedantic = { level = "deny", priority = -1 } nursery = { level = "deny", priority = -1 } -cast_possible_truncation = "allow" -cast_possible_wrap = "allow" -cast_sign_loss = "allow" -cast_precision_loss = "allow" -significant_drop_tightening = { level = "allow", priority = 1 } -module_name_repetitions = "allow" - [dependencies] lyra_proc = { path = "../lyra_proc" } lyra_ext = { path = "../lyra_ext" } paste = "1.0.15" const-str = "0.5.7" -const_panic = { version = "0.2.8", features = ["derive"] } -bitflags = "2.5.0" -dashmap = "5.5.3" +const_panic = { version = "0.2.9", features = ["derive"] } +bitflags = "2.6.0" +dashmap = "6.1.0" dotenvy = "0.15.7" dotenvy_macro = "0.15.7" -thiserror = "1.0.61" +thiserror = "1.0.63" color-eyre = "0.6.3" futures = "0.3.30" -tokio = { version = "1.38.0", features = [ +tokio = { version = "1.40.0", features = [ "sync", "signal", "rt-multi-thread", "macros", ] } -serde = "1.0.203" -serde_json = "1.0.117" -regex = "1.10.4" +serde = "1.0.210" +serde_json = "1.0.128" +regex = "1.10.6" linkify = "0.10.0" fuzzy-matcher = "0.3.7" -log = "0.4.21" +log = "0.4.22" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } rand = "0.8.5" itertools = "0.13.0" rayon = "1.10.0" -sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio-rustls"] } +sqlx = { version = "0.8.2", features = ["postgres", "runtime-tokio-rustls"] } mixbox = "2.0.0" -lavalink-rs = { git = "https://gitlab.com/vicky5124/lavalink-rs", rev = "56fd9191", features = [ +lavalink-rs = { git = "https://github.com/lyra-music/lavalink-rs", branch = "lyra", features = [ "twilight16", ] } aho-corasick = "1.1.3" -twilight = "=0.16.0-rc.1" -twilight-cache-inmemory = { version = "=0.16.0-rc.1", features = [ +twilight = { git = "https://github.com/lyra-music/twilight", branch = "lyra" } +twilight-cache-inmemory = { git = "https://github.com/lyra-music/twilight", branch = "lyra", features = [ "permission-calculator", ] } -twilight-gateway = "=0.16.0-rc.1" -twilight-http = "=0.16.0-rc.1" -twilight-model = "=0.16.0-rc.1" -twilight-standby = "=0.16.0-rc.1" -twilight-validate = "=0.16.0-rc.1" -twilight-mention = "=0.16.0-rc.1" -twilight-util = { version = "=0.16.0-rc.1", features = [ +twilight-gateway = { git = "https://github.com/lyra-music/twilight", branch = "lyra" } +twilight-http = { git = "https://github.com/lyra-music/twilight", branch = "lyra" } +twilight-model = { git = "https://github.com/lyra-music/twilight", branch = "lyra" } +twilight-standby = { git = "https://github.com/lyra-music/twilight", branch = "lyra" } +twilight-validate = { git = "https://github.com/lyra-music/twilight", branch = "lyra" } +twilight-mention = { git = "https://github.com/lyra-music/twilight", branch = "lyra" } +twilight-util = { git = "https://github.com/lyra-music/twilight", branch = "lyra", features = [ "permission-calculator", "builder", ] } -twilight-interactions = "0.16.0-rc.1" +twilight-interactions = { git = "https://github.com/lyra-music/twilight-interactions", branch = "lyra" } diff --git a/lyra/src/client.rs b/lyra/src/client.rs index dc56775..dbe8dd6 100644 --- a/lyra/src/client.rs +++ b/lyra/src/client.rs @@ -7,7 +7,7 @@ fn parse_directive(parsed: &str) -> tracing_subscriber::filter::Directive { } #[tracing::instrument(err)] -pub async fn run() -> Result<(), super::error::RunError> { +pub async fn run() -> Result<(), super::error::Run> { color_eyre::install()?; dotenvy::dotenv()?; @@ -15,8 +15,7 @@ pub async fn run() -> Result<(), super::error::RunError> { .with_env_filter( tracing_subscriber::EnvFilter::builder() .with_default_directive(tracing::level_filters::LevelFilter::INFO.into()) - .from_env_lossy(), // .add_directive(parse_directive("lyra=trace")), - // .add_directive(parse_directive("lavalink_rs=trace")), + .from_env_lossy(), ) .init(); diff --git a/lyra/src/command/check.rs b/lyra/src/command/check.rs index 53c347f..b04e9f3 100644 --- a/lyra/src/command/check.rs +++ b/lyra/src/command/check.rs @@ -32,7 +32,7 @@ use crate::{ self, AlternateVoteResponse, PollResolvableError, SendSupersededWinNoticeError, UserOnlyInError, }, - CacheResult, + Cache, InVoiceWithSomeoneElse as InVoiceWithSomeoneElseError, InVoiceWithoutUser as InVoiceWithoutUserError, NotUsersTrack as NotUsersTrackError, @@ -47,16 +47,16 @@ use crate::{ CorrectTrackInfo, // DelegateMethods, Event, - LavalinkAware, // PlayerAware, QueueItem, }, + LavalinkAware, }; use super::{ model::{GuildCtx, RespondViaMessage}, poll::{self, Resolution as PollResolution, Topic as PollTopic}, - require::{self, someone_else_in, InVoice, PartialInVoice}, + require::{self, someone_else_in, CurrentTrack, InVoice, PartialInVoice}, }; pub const DJ_PERMISSIONS: Permissions = Permissions::MOVE_MEMBERS.union(Permissions::MUTE_MEMBERS); @@ -107,7 +107,7 @@ pub fn user_is_stage_manager( } pub async fn user_allowed_in(ctx: &Ctx) -> Result<(), check::UserAllowedError> { - let Some(weak) = require::guild_weak(ctx).ok() else { + let Some(weak) = require::guild_ref(ctx).ok() else { return Ok(()); }; @@ -146,7 +146,7 @@ pub async fn user_allowed_in(ctx: &Ctx) -> Result<(), check::UserA let user_allowed_to_use_commands = access_calculator_builder.build().await?.calculate(); if !user_allowed_to_use_commands { - Err(UserNotAllowedError)?; + return Err(UserNotAllowedError.into()); } Ok(()) } @@ -171,7 +171,7 @@ pub async fn user_allowed_to_use( let allowed_to_use_channel = access_calculator_builder.build().await?.calculate(); if !allowed_to_use_channel { - Err(UserNotAllowedError)?; + return Err(UserNotAllowedError.into()); }; Ok(()) } @@ -187,77 +187,96 @@ pub enum InVoiceWithUserResultKind { } pub fn noone_else_in( - channel_id: Id, + in_voice: PartialInVoice, ctx: &GuildCtx, ) -> Result<(), check::UserOnlyInError> { if is_user_dj(ctx) { return Ok(()); } - if someone_else_in(channel_id, ctx)? { - Err(InVoiceWithSomeoneElseError(channel_id))?; + if someone_else_in(in_voice.channel_id(), ctx)? { + return Err(InVoiceWithSomeoneElseError(in_voice).into()); } Ok(()) } +pub struct PollStarter(PollStarterInfo); + pub struct PollStarterInfo { topic: PollTopic, error: PollResolvableError, in_voice: PartialInVoice, } -pub enum PollStarter { - NotRequired, - Required(PollStarterInfo), -} - -impl PollStarter { - pub async fn start( - self, - ctx: &mut GuildCtx, - ) -> Result<(), check::HandlePollError> { - if let Self::Required(s) = self { - return handle_poll(s.error, &s.topic, ctx, &s.in_voice).await; - }; - - Ok(()) - } -} +type InVoiceWithUserOnlyResult = Result<(), check::UserOnlyInError>; impl<'a> InVoiceWithUserResult<'a> { - pub fn only(self) -> Result<(), check::UserOnlyInError> { + pub fn only(self) -> InVoiceWithUserOnlyResult { if matches!(self.kind, InVoiceWithUserResultKind::UserIsDj) { return Ok(()); } - let InVoiceWithUserResult { in_voice, .. } = self; - let channel_id = in_voice.channel_id(); - if someone_else_in(channel_id, &in_voice)? { - Err(InVoiceWithSomeoneElseError(channel_id))?; - } - Ok(()) + user_only_in(&self.in_voice) } +} - pub fn only_else_poll(self, topic: PollTopic) -> CacheResult { - let in_voice = From::from(&self.in_voice); - let Err(error) = self.only() else { - return Ok(PollStarter::NotRequired); +pub trait ResolveWithPoll { + type Error; + fn or_else_try_resolve_with(self, topic: PollTopic) + -> Result, Self::Error>; +} + +impl ResolveWithPoll for InVoiceWithUserOnlyResult { + type Error = Cache; + + fn or_else_try_resolve_with( + self, + topic: PollTopic, + ) -> Result, Self::Error> { + let Err(error) = self else { + return Ok(None); }; match error { check::UserOnlyInError::InVoiceWithSomeoneElse(e) => { - Ok(PollStarter::Required(PollStarterInfo { + let in_voice = e.0.clone(); + Ok(Some(PollStarter(PollStarterInfo { topic, error: check::PollResolvableError::InVoiceWithSomeoneElse(e), in_voice, - })) + }))) } - check::UserOnlyInError::Cache(e) => Err(e)?, + check::UserOnlyInError::Cache(e) => Err(e), } } } -pub fn in_voice_with_user( +pub trait StartPoll { + async fn and_then_start( + self, + ctx: &mut GuildCtx, + ) -> Result<(), check::HandlePollError> + where + Self: Sized; +} + +impl StartPoll for Option { + async fn and_then_start( + self, + ctx: &mut GuildCtx, + ) -> Result<(), check::HandlePollError> + where + Self: Sized, + { + let Some(PollStarter(info)) = self else { + return Ok(()); + }; + + handle_poll(info.error, &info.topic, ctx, &info.in_voice).await + } +} + +pub fn user_in( in_voice: InVoice<'_>, ) -> Result, InVoiceWithoutUserError> { if is_user_dj(&in_voice) { @@ -280,34 +299,25 @@ pub fn in_voice_with_user( }) } -pub fn all_users_track( - positions: impl Iterator, - in_voice_with_user: InVoiceWithUserResult, - queue: &lavalink::Queue, - ctx: &impl AuthorIdAware, -) -> Result<(), check::UsersTrackError> { - if let (Some((position, track)), Err(e)) = ( - positions - .map(|p| (p, &queue[p.get() - 1])) - .find(|(_, t)| t.requester() != ctx.author_id()), - in_voice_with_user.only(), - ) { - return Err(impl_users_track(position, track, e)); +fn user_only_in(in_voice: &InVoice<'_>) -> InVoiceWithUserOnlyResult { + let channel_id = in_voice.channel_id(); + if someone_else_in(channel_id, in_voice)? { + return Err(InVoiceWithSomeoneElseError(PartialInVoice::from(in_voice)).into()); } Ok(()) } fn impl_users_track( - position: NonZeroUsize, track: &QueueItem, + position: NonZeroUsize, user_only_in: UserOnlyInError, ) -> check::UsersTrackError { let channel_id = match user_only_in { - check::UserOnlyInError::InVoiceWithSomeoneElse(e) => e.0, + check::UserOnlyInError::InVoiceWithSomeoneElse(e) => e.0.channel_id(), check::UserOnlyInError::Cache(e) => return e.into(), }; - let title = track.track().info.corrected_title().into(); + let title = track.data().info.corrected_title().into(); let requester = track.requester(); NotUsersTrackError { @@ -319,22 +329,50 @@ fn impl_users_track( .into() } -pub fn users_track( +pub fn track_is_users( + track: &QueueItem, position: NonZeroUsize, in_voice_with_user: InVoiceWithUserResult, - queue: &lavalink::Queue, - ctx: &impl AuthorIdAware, ) -> Result<(), check::UsersTrackError> { - if let Err(e) = in_voice_with_user.only() { - let track = &queue[position.get() - 1]; - if track.requester() != ctx.author_id() { - return Err(impl_users_track(position, track, e)); + let author_id = in_voice_with_user.in_voice.author_id; + if let Err(user_only_in) = in_voice_with_user.only() { + if track.requester() != author_id { + return Err(impl_users_track(track, position, user_only_in)); } } Ok(()) } +pub fn current_track_is_users( + current_track: &CurrentTrack, + in_voice_with_user: InVoiceWithUserResult, +) -> Result<(), check::UsersTrackError> { + track_is_users( + current_track.track, + current_track.position, + in_voice_with_user, + ) +} + +pub fn all_users_track( + queue: &lavalink::Queue, + positions: impl Iterator, + in_voice_with_user: InVoiceWithUserResult, +) -> Result<(), check::UsersTrackError> { + let author_id = in_voice_with_user.in_voice.author_id; + if let (Some((position, track)), Err(user_only_in)) = ( + positions + .map(|p| (p, &queue[p])) + .find(|(_, t)| t.requester() != author_id), + in_voice_with_user.only(), + ) { + return Err(impl_users_track(track, position, user_only_in)); + } + + Ok(()) +} + // async fn currently_playing(ctx: &Ctx) -> Result { // let guild_id = ctx.guild_id(); // let lavalink = ctx.lavalink(); @@ -721,10 +759,11 @@ async fn handle_poll( if is_user_dj(ctx) { connection.dispatch(Event::AlternateVoteDjCast); - Err(check::AnotherPollOngoingError { + return Err(check::AnotherPollOngoingError { message: message.clone(), alternate_vote: Some(AlternateVoteResponse::DjCasted), - })?; + } + .into()); } connection.dispatch(Event::AlternateVoteCast(ctx.author_id().into())); @@ -739,25 +778,29 @@ async fn handle_poll( .await? { if let Event::AlternateVoteCastedAlready(casted) = event { - Err(check::AnotherPollOngoingError { + return Err(check::AnotherPollOngoingError { message: message.clone(), alternate_vote: Some(AlternateVoteResponse::CastedAlready(casted)), - })?; + } + .into()); } - Err(check::AnotherPollOngoingError { + return Err(check::AnotherPollOngoingError { message: message.clone(), alternate_vote: Some(AlternateVoteResponse::CastDenied), - })?; + } + .into()); } - Err(check::AnotherPollOngoingError { + return Err(check::AnotherPollOngoingError { message: message.clone(), alternate_vote: Some(AlternateVoteResponse::Casted), - })?; + } + .into()); } - Err(check::AnotherPollOngoingError { + return Err(check::AnotherPollOngoingError { message, alternate_vote: None, - })?; + } + .into()); } drop(connection); @@ -768,12 +811,14 @@ async fn handle_poll( PollResolution::UnanimousLoss => Err(check::PollLossError { source: error, kind: check::PollLossErrorKind::UnanimousLoss, - })?, + } + .into()), PollResolution::TimedOut => Err(check::PollLossError { source: error, kind: check::PollLossErrorKind::TimedOut, - })?, - PollResolution::Voided(e) => Err(check::PollVoidedError(e))?, + } + .into()), + PollResolution::Voided(e) => Err(check::PollVoidedError(e).into()), PollResolution::SupersededWinViaDj => { traced::tokio_spawn(send_superseded_win_notice( ctx.interaction_token().to_owned(), @@ -784,7 +829,8 @@ async fn handle_poll( PollResolution::SupersededLossViaDj => Err(check::PollLossError { source: error, kind: check::PollLossErrorKind::SupersededLossViaDj, - })?, + } + .into()), } } diff --git a/lyra/src/command/declare.rs b/lyra/src/command/declare.rs index c57178c..733f06f 100644 --- a/lyra/src/command/declare.rs +++ b/lyra/src/command/declare.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, sync::OnceLock}; +use std::{ + collections::HashMap, + sync::{LazyLock, OnceLock}, +}; use twilight_interactions::command::{CommandModel, CreateCommand}; use twilight_model::application::{ @@ -15,6 +18,7 @@ use crate::{ config::Config, connection::{Join, Leave}, misc::Ping, + playback::{Back, Jump, JumpAutocomplete, PlayPause, Restart, Seek, Skip}, queue::{ AddToQueue, Clear, FairQueue, Move, MoveAutocomplete, Play, PlayAutocomplete, PlayFile, Remove, RemoveAutocomplete, RemoveRange, RemoveRangeAutocomplete, Repeat, Shuffle, @@ -37,36 +41,29 @@ macro_rules! declare_slash_commands { } } - fn slash_commands_map() -> &'static SlashCommandMap { - static SLASH_COMMANDS_MAP: OnceLock = OnceLock::new(); - SLASH_COMMANDS_MAP.get_or_init(|| { - ::paste::paste! { - SlashCommandMap { - $([<_ $raw_cmd:snake>]: <$raw_cmd>::create_command().into(),)* - } + static SLASH_COMMANDS_MAP: LazyLock = LazyLock::new(|| { + ::paste::paste! { + SlashCommandMap { + $([<_ $raw_cmd:snake>]: <$raw_cmd>::create_command().into(),)* } - }) - } + } + }); type SlashCommands = [Command; count!($($raw_cmd)*)]; - pub fn slash_commands() -> &'static SlashCommands { - static SLASH_COMMANDS: OnceLock = OnceLock::new(); - SLASH_COMMANDS.get_or_init(|| { - let map = slash_commands_map(); - ::paste::paste! { - [$(map.[<_ $raw_cmd:snake>].clone(),)*] - } - }) - } + pub static SLASH_COMMANDS: LazyLock = LazyLock::new(|| { + ::paste::paste! { + [$(SLASH_COMMANDS_MAP.[<_ $raw_cmd:snake>].clone(),)*] + } + }); - pub static POPULATED_COMMANDS_MAP: OnceLock, Command>> = OnceLock::new(); + pub static POPULATED_COMMANDS_MAP: OnceLock> = OnceLock::new(); $( impl CommandInfoAware for $raw_cmd { fn name() -> &'static str { ::paste::paste! { - &slash_commands_map().[<_ $raw_cmd:snake>].name + &SLASH_COMMANDS_MAP.[<_ $raw_cmd:snake>].name } } } @@ -100,34 +97,27 @@ macro_rules! declare_message_commands { } } - fn message_commands_map() -> &'static MessageCommandMap { - static MESSAGE_COMMANDS_MAP: OnceLock = OnceLock::new(); - MESSAGE_COMMANDS_MAP.get_or_init(|| { - ::paste::paste! { - MessageCommandMap { - $([<_ $raw_cmd:snake>]: <$raw_cmd>::create_command().into(),)* - } + static MESSAGE_COMMANDS_MAP: LazyLock = LazyLock::new(|| { + ::paste::paste! { + MessageCommandMap { + $([<_ $raw_cmd:snake>]: <$raw_cmd>::create_command().into(),)* } - }) - } + } + }); type MessageCommands = [Command; count!($($raw_cmd)*)]; - pub fn message_commands() -> &'static MessageCommands { - static MESSAGE_COMMANDS: OnceLock = OnceLock::new(); - MESSAGE_COMMANDS.get_or_init(|| { - let map = message_commands_map(); - ::paste::paste! { - [$(map.[<_ $raw_cmd:snake>].clone(),)*] - } - }) - } + pub static MESSAGE_COMMANDS: LazyLock = LazyLock::new(|| { + ::paste::paste! { + [$(MESSAGE_COMMANDS_MAP.[<_ $raw_cmd:snake>].clone(),)*] + } + }); $( impl CommandInfoAware for $raw_cmd { fn name() -> &'static str { ::paste::paste! { - &message_commands_map().[<_ $raw_cmd:snake>].name + &MESSAGE_COMMANDS_MAP.[<_ $raw_cmd:snake>].name } } } @@ -191,6 +181,12 @@ declare_slash_commands![ Filter, Speed, Equaliser, + PlayPause, + Seek, + Restart, + Jump, + Skip, + Back, ]; declare_message_commands![AddToQueue,]; @@ -199,4 +195,5 @@ declare_autocomplete![ Remove => RemoveAutocomplete, RemoveRange => RemoveRangeAutocomplete, Move => MoveAutocomplete, + Jump => JumpAutocomplete, ]; diff --git a/lyra/src/command/macros.rs b/lyra/src/command/macros.rs index ffe4057..27a7804 100644 --- a/lyra/src/command/macros.rs +++ b/lyra/src/command/macros.rs @@ -1,9 +1,11 @@ macro_rules! out { ($cnt: expr, $ctx: expr) => { + use crate::core::model::AcknowledgementAware; $ctx.respond($cnt).await?; return Ok(()); }; ($cnt: expr, ?$ctx: expr) => { + use crate::core::model::AcknowledgementAware; $ctx.respond($cnt).await?; }; } @@ -20,37 +22,25 @@ macro_rules! out { macro_rules! out_or_fol { ($cnt: expr, $ctx: expr) => { - if $ctx.acknowledged() { - $ctx.followup(&$cnt).await?; - return Ok(()); - } - $ctx.respond($cnt).await?; + use crate::core::model::AcknowledgementAware; + $ctx.respond_or_followup($cnt).await?; return Ok(()); }; ($cnt: expr, ?$ctx: expr) => { - if $ctx.acknowledged() { - $ctx.followup(&$cnt).await?; - } else { - $ctx.respond($cnt).await?; - } + use crate::core::model::AcknowledgementAware; + $ctx.respond_or_followup($cnt).await?; }; } macro_rules! out_or_upd { ($cnt: expr, $ctx: expr) => { - if $ctx.acknowledged() { - $ctx.update_no_components_embeds(&$cnt).await?; - return Ok(()); - } - $ctx.respond($cnt).await?; + use crate::core::model::AcknowledgementAware; + $ctx.respond_or_update($cnt).await?; return Ok(()); }; ($cnt: expr, ?$ctx: expr) => { - if $ctx.acknowledged() { - $ctx.update_no_components_embeds(&$cnt).await?; - } else { - $ctx.respond($cnt).await?; - } + use crate::core::model::AcknowledgementAware; + $ctx.respond_or_update($cnt).await?; }; } @@ -66,42 +56,43 @@ macro_rules! out_upd { macro_rules! hid { ($cnt: expr, $ctx: expr) => { - $ctx.ephem($cnt).await?; + #[allow(unused_imports)] + use crate::core::model::AcknowledgementAware; + $ctx.respond_ephemeral($cnt).await?; return Ok(()); }; ($cnt: expr, ?$ctx: expr) => { - $ctx.ephem($cnt).await?; + #[allow(unused_imports)] + use crate::core::model::AcknowledgementAware; + $ctx.respond_ephemeral($cnt).await?; }; } macro_rules! hid_fol { ($cnt: expr, $ctx: expr) => { - $ctx.followup_ephem(&$cnt).await?; + #[allow(unused_imports)] + use crate::core::model::AcknowledgementAware; + $ctx.followup_ephemeral($cnt).await?; + return Ok(()); + }; + ($cnt: expr, ?$ctx: expr) => {{ + use crate::core::model::AcknowledgementAware; + $ctx.followup_ephemeral($cnt).await? + }}; +} + +macro_rules! hid_or_fol { + ($cnt: expr, $ctx: expr) => { + use crate::core::model::AcknowledgementAware; + $ctx.respond_ephemeral_or_followup($cnt).await?; return Ok(()); }; ($cnt: expr, ?$ctx: expr) => { - $ctx.followup_ephem(&$cnt).await? + use crate::core::model::AcknowledgementAware; + $ctx.respond_ephemeral_or_followup($cnt).await?; }; } -// macro_rules! hid_or_fol { -// ($cnt: expr, $ctx: expr) => { -// if $ctx.acknowledged() { -// $ctx.followup_ephem(&$cnt).await?; -// return Ok(()); -// } -// $ctx.ephem($cnt).await?; -// return Ok(()); -// }; -// ($cnt: expr, ?$ctx: expr) => { -// if $ctx.acknowledged() { -// $ctx.followup_ephem(&$cnt).await?; -// } else { -// $ctx.ephem($cnt).await?; -// } -// }; -// } - macro_rules! generate_hid_variants { ($($name: ident => $emoji: ident),+$(,)?) => { $( @@ -138,30 +129,30 @@ macro_rules! generate_hid_fol_variants { } } -// macro_rules! generate_hid_or_fol_variants { -// ($($name: ident => $emoji: ident),+$(,)?) => { -// $( -// macro_rules! $name { -// ($cnt: expr, $ctx: expr) => { -// use crate::core::consts::exit_code; -// hid_or_fol!(format!("{} {}", exit_code::$emoji, $cnt), $ctx); -// }; -// ($cnt: expr, ?$ctx: expr) => { -// use crate::core::consts::exit_code; -// hid_or_fol!(format!("{} {}", exit_code::$emoji, $cnt), ?$ctx); -// }; -// } -// )+ - -// pub(crate) use {$($name,)+}; -// } -// } +macro_rules! generate_hid_or_fol_variants { + ($($name: ident => $emoji: ident),+$(,)?) => { + $( + macro_rules! $name { + ($cnt: expr, $ctx: expr) => { + use crate::core::r#const::exit_code; + crate::command::macros::hid_or_fol!(format!("{} {}", exit_code::$emoji, $cnt), $ctx); + }; + ($cnt: expr, ?$ctx: expr) => { + use crate::core::r#const::exit_code; + crate::command::macros::hid_or_fol!(format!("{} {}", exit_code::$emoji, $cnt), ?$ctx); + }; + } + )+ + + pub(crate) use {$($name,)+}; + } +} generate_hid_variants! { note => NOTICE, sus => DUBIOUS, caut => WARNING, - what => NOT_FOUND, + // what => NOT_FOUND, bad => INVALID, nope => PROHIBITED, cant => FORBIDDEN, @@ -181,16 +172,16 @@ generate_hid_fol_variants! { // crit_fol => UNKNOWN_ERROR } -// generate_hid_or_fol_variants! { -// note_or_fol => NOTICE, -// dub_or_fol => DUBIOUS, -// caut_or_fol => WARNING, -// miss_or_fol => NOT_FOUND, -// bad_or_fol => INVALID, -// nope_or_fol => PROHIBITED, -// cant_or_fol => FORBIDDEN, -// err_or_fol => KNOWN_ERROR, -// crit_or_fol => UNKNOWN_ERROR -// } +generate_hid_or_fol_variants! { + // note_or_fol => NOTICE, + // dub_or_fol => DUBIOUS, + // caut_or_fol => WARNING, + what_or_fol => NOT_FOUND, + bad_or_fol => INVALID, + nope_or_fol => PROHIBITED, + cant_or_fol => FORBIDDEN, + // err_or_fol => KNOWN_ERROR, + crit_or_fol => UNKNOWN_ERROR +} -pub(crate) use {hid, hid_fol, out, out_or_fol, out_or_upd, out_upd}; +pub(crate) use {hid, hid_fol, hid_or_fol, out, out_or_fol, out_or_upd, out_upd}; diff --git a/lyra/src/command/model.rs b/lyra/src/command/model.rs index 7950aa3..86195af 100644 --- a/lyra/src/command/model.rs +++ b/lyra/src/command/model.rs @@ -10,21 +10,22 @@ use twilight_model::{ channel::Channel, guild::{PartialMember, Permissions}, id::{ - marker::{ChannelMarker, CommandMarker, GenericMarker, UserMarker}, + marker::{ChannelMarker, GenericMarker, UserMarker}, Id, }, - user::User, + user::User as TwilightUser, }; use crate::error::{command::AutocompleteResult, CommandResult}; pub use self::ctx::{ - AutocompleteCtx, CommandDataAware, Ctx, CtxKind, GuildCtx, GuildModalCtx, MessageCtx, - RespondViaMessage, RespondViaModal, SlashCtx, UserCtx, WeakGuildCtx, + Autocomplete as AutocompleteCtx, CommandDataAware, Ctx, Guild as GuildCtx, + GuildModal as GuildModalCtx, GuildRef as GuildCtxRef, Kind as CtxKind, Message as MessageCtx, + RespondViaMessage, RespondViaModal, Slash as SlashCtx, User, }; pub trait NonPingInteraction { - unsafe fn author_unchecked(&self) -> &User; + unsafe fn author_unchecked(&self) -> &TwilightUser; unsafe fn author_id_unchecked(&self) -> Id { // SAFETY: interaction type is not `Ping`, so an author exists let author = unsafe { self.author_unchecked() }; @@ -39,7 +40,7 @@ pub trait NonPingInteraction { } impl NonPingInteraction for Interaction { - unsafe fn author_unchecked(&self) -> &User { + unsafe fn author_unchecked(&self) -> &TwilightUser { // SAFETY: interaction type is not `Ping`, so an author exists unsafe { self.author().unwrap_unchecked() } } @@ -69,7 +70,6 @@ impl GuildInteraction for Interaction { #[derive(Debug)] pub struct PartialCommandData { - pub id: Id, pub name: Arc, pub target_id: Option>, pub resolved: Option, @@ -79,7 +79,6 @@ pub struct PartialCommandData { impl PartialCommandData { pub fn new(data: &CommandData) -> Self { Self { - id: data.id, name: data.name.to_string().into(), target_id: data.target_id, resolved: data.resolved.clone(), @@ -103,7 +102,7 @@ pub trait BotSlashCommand: CommandInfoAware { } pub trait BotUserCommand: CommandInfoAware { - async fn run(ctx: UserCtx) -> CommandResult; + async fn run(ctx: User) -> CommandResult; } pub trait BotMessageCommand: CommandInfoAware { diff --git a/lyra/src/command/model/ctx.rs b/lyra/src/command/model/ctx.rs index 9812b7a..8c7a929 100644 --- a/lyra/src/command/model/ctx.rs +++ b/lyra/src/command/model/ctx.rs @@ -6,6 +6,7 @@ mod modal; use std::{marker::PhantomData, sync::Arc}; +use tokio::sync::oneshot; use twilight_cache_inmemory::{model::CachedMember, InMemoryCache, Reference}; use twilight_gateway::{Latency, MessageSender}; use twilight_http::Client as HttpClient; @@ -14,10 +15,10 @@ use twilight_model::{ gateway::payload::incoming::InteractionCreate, guild::{PartialMember, Permissions}, id::{ - marker::{ChannelMarker, GuildMarker, UserMarker}, + marker::{ChannelMarker, GuildMarker as TwilightGuildMarker, UserMarker}, Id, }, - user::User, + user::User as TwilightUser, }; use crate::{ @@ -30,25 +31,26 @@ use crate::{ command::RespondError, core::DeserializeBodyFromHttpError, Cache, CacheResult, NotInGuild, }, gateway::{GuildIdAware, OptionallyGuildIdAware, SenderAware}, - lavalink::{Lavalink, LavalinkAware, PlayerAware}, + lavalink::Lavalink, + LavalinkAndGuildIdAware, LavalinkAware, }; use super::PartialInteractionData; -use self::modal::ModalMarker; +use self::modal::Marker as ModalMarker; pub use self::{ - autocomplete::AutocompleteCtx, - command_data::CommandDataAware, - menu::{MessageCtx, UserCtx}, - message::RespondViaMessage, - modal::{GuildModalCtx, RespondViaModal}, + autocomplete::Autocomplete, + command_data::Aware as CommandDataAware, + menu::{Message, User}, + message::RespondVia as RespondViaMessage, + modal::{Guild as GuildModal, RespondVia as RespondViaModal}, }; type RespondResult = Result; type UnitRespondResult = RespondResult<()>; -type CachedBotMember<'a> = Reference<'a, (Id, Id), CachedMember>; +type CachedBotMember<'a> = Reference<'a, (Id, Id), CachedMember>; -pub trait CtxKind {} +pub trait Kind {} pub trait AppCtxKind {} @@ -56,38 +58,43 @@ pub struct SlashAppMarker; impl AppCtxKind for SlashAppMarker {} pub struct AppCtxMarker(PhantomData T>); -impl CtxKind for AppCtxMarker {} +impl Kind for AppCtxMarker {} pub type SlashMarker = AppCtxMarker; -pub type SlashCtx = Ctx; -pub type GuildSlashCtx = Ctx; +pub type Slash = Ctx; +pub type GuildSlash = Ctx; pub struct ComponentMarker; -impl CtxKind for ComponentMarker {} +impl Kind for ComponentMarker {} -pub type ComponentCtx = Ctx; +pub type Component = Ctx; -pub trait CtxLocation {} +pub trait Location {} pub struct Unknown; -impl CtxLocation for Unknown {} +impl Location for Unknown {} -pub struct Guild; -impl CtxLocation for Guild {} -pub type GuildCtx = Ctx; +pub struct GuildMarker; +impl Location for GuildMarker {} +pub type Guild = Ctx; -pub struct Ctx { +pub struct Ctx +where + Of: Kind, + In: Location, +{ inner: Box, bot: OwnedBotState, latency: Latency, sender: MessageSender, data: Option, acknowledged: bool, + acknowledgement: Option>, kind: PhantomData Of>, location: PhantomData In>, } -impl TryFrom> for Ctx { +impl TryFrom> for Ctx { type Error = NotInGuild; fn try_from(value: Ctx) -> Result { @@ -99,13 +106,14 @@ impl TryFrom> for Ctx { sender: value.sender, data: value.data, acknowledged: value.acknowledged, + acknowledgement: value.acknowledgement, kind: value.kind, - location: PhantomData:: Guild>, + location: PhantomData:: GuildMarker>, }) } } -impl Ctx { +impl Ctx { pub fn into_modal_interaction(self, inner: Box) -> Ctx { Ctx { inner, @@ -115,6 +123,7 @@ impl Ctx { location: self.location, data: None, acknowledged: false, + acknowledgement: None, kind: PhantomData:: ModalMarker>, } } @@ -123,12 +132,11 @@ impl Ctx { &self.latency } - pub const fn acknowledged(&self) -> bool { - self.acknowledged - } - pub fn acknowledge(&mut self) { self.acknowledged = true; + if let Some(tx) = std::mem::take(&mut self.acknowledgement) { + let _ = tx.send(()); + } } pub fn db(&self) -> &sqlx::Pool { @@ -145,7 +153,7 @@ impl Ctx { unsafe { self.inner.channel_unchecked() } } - pub fn author(&self) -> &User { + pub fn author(&self) -> &TwilightUser { // SAFETY: Interaction type is not `Ping`, so `author()` is present. unsafe { self.inner.author_unchecked() } } @@ -162,7 +170,7 @@ impl Ctx { Ok(self.bot.interaction().await?.interfaces(&self.inner)) } - pub unsafe fn guild_id_unchecked(&self) -> Id { + pub unsafe fn guild_id_unchecked(&self) -> Id { // SAFETY: this interaction was invoked in a guild, // so `self.inner.guild_id` is present unsafe { self.get_guild_id().unwrap_unchecked() } @@ -190,7 +198,7 @@ impl Ctx { } } -impl Ctx { +impl Ctx { pub fn bot_member(&self) -> CacheResult { self.cache() .member(self.guild_id(), self.bot().user_id()) @@ -249,66 +257,66 @@ impl Ctx { } } -impl BotStateAware for Ctx { +impl BotStateAware for Ctx { fn bot(&self) -> &BotState { &self.bot } } -impl OwnedBotStateAware for Ctx { +impl OwnedBotStateAware for Ctx { fn bot_owned(&self) -> Arc { self.bot.clone() } } -impl SenderAware for Ctx { +impl SenderAware for Ctx { fn sender(&self) -> &MessageSender { &self.sender } } -impl CacheAware for Ctx { +impl CacheAware for Ctx { fn cache(&self) -> &InMemoryCache { self.bot.cache() } } -impl HttpAware for Ctx { +impl HttpAware for Ctx { fn http(&self) -> &HttpClient { self.bot.http() } } -impl LavalinkAware for Ctx { +impl LavalinkAware for Ctx { fn lavalink(&self) -> &Lavalink { self.bot.lavalink() } } -impl PlayerAware for GuildCtx {} +impl LavalinkAndGuildIdAware for Guild {} -impl OptionallyGuildIdAware for Ctx { - fn get_guild_id(&self) -> Option> { +impl OptionallyGuildIdAware for Ctx { + fn get_guild_id(&self) -> Option> { self.inner.guild_id } } -impl AuthorIdAware for Ctx { +impl AuthorIdAware for Ctx { fn author_id(&self) -> Id { self.author().id } } -impl GuildIdAware for Ctx { +impl GuildIdAware for Ctx { #[inline] - fn guild_id(&self) -> Id { + fn guild_id(&self) -> Id { // SAFETY: `Ctx<_, Guild>` is proven to be of an interaction that was invoked in a guild, // so `self.guild_id_unchecked()` is safe. unsafe { self.guild_id_unchecked() } } } -impl AuthorPermissionsAware for Ctx { +impl AuthorPermissionsAware for Ctx { fn author_permissions(&self) -> Permissions { // SAFETY: `Ctx<_, Guild>` is proven to be of an interaction that was invoked in a guild, // so `self.author_permissions_unchecked()` is safe. @@ -316,9 +324,9 @@ impl AuthorPermissionsAware for Ctx { } } -pub struct WeakGuildCtx<'a, T: CtxKind>(&'a Ctx); +pub struct GuildRef<'a, T: Kind>(&'a Ctx); -impl<'a, T: CtxKind> TryFrom<&'a Ctx> for WeakGuildCtx<'a, T> { +impl<'a, T: Kind> TryFrom<&'a Ctx> for GuildRef<'a, T> { type Error = NotInGuild; fn try_from(value: &'a Ctx) -> Result { @@ -327,8 +335,8 @@ impl<'a, T: CtxKind> TryFrom<&'a Ctx> for WeakGuildCtx<'a, T> { } } -impl GuildIdAware for WeakGuildCtx<'_, T> { - fn guild_id(&self) -> Id { +impl GuildIdAware for GuildRef<'_, T> { + fn guild_id(&self) -> Id { // SAFETY: `self.0.get_guild_id()` is proven to be present from `Self::try_from`, // proving that this was an interaction invoked in a guild, // so `self.0.guild_id_unchecked()` is safe. @@ -336,7 +344,7 @@ impl GuildIdAware for WeakGuildCtx<'_, T> { } } -impl WeakGuildCtx<'_, T> { +impl GuildRef<'_, T> { pub fn member(&self) -> &PartialMember { // SAFETY: `self.0.get_guild_id()` is proven to be present from `Self::try_from`, // proving that this was an interaction invoked in a guild, @@ -345,7 +353,7 @@ impl WeakGuildCtx<'_, T> { } } -impl AuthorPermissionsAware for WeakGuildCtx<'_, T> { +impl AuthorPermissionsAware for GuildRef<'_, T> { fn author_permissions(&self) -> Permissions { // SAFETY: `self.0.get_guild_id()` is proven to be present from `Self::try_from`, // proving that this was an interaction invoked in a guild, diff --git a/lyra/src/command/model/ctx/autocomplete.rs b/lyra/src/command/model/ctx/autocomplete.rs index 33aa2c5..3b889fb 100644 --- a/lyra/src/command/model/ctx/autocomplete.rs +++ b/lyra/src/command/model/ctx/autocomplete.rs @@ -1,13 +1,13 @@ use twilight_model::application::command::CommandOptionChoice; -use super::{Ctx, CtxKind, CtxLocation, Guild, UnitRespondResult}; +use super::{Ctx, GuildMarker, Kind, Location, UnitRespondResult}; -pub struct AutocompleteMarker; -impl CtxKind for AutocompleteMarker {} -pub type AutocompleteCtx = Ctx; -pub type GuildAutocompleteCtx = Ctx; +pub struct Marker; +impl Kind for Marker {} +pub type Autocomplete = Ctx; +pub type GuildAutocompleteCtx = Ctx; -impl Ctx { +impl Ctx { pub async fn autocomplete( &mut self, choices: impl IntoIterator + Send, diff --git a/lyra/src/command/model/ctx/command_data.rs b/lyra/src/command/model/ctx/command_data.rs index 1da9bc9..9d8f50a 100644 --- a/lyra/src/command/model/ctx/command_data.rs +++ b/lyra/src/command/model/ctx/command_data.rs @@ -1,5 +1,6 @@ use std::{hint::unreachable_unchecked, marker::PhantomData, sync::Arc}; +use tokio::sync::oneshot; use twilight_gateway::{Latency, MessageSender}; use twilight_model::{ application::interaction::application_command::{ @@ -13,21 +14,20 @@ use crate::{ core::model::OwnedBotState, }; -use super::{ - autocomplete::AutocompleteMarker, AppCtxKind, AppCtxMarker, Ctx, CtxKind, CtxLocation, -}; +use super::{autocomplete::Marker, AppCtxKind, AppCtxMarker, Ctx, Kind, Location}; -pub trait CommandDataAware: CtxKind {} -impl CommandDataAware for AppCtxMarker {} -impl CommandDataAware for AutocompleteMarker {} +pub trait Aware: Kind {} +impl Aware for AppCtxMarker {} +impl Aware for Marker {} -impl Ctx { +impl Ctx { pub fn from_partial_data( inner: Box, data: &CommandData, bot: OwnedBotState, latency: Latency, sender: MessageSender, + acknowledgement: oneshot::Sender<()>, ) -> Self { Self { data: Some(PartialInteractionData::Command(PartialCommandData::new( @@ -38,13 +38,14 @@ impl Ctx { latency, sender, acknowledged: false, + acknowledgement: Some(acknowledgement), kind: PhantomData:: T>, location: PhantomData, } } } -impl Ctx { +impl Ctx { pub fn command_data(&self) -> &PartialCommandData { // SAFETY: `self` is `Ctx`, // so `self.data` is present @@ -94,8 +95,4 @@ impl Ctx { .join(" ") .into() } - - pub fn command_mention_full(&self) -> Box { - format!("", self.command_name_full(), self.command_data().id).into() - } } diff --git a/lyra/src/command/model/ctx/menu.rs b/lyra/src/command/model/ctx/menu.rs index bd9b1f3..d4b6480 100644 --- a/lyra/src/command/model/ctx/menu.rs +++ b/lyra/src/command/model/ctx/menu.rs @@ -1,32 +1,32 @@ use twilight_model::{ application::interaction::InteractionDataResolved, - channel::Message, + channel::Message as TwilightMessage, id::{ marker::{ GenericMarker, MessageMarker as TwilightMessageMarker, UserMarker as TwilightUserMarker, }, Id, }, - user::User, + user::User as TwilightUser, }; -use super::{AppCtxKind, AppCtxMarker, Ctx, CtxLocation}; +use super::{AppCtxKind, AppCtxMarker, Ctx, Location}; pub struct UserAppMarker; impl AppCtxKind for UserAppMarker {} pub type UserMarker = AppCtxMarker; -pub type UserCtx = Ctx; +pub type User = Ctx; pub struct MessageAppMarker; impl AppCtxKind for MessageAppMarker {} pub type MessageMarker = AppCtxMarker; -pub type MessageCtx = Ctx; +pub type Message = Ctx; pub trait TargetIdAware: AppCtxKind {} impl TargetIdAware for UserAppMarker {} impl TargetIdAware for MessageAppMarker {} -impl Ctx, U> { +impl Ctx, U> { pub fn target_id(&self) -> Id { // SAFETY: `self` is `Ctx`, // so `self.partial_command_data().target_id` is present @@ -40,13 +40,13 @@ impl Ctx, U> { } } -impl Ctx { +impl Ctx { #[inline] pub fn target_user_id(&self) -> Id { self.target_id().cast() } - pub fn target_user(&self) -> &User { + pub fn target_user(&self) -> &TwilightUser { // SAFETY: `self` is `Ctx`, // so `self.resolved_data().users.get(&self.target_user_id())` is present unsafe { @@ -58,13 +58,13 @@ impl Ctx { } } -impl Ctx { +impl Ctx { #[inline] pub fn target_message_id(&self) -> Id { self.target_id().cast() } - pub fn target_message(&self) -> &Message { + pub fn target_message(&self) -> &TwilightMessage { // SAFETY: `self` is `Ctx`, // so `self.resolved_data().messages.get(&self.target_message_id())` is present unsafe { diff --git a/lyra/src/command/model/ctx/message.rs b/lyra/src/command/model/ctx/message.rs index e509238..9238020 100644 --- a/lyra/src/command/model/ctx/message.rs +++ b/lyra/src/command/model/ctx/message.rs @@ -7,19 +7,18 @@ use twilight_util::builder::InteractionResponseDataBuilder; use crate::{core::model::MessageResponse, error::command::FollowupError}; use super::{ - AppCtxKind, AppCtxMarker, ComponentMarker, Ctx, CtxKind, CtxLocation, ModalMarker, - RespondResult, + AppCtxKind, AppCtxMarker, ComponentMarker, Ctx, Kind, Location, ModalMarker, RespondResult, + UnitRespondResult, }; type MessageRespondResult = RespondResult; -type MessageFollowupResult = Result; -pub trait RespondViaMessage: CtxKind {} -impl RespondViaMessage for AppCtxMarker {} -impl RespondViaMessage for ModalMarker {} -impl RespondViaMessage for ComponentMarker {} +pub trait RespondVia: Kind {} +impl RespondVia for AppCtxMarker {} +impl RespondVia for ModalMarker {} +impl RespondVia for ComponentMarker {} -impl Ctx { +impl Ctx { fn base_response_data_builder() -> InteractionResponseDataBuilder { InteractionResponseDataBuilder::new().allowed_mentions(AllowedMentions::default()) } @@ -33,20 +32,7 @@ impl Ctx { Ok(response?) } - pub async fn respond(&mut self, content: impl Into + Send) -> MessageRespondResult { - let data = Self::base_response_data_builder().content(content).build(); - self.respond_with(Some(data)).await - } - - pub async fn update_no_components_embeds(&mut self, content: &str) -> MessageFollowupResult { - Ok(self - .interface() - .await? - .update_no_components_embeds(content) - .await?) - } - - pub async fn respond_embeds_only( + pub async fn respond_embeds( &mut self, embeds: impl IntoIterator + Send, ) -> MessageRespondResult { @@ -66,7 +52,33 @@ impl Ctx { self.respond_with(Some(data)).await } - pub async fn ephem(&mut self, content: impl Into + Send) -> MessageRespondResult { + pub async fn defer(&mut self) -> UnitRespondResult { + self.acknowledge(); + Ok(self.interface().await?.defer().await?) + } +} + +impl crate::core::model::AcknowledgementAware for Ctx { + type FollowupError = FollowupError; + type RespondError = crate::error::command::RespondError; + type RespondOrFollowupError = crate::error::command::RespondOrFollowupError; + + fn acknowledged(&self) -> bool { + self.acknowledged + } + + async fn respond( + &mut self, + content: impl Into + Send, + ) -> Result { + let data = Self::base_response_data_builder().content(content).build(); + self.respond_with(Some(data)).await + } + + async fn respond_ephemeral( + &mut self, + content: impl Into + Send, + ) -> MessageRespondResult { let data = Self::base_response_data_builder() .content(content) .flags(MessageFlags::EPHEMERAL) @@ -74,11 +86,28 @@ impl Ctx { self.respond_with(Some(data)).await } - pub async fn followup(&self, content: &str) -> MessageFollowupResult { + async fn update( + &self, + content: impl Into + Send, + ) -> Result { + Ok(self + .interface() + .await? + .update_no_components_embeds(content) + .await?) + } + + async fn followup( + &self, + content: impl Into + Send, + ) -> Result { Ok(self.interface().await?.followup(content).await?) } - pub async fn followup_ephem(&self, content: &str) -> MessageFollowupResult { - Ok(self.interface().await?.followup_ephem(content).await?) + async fn followup_ephemeral( + &self, + content: impl Into + Send, + ) -> Result { + Ok(self.interface().await?.followup_ephemeral(content).await?) } } diff --git a/lyra/src/command/model/ctx/modal.rs b/lyra/src/command/model/ctx/modal.rs index 135beed..91611bf 100644 --- a/lyra/src/command/model/ctx/modal.rs +++ b/lyra/src/command/model/ctx/modal.rs @@ -4,19 +4,19 @@ use twilight_model::{ }; use super::{ - AppCtxKind, AppCtxMarker, ComponentMarker, Ctx, CtxKind, CtxLocation, Guild, UnitRespondResult, + AppCtxKind, AppCtxMarker, ComponentMarker, Ctx, GuildMarker, Kind, Location, UnitRespondResult, }; -pub struct ModalMarker; -impl CtxKind for ModalMarker {} -pub type ModalCtx = Ctx; -pub type GuildModalCtx = Ctx; +pub struct Marker; +impl Kind for Marker {} +pub type Modal = Ctx; +pub type Guild = Ctx; -pub trait RespondViaModal: CtxKind {} -impl RespondViaModal for AppCtxMarker {} -impl RespondViaModal for ComponentMarker {} +pub trait RespondVia: Kind {} +impl RespondVia for AppCtxMarker {} +impl RespondVia for ComponentMarker {} -impl Ctx { +impl Ctx { pub fn submit_data(&self) -> &ModalInteractionData { let Some(InteractionData::ModalSubmit(ref data)) = self.inner.data else { // SAFETY: `self` is `Ctx`, @@ -27,7 +27,7 @@ impl Ctx { } } -impl Ctx { +impl Ctx { pub async fn modal( &mut self, custom_id: impl Into + Send, diff --git a/lyra/src/command/poll.rs b/lyra/src/command/poll.rs index 80d992a..7edeb10 100644 --- a/lyra/src/command/poll.rs +++ b/lyra/src/command/poll.rs @@ -15,7 +15,7 @@ use twilight_model::{ application::interaction::{Interaction, InteractionData}, channel::message::{ component::{ActionRow, Button, ButtonStyle}, - Component, Embed, ReactionType, + Component, Embed, EmojiReactionType, }, guild::Permissions, id::{ @@ -41,7 +41,8 @@ use crate::{ Cache as CacheError, }, gateway::GuildIdAware, - lavalink::{Event, EventRecvResult, LavalinkAware}, + lavalink::{Event, EventRecvResult}, + LavalinkAware, }; use super::{ @@ -279,22 +280,24 @@ fn generate_upvote_button_id_and_row() -> (String, Component) { let upvote_button = Component::Button(Button { custom_id: Some(upvote_button_id.clone()), disabled: false, - emoji: Some(ReactionType::Unicode { + emoji: Some(EmojiReactionType::Unicode { name: String::from("➕"), }), label: None, style: ButtonStyle::Primary, url: None, + sku_id: None, }); let downvote_button = Component::Button(Button { custom_id: Some(downvote_button_id), disabled: false, - emoji: Some(ReactionType::Unicode { + emoji: Some(EmojiReactionType::Unicode { name: String::from("➖"), }), label: None, style: ButtonStyle::Danger, url: None, + sku_id: None, }); let row = Component::ActionRow(ActionRow { components: vec![upvote_button, downvote_button], @@ -373,10 +376,10 @@ impl EmbedUpdate<'_> { } } -fn calculate_vote_ratios_and_votes( +fn calculate_votes_and_ratios( votes: &HashMap, Vote>, threshold: usize, -) -> ((usize, usize), (f32, f32, f32)) { +) -> ((usize, usize), (f64, f64, f64)) { let total_votes = votes.len(); let upvotes = votes .values() @@ -385,9 +388,18 @@ fn calculate_vote_ratios_and_votes( .count(); let downvotes = total_votes - upvotes; let votes_left = threshold - total_votes; - let upvote_ratio = upvotes as f32 / threshold as f32; - let downvote_ratio = downvotes as f32 / threshold as f32; - let votes_left_ratio = votes_left as f32 / threshold as f32; + + #[allow(clippy::cast_precision_loss)] + let (threshold_f64, upvotes_f64, downvotes_f64, votes_left_f64) = ( + threshold as f64, + upvotes as f64, + downvotes as f64, + votes_left as f64, + ); + + let upvote_ratio = upvotes_f64 / threshold_f64; + let downvote_ratio = downvotes_f64 / threshold_f64; + let votes_left_ratio = votes_left_f64 / threshold_f64; ( (upvotes, downvotes), @@ -398,8 +410,8 @@ fn calculate_vote_ratios_and_votes( fn calculate_vote_ratios( votes: &HashMap, Vote>, threshold: usize, -) -> (f32, f32, f32) { - calculate_vote_ratios_and_votes(votes, threshold).1 +) -> (f64, f64, f64) { + calculate_votes_and_ratios(votes, threshold).1 } fn generate_embed_colour( @@ -411,11 +423,16 @@ fn generate_embed_colour( let mut z_mix = [0.0; mixbox::LATENT_SIZE]; for (i, z) in z_mix.iter_mut().enumerate() { *z = votes_left_ratio.mul_add( - latent.base[i], - upvote_ratio.mul_add(latent.upvote[i], downvote_ratio * latent.downvote[i]), + f64::from(latent.base[i]), + upvote_ratio.mul_add( + f64::from(latent.upvote[i]), + downvote_ratio * f64::from(latent.downvote[i]), + ), ); } - mixbox::latent_to_rgb(&z_mix) + + #[allow(clippy::cast_possible_truncation)] + mixbox::latent_to_rgb(&z_mix.map(|f| f as f32)) } async fn update_embed( @@ -447,7 +464,12 @@ pub async fn start( let users_in_voice = get_users_in_voice(ctx, in_voice)?; let votes = HashMap::from([(ctx.author_id(), Vote(true))]); - let threshold = ((users_in_voice.len() + 1) as f64 / 2.).round() as usize; + + #[allow(clippy::cast_precision_loss)] + let users_in_voice_plus_1 = (users_in_voice.len() + 1) as f64; + + #[allow(clippy::cast_possible_truncation)] + let threshold = ((users_in_voice_plus_1 / 2.).round() as isize).unsigned_abs(); let embed = generate_embed( topic, @@ -504,7 +526,7 @@ fn calculate_vote_resolution( threshold: usize, ) -> Option { let res = votes.values().copied().map(Vote::value).sum::(); - if res.max(0) as usize == threshold { + if res.unsigned_abs() == threshold { if res.is_positive() { return Some(Resolution::UnanimousWin); } @@ -515,11 +537,16 @@ fn calculate_vote_resolution( fn generate_poll_description(votes: &HashMap, Vote>, threshold: usize) -> String { let ((upvotes, downvotes), (upvote_ratio, downvote_ratio, _)) = - calculate_vote_ratios_and_votes(votes, threshold); - let ratio_bar_size = RATIO_BAR_SIZE as f32; + calculate_votes_and_ratios(votes, threshold); - let upvote_char_n = (upvote_ratio * ratio_bar_size) as usize; - let downvote_char_n = (downvote_ratio * ratio_bar_size) as usize; + #[allow(clippy::cast_precision_loss)] + let ratio_bar_size = RATIO_BAR_SIZE as f64; + + #[allow(clippy::cast_possible_truncation)] + let (upvote_char_n, downvote_char_n) = ( + ((upvote_ratio * ratio_bar_size) as isize).unsigned_abs(), + ((downvote_ratio * ratio_bar_size) as isize).unsigned_abs(), + ); let votes_left_char_n = RATIO_BAR_SIZE - upvote_char_n - downvote_char_n; format!( @@ -630,7 +657,7 @@ async fn wait_for_votes( PollAction::Void(e) => return Ok(Resolution::Voided(e)), }, Ok(Ok(None)) => {} - Ok(Err(e)) => Err(e)?, + Ok(Err(e)) => return Err(e.into()), Err(_) => return Ok(Resolution::TimedOut), } } diff --git a/lyra/src/command/require.rs b/lyra/src/command/require.rs index 85690d1..8249c48 100644 --- a/lyra/src/command/require.rs +++ b/lyra/src/command/require.rs @@ -1,3 +1,5 @@ +use std::{num::NonZeroUsize, time::Duration}; + use lavalink_rs::{ error::LavalinkResult, model::player::Player as PlayerInfo, player_context::PlayerContext, }; @@ -14,54 +16,102 @@ use twilight_model::{ use crate::{ core::model::{AuthorIdAware, AuthorPermissionsAware, CacheAware}, error::{ - command::check, lavalink::NoPlayerError, Cache, CacheResult, InVoiceWithoutSomeoneElse, - NotInGuild, NotInVoice, QueueEmpty, Suppressed, + command::require::{InVoiceWithSomeoneElseError, UnsuppressedError}, + lavalink::NoPlayerError, + Cache, CacheResult, InVoiceWithoutSomeoneElse, NotInGuild, NotInVoice, NotPlaying, + QueueEmpty, Suppressed, }, gateway::GuildIdAware, - lavalink::{PlayerAware, PlayerDataRwLockArc, UnwrappedPlayerData}, + lavalink::{ + DelegateMethods, OwnedPlayerData, PlayerDataRead, PlayerDataWrite, Queue, QueueItem, + UnwrappedData, + }, + LavalinkAndGuildIdAware, }; -use super::model::{Ctx, CtxKind, GuildCtx, WeakGuildCtx}; +use super::model::{Ctx, CtxKind, GuildCtx, GuildCtxRef}; pub fn guild(ctx: Ctx) -> Result, NotInGuild> { GuildCtx::try_from(ctx) } -pub fn guild_weak(ctx: &Ctx) -> Result, NotInGuild> { - WeakGuildCtx::try_from(ctx) +pub fn guild_ref(ctx: &Ctx) -> Result, NotInGuild> { + GuildCtxRef::try_from(ctx) +} + +pub fn player(cx: &impl LavalinkAndGuildIdAware) -> Result { + let context = cx.get_player().ok_or(NoPlayerError)?; + Ok(PlayerInterface { context }) } -pub struct Player { +pub struct PlayerInterface { pub context: PlayerContext, } -impl Player { +impl PlayerInterface { pub async fn info(&self) -> LavalinkResult { self.context.get_player().await } - pub fn data(&self) -> PlayerDataRwLockArc { + pub fn data(&self) -> OwnedPlayerData { self.context.data_unwrapped() } - pub async fn and_queue_not_empty(self) -> Result { - if self.data().read().await.queue().is_empty() { - return Err(QueueEmpty); + pub async fn acquire_advance_lock_and_stop_with(&self, queue: &Queue) -> LavalinkResult<()> { + queue.acquire_advance_lock(); + self.context.stop_now().await?; + Ok(()) + } + + pub async fn update_voice_channel(&self, voice_is_empty: bool) -> LavalinkResult<()> { + let mut update_player = lavalink_rs::model::http::UpdatePlayer { + voice: Some( + self.context + .client + .get_connection_info_traced(self.context.guild_id) + .await?, + ), + ..Default::default() + }; + if voice_is_empty { + update_player.paused = Some(true); + self.data().write().await.set_pause(true); } + self.context.update_player(&update_player, true).await?; + Ok(()) + } - Ok(self) + pub async fn seek_to_with<'data, 'guard>( + &self, + timestamp: Duration, + data_w: &'guard mut PlayerDataWrite<'data>, + ) -> LavalinkResult<()> { + data_w.seek_to(timestamp); + self.context.set_position(timestamp).await?; + Ok(()) + } + + pub async fn set_pause(&self, state: bool) -> LavalinkResult<()> { + let data = self.data(); + let mut data_w = data.write().await; + self.set_pause_with(state, &mut data_w).await } -} -pub fn player(ctx: &impl PlayerAware) -> Result { - let context = ctx.get_player().ok_or(NoPlayerError)?; - Ok(Player { context }) + pub async fn set_pause_with<'data, 'guard>( + &self, + state: bool, + data_w: &'guard mut PlayerDataWrite<'data>, + ) -> LavalinkResult<()> { + data_w.set_pause(state); + self.context.set_pause(state).await?; + Ok(()) + } } pub type CachedVoiceStateRef<'a> = Reference<'a, (Id, Id), CachedVoiceState>; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct InVoiceCachedVoiceState { guild_id: Id, channel_id: Id, @@ -89,16 +139,21 @@ pub struct InVoice<'a> { } #[must_use] +#[derive(Debug, Clone)] pub struct PartialInVoice { state: InVoiceCachedVoiceState, - pub author_id: Id, +} + +impl PartialInVoice { + pub const fn channel_id(&self) -> Id { + self.state.channel_id + } } impl From<&InVoice<'_>> for PartialInVoice { fn from(value: &InVoice<'_>) -> Self { Self { state: value.state.clone(), - author_id: value.author_id, } } } @@ -132,27 +187,27 @@ impl<'a> InVoice<'a> { self.state.channel_id } - pub fn and_unsuppressed(self) -> Result { + pub fn and_unsuppressed(self) -> Result { let state = &self.state; - let voice_state_channel = self.cache.channel(state.channel_id).ok_or(Cache)?; + let voice_state_channel_kind = self.cache.channel(state.channel_id).ok_or(Cache)?.kind; if state.mute { - Err(Suppressed::Muted)?; + return Err(Suppressed::Muted.into()); } let speaker_in_stage = - state.suppress && matches!(voice_state_channel.kind, ChannelType::GuildStageVoice); + state.suppress && matches!(voice_state_channel_kind, ChannelType::GuildStageVoice); if speaker_in_stage { - Err(Suppressed::NotSpeaker)?; + return Err(Suppressed::NotSpeaker.into()); } Ok(self) } - pub fn and_with_someone_else(self) -> Result { + pub fn and_with_someone_else(self) -> Result { let channel_id = self.channel_id(); if !someone_else_in(channel_id, &self)? { - Err(InVoiceWithoutSomeoneElse(channel_id))?; + return Err(InVoiceWithoutSomeoneElse(channel_id).into()); } Ok(self) } @@ -184,14 +239,14 @@ impl GuildIdAware for InVoice<'_> { pub fn someone_else_in( channel_id: Id, - ctx: &(impl CacheAware + AuthorIdAware), + cx: &(impl CacheAware + AuthorIdAware), ) -> CacheResult { - let cache = ctx.cache(); + let cache = cx.cache(); cache .voice_channel_states(channel_id) .and_then(|states| { for state in states { - if !cache.user(state.user_id())?.bot && state.user_id() != ctx.author_id() { + if !cache.user(state.user_id())?.bot && state.user_id() != cx.author_id() { return Some(true); } } @@ -199,3 +254,47 @@ pub fn someone_else_in( }) .ok_or(Cache) } + +fn impl_queue_not_empty(queue: &Queue) -> Result<(), QueueEmpty> { + if queue.is_empty() { + return Err(QueueEmpty); + } + Ok(()) +} + +pub fn queue_not_empty<'guard, 'data, 'borrow>( + data_r: &'guard PlayerDataRead<'data>, +) -> Result<&'borrow Queue, QueueEmpty> +where + 'guard: 'borrow, + 'data: 'borrow, +{ + let queue = data_r.queue(); + impl_queue_not_empty(queue)?; + Ok(queue) +} + +pub fn queue_not_empty_mut<'guard, 'data, 'borrow>( + data_w: &'guard mut PlayerDataWrite<'data>, +) -> Result<&'borrow mut Queue, QueueEmpty> +where + 'guard: 'borrow, + 'data: 'borrow, +{ + let queue = data_w.queue_mut(); + impl_queue_not_empty(queue)?; + Ok(queue) +} + +pub fn current_track(queue: &Queue) -> Result { + let (current, position) = queue.current_and_position(); + Ok(CurrentTrack { + track: current.ok_or(NotPlaying)?, + position, + }) +} + +pub struct CurrentTrack<'a> { + pub track: &'a QueueItem, + pub position: NonZeroUsize, +} diff --git a/lyra/src/command/util.rs b/lyra/src/command/util.rs index 2894168..9823327 100644 --- a/lyra/src/command/util.rs +++ b/lyra/src/command/util.rs @@ -1,5 +1,5 @@ use lavalink_rs::{error::LavalinkResult, player_context::PlayerContext}; -use lyra_ext::time::unix::unix_time; +use lyra_ext::unix_time; use rand::{distributions::Alphanumeric, Rng}; use twilight_gateway::Event; use twilight_model::{ @@ -36,16 +36,17 @@ use crate::{ }, error::{ command::{ - check::NotSuppressedError, + require::UnsuppressedError, util::{ - AutoJoinOrCheckInVoiceWithUserError, AutoJoinSuppressedError, ConfirmationError, + AutoJoinOrCheckInVoiceWithUserError, AutoJoinSuppressedError, HandleSuppressedAutoJoinError, PromptForConfirmationError, }, }, - Suppressed as SuppressedError, + ConfirmationTimedOut, Suppressed as SuppressedError, }, gateway::GuildIdAware, - lavalink::{DelegateMethods, LavalinkAware}, + lavalink::DelegateMethods, + LavalinkAware, }; pub trait MessageLinkAware { @@ -132,7 +133,8 @@ pub trait AvatarUrlAware { let avatar = self.avatar()?; let ext = if avatar.is_animated() { "gif" } else { "png" }; - format!("{}/avatars/{}/{}.{}", CDN_URL, self.id(), avatar, ext).into() + let formatted = format!("{}/avatars/{}/{}.{}", CDN_URL, self.id(), avatar, ext); + Some(formatted) } } @@ -152,15 +154,15 @@ pub trait GuildAvatarUrlAware { let avatar = self.avatar()?; let ext = if avatar.is_animated() { "gif" } else { "png" }; - format!( + let formatted = format!( "{}/guilds/{}/users/{}/avatars/{}.{}", CDN_URL, guild_id, self.id(), avatar, ext, - ) - .into() + ); + Some(formatted) } } @@ -207,14 +209,14 @@ pub async fn auto_join_or_check_in_voice_with_user_and_check_not_suppressed( ) -> Result<(), AutoJoinOrCheckInVoiceWithUserError> { if let Ok(in_voice) = require::in_voice(ctx) { let in_voice = in_voice.and_unsuppressed()?; - check::in_voice_with_user(in_voice)?; + check::user_in(in_voice)?; return Ok(()); } match auto_join(ctx).await { - Err(e) => Err(e.unflatten_into_auto_join_attempt())?, + Err(e) => Err(e.unflatten_into_auto_join_attempt().into()), Ok(state) => { - let Err(NotSuppressedError::Suppressed(suppressed)) = { + let Err(UnsuppressedError::Suppressed(suppressed)) = { // SAFETY: as `auto_join` was called and ran successfully, // there must now be an active voice connection. unsafe { InVoice::new(state, ctx) } @@ -234,7 +236,7 @@ async fn handle_suppressed_auto_join( ) -> Result<(), HandleSuppressedAutoJoinError> { let bot_user_id = ctx.bot().user_id(); match error { - SuppressedError::Muted => Err(AutoJoinSuppressedError::Muted)?, + SuppressedError::Muted => Err(AutoJoinSuppressedError::Muted.into()), SuppressedError::NotSpeaker => { let bot = ctx.bot_owned(); let wait_for_speaker = @@ -262,15 +264,14 @@ async fn handle_suppressed_auto_join( ?ctx ); let requested_to_speak_message = requested_to_speak.model().await?; - let wait_for_speaker = tokio::time::timeout( - *r#const::misc::wait_for_bot_events_timeout(), - wait_for_speaker, - ); + let wait_for_speaker = + tokio::time::timeout(r#const::misc::WAIT_FOR_BOT_EVENTS_TIMEOUT, wait_for_speaker); if wait_for_speaker.await.is_err() { - Err(AutoJoinSuppressedError::StillNotSpeaker { + return Err(AutoJoinSuppressedError::StillNotSpeaker { last_followup_id: requested_to_speak_message.id, - })?; + } + .into()); } Ok(()) } @@ -279,7 +280,7 @@ async fn handle_suppressed_auto_join( pub async fn prompt_for_confirmation( mut ctx: GuildCtx, -) -> Result { +) -> Result<(GuildModalCtx, bool), PromptForConfirmationError> { let text_input = TextInput { custom_id: String::new(), label: String::from("This is a destructive command. Are you sure?"), @@ -320,31 +321,28 @@ pub async fn prompt_for_confirmation( }); let wait_for_modal_submit = tokio::time::timeout( - *r#const::misc::destructive_command_confirmation_timeout(), + r#const::misc::DESTRUCTIVE_COMMAND_CONFIRMATION_TIMEOUT, wait_for_modal_submit, ) .await; - let modal_ctx = match wait_for_modal_submit { + let ctx_and_confirmed = match wait_for_modal_submit { Ok(Ok(Event::InteractionCreate(interaction))) => { let ctx = ctx.into_modal_interaction(interaction); - if ctx.submit_data().components[0].components[0] + let confirmed = ctx.submit_data().components[0].components[0] .value .as_ref() - .is_some_and(|s| s == "YES") - { - Err(ConfirmationError::Cancelled)?; - } - ctx + .is_some_and(|s| s == "YES"); + (ctx, confirmed) } // SAFETY: the future has been filtered to only match modal submit interaction // so this branch is unreachable Ok(Ok(_)) => unsafe { std::hint::unreachable_unchecked() }, - Ok(Err(e)) => Err(e)?, - Err(_) => Err(ConfirmationError::TimedOut)?, + Ok(Err(e)) => return Err(e.into()), + Err(_) => return Err(ConfirmationTimedOut.into()), }; - Ok(modal_ctx) + Ok(ctx_and_confirmed) } pub async fn auto_new_player(ctx: &GuildCtx) -> LavalinkResult { @@ -353,7 +351,7 @@ pub async fn auto_new_player(ctx: &GuildCtx) -> LavalinkResult player, - None => lavalink.new_player(guild_id).await?, + None => lavalink.new_player(guild_id, ctx.channel_id()).await?, }; Ok(player) diff --git a/lyra/src/component/config/access.rs b/lyra/src/component/config/access.rs index 4e97a51..5f52a2b 100644 --- a/lyra/src/component/config/access.rs +++ b/lyra/src/component/config/access.rs @@ -6,7 +6,9 @@ mod view; use bitflags::bitflags; use const_str::concat as const_str_concat; use itertools::Itertools; -use lyra_ext::{logical_bind::LogicalBind, pretty::flags_display::PrettyFlagsDisplay}; +use lyra_ext::{ + logical_bind::LogicalBind, num::u64_to_i64_truncating, pretty::flags_display::FlagsDisplay, +}; use sqlx::{Pool, Postgres}; use tokio::task::JoinSet; use twilight_interactions::command::{CommandModel, CommandOption, CreateCommand, CreateOption}; @@ -54,7 +56,7 @@ pub struct CalculatorBuilder { impl CalculatorBuilder { pub fn new(guild_id: Id, db: Pool) -> Self { - let guild_id = guild_id.get() as i64; + let guild_id = u64_to_i64_truncating(guild_id.get()); Self { set: JoinSet::new(), db, @@ -126,27 +128,27 @@ impl CalculatorBuilder { } pub fn user(self, user_id: Id) -> Self { - let id = user_id.get() as i64; + let id = u64_to_i64_truncating(user_id.get()); self.query(&AccessCategoryFlag::Users, id) } pub fn thread(self, thread_id: Id) -> Self { - let id = thread_id.get() as i64; + let id = u64_to_i64_truncating(thread_id.get()); self.query(&AccessCategoryFlag::Threads, id) } pub fn text_channel(self, text_channel_id: Id) -> Self { - let id = text_channel_id.get() as i64; + let id = u64_to_i64_truncating(text_channel_id.get()); self.query(&AccessCategoryFlag::TextChannels, id) } pub fn voice_channel(self, voice_channel_id: Id) -> Self { - let id = voice_channel_id.get() as i64; + let id = u64_to_i64_truncating(voice_channel_id.get()); self.query(&AccessCategoryFlag::VoiceChannels, id) } pub fn category_channel(self, category_channel_id: Id) -> Self { - let id = category_channel_id.get() as i64; + let id = u64_to_i64_truncating(category_channel_id.get()); self.query(&AccessCategoryFlag::CategoryChannels, id) } @@ -187,7 +189,7 @@ impl TryFrom for AccessCategoryFlag { // SAFETY: `value` is guruanteed to only have one flag, // so this transmute is safe - Ok(unsafe { std::mem::transmute(value.bits()) }) + Ok(unsafe { std::mem::transmute::(value.bits()) }) } } @@ -231,11 +233,12 @@ bitflags! { } } -impl PrettyFlagsDisplay for AccessCategoryFlags {} +impl FlagsDisplay for AccessCategoryFlags {} impl From for AccessCategoryFlags { fn from(category: AccessCategory) -> Self { - Self::from_bits_retain(category.value() as u8) + #[allow(clippy::cast_possible_truncation)] + Self::from_bits_retain(category.value().unsigned_abs() as u8) } } diff --git a/lyra/src/component/config/access/clear.rs b/lyra/src/component/config/access/clear.rs index 0fbfc43..6cf7cae 100644 --- a/lyra/src/component/config/access/clear.rs +++ b/lyra/src/component/config/access/clear.rs @@ -1,4 +1,4 @@ -use lyra_ext::pretty::flags_display::PrettyFlagsDisplay; +use lyra_ext::{num::u64_to_i64_truncating, pretty::flags_display::FlagsDisplay}; use tokio::task::JoinSet; use twilight_interactions::command::{CommandModel, CreateCommand}; @@ -6,7 +6,7 @@ use super::AccessCategory; use crate::{ command::{ check, - macros::{out, sus}, + macros::{note, out, sus}, model::BotSlashCommand, require, util::prompt_for_confirmation, @@ -36,20 +36,20 @@ impl BotSlashCommand for Clear { let mut set = JoinSet::new(); category_flags.iter_as_columns().for_each(|c| { let db = ctx.db().clone(); - let g = ctx.guild_id().get() as i64; + let g = u64_to_i64_truncating(ctx.guild_id().get()); set.spawn(async move { - sqlx::query(&format!( - "--sql - DELETE FROM {c} WHERE guild = $1;" - )) - .bind(g) - .execute(&db) - .await + sqlx::query(&format!("DELETE FROM {c} WHERE guild = $1;")) + .bind(g) + .execute(&db) + .await }); }); - let mut ctx = prompt_for_confirmation(ctx).await?; + let (mut ctx, confirmed) = prompt_for_confirmation(ctx).await?; + if !confirmed { + note!("Cancelled executing this command", ctx); + } let mut rows_affected = 0; while let Some(res) = set.join_next().await { diff --git a/lyra/src/component/config/access/edit.rs b/lyra/src/component/config/access/edit.rs index cace494..82c512c 100644 --- a/lyra/src/component/config/access/edit.rs +++ b/lyra/src/component/config/access/edit.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use itertools::Itertools; +use lyra_ext::num::u64_to_i64_truncating; use sqlx::{postgres::PgQueryResult, Pool, Postgres}; use tokio::task::JoinSet; use twilight_interactions::command::{ @@ -47,8 +48,7 @@ fn add_access( join_set.spawn(async move { sqlx::query(&format!( - "--sql - INSERT INTO {column} + "INSERT INTO {column} SELECT ch_new.guild, ch_new.id FROM (VALUES {values_clause}) AS ch_new (guild, id) WHERE NOT EXISTS @@ -76,8 +76,7 @@ fn remove_access( join_set.spawn(async move { sqlx::query(&format!( - "--sql - DELETE FROM {column} + "DELETE FROM {column} WHERE guild = $1 AND ({where_clause}); ", )) @@ -182,7 +181,7 @@ impl BotSlashCommand for MemberRole { let input_mentionables_len = input_mentionables.values().fold(0, |acc, v| acc + v.len()); let database = ctx.db(); - let guild_id = ctx.guild_id().get() as i64; + let guild_id = u64_to_i64_truncating(ctx.guild_id().get()); let mut set = JoinSet::new(); match self.action { EditAction::Add => { @@ -263,7 +262,6 @@ pub struct Channel { target_5: Option, } -#[allow(clippy::too_many_lines)] impl BotSlashCommand for Channel { async fn run(self, ctx: SlashCtx) -> CommandResult { let mut ctx = require::guild(ctx)?; @@ -299,7 +297,7 @@ impl BotSlashCommand for Channel { let input_channels_len = input_channels.values().fold(0, |acc, v| acc + v.len()); let database = ctx.db(); - let guild_id = ctx.guild_id().get() as i64; + let guild_id = u64_to_i64_truncating(ctx.guild_id().get()); let mut set = JoinSet::new(); match self.action { EditAction::Add => { diff --git a/lyra/src/component/config/access/mode.rs b/lyra/src/component/config/access/mode.rs index 427ec28..dbf9493 100644 --- a/lyra/src/component/config/access/mode.rs +++ b/lyra/src/component/config/access/mode.rs @@ -1,7 +1,7 @@ use std::fmt::Display; use itertools::Itertools; -use lyra_ext::pretty::flags_display::PrettyFlagsDisplay; +use lyra_ext::{num::u64_to_i64_truncating, pretty::flags_display::FlagsDisplay}; use twilight_interactions::command::{CommandModel, CommandOption, CreateCommand, CreateOption}; use super::AccessCategory; @@ -105,7 +105,7 @@ impl BotSlashCommand for Mode { check::user_is_access_manager(&ctx)?; let access_mode = >::from(self.mode); - let sql_access_mode = access_mode.map_or_else(|| "null".into(), |b| b.to_string()); + let sql_access_mode = access_mode.map_or_else(|| String::from("null"), |b| b.to_string()); let category_flags = AccessCategoryFlags::from(self.category); let set_statements = category_flags @@ -118,11 +118,9 @@ impl BotSlashCommand for Mode { .join(" OR "); let res = sqlx::query(&format!( - "--sql - UPDATE guild_configs SET {set_statements} WHERE id = $1 AND ({where_clause}); - " + "UPDATE guild_configs SET {set_statements} WHERE id = $1 AND ({where_clause});" )) - .bind(ctx.guild_id().get() as i64) + .bind(u64_to_i64_truncating(ctx.guild_id().get())) .bind(access_mode) .execute(ctx.db()) .await?; diff --git a/lyra/src/component/config/access/view.rs b/lyra/src/component/config/access/view.rs index c12dd5f..4758a6a 100644 --- a/lyra/src/component/config/access/view.rs +++ b/lyra/src/component/config/access/view.rs @@ -31,7 +31,7 @@ impl BotSlashCommand for View { ); let embed = embed.validate()?.build(); - ctx.respond_embeds_only([embed]).await?; + ctx.respond_embeds([embed]).await?; Ok(()) } } diff --git a/lyra/src/component/config/now_playing.rs b/lyra/src/component/config/now_playing.rs index 4ec6bf5..871a47f 100644 --- a/lyra/src/component/config/now_playing.rs +++ b/lyra/src/component/config/now_playing.rs @@ -1,3 +1,4 @@ +use lyra_ext::num::u64_to_i64_truncating; use twilight_interactions::command::{CommandModel, CreateCommand}; use crate::{ @@ -24,7 +25,7 @@ impl BotSlashCommand for Toggle { let mut ctx = require::guild(ctx)?; let new_now_playing = sqlx::query!( "UPDATE guild_configs SET now_playing = NOT now_playing WHERE id = $1 RETURNING now_playing;", - ctx.guild_id().get() as i64, + u64_to_i64_truncating(ctx.guild_id().get()), ) .fetch_one(ctx.db()) .await? diff --git a/lyra/src/component/connection.rs b/lyra/src/component/connection.rs index fb22507..5f62cc8 100644 --- a/lyra/src/component/connection.rs +++ b/lyra/src/component/connection.rs @@ -3,7 +3,7 @@ mod leave; pub use join::{auto as auto_join, Join}; pub use leave::Leave; -use lyra_ext::time::{rfc3339::rfc3339_time, unix::unix_time}; +use lyra_ext::{iso8601_time, unix_time}; use std::sync::Arc; @@ -18,6 +18,7 @@ use twilight_model::id::{ use self::join::JoinedChannel; use crate::{ + command::require, component::connection::{ join::JoinedChannelType, leave::{disconnect, pre_disconnect_cleanup, LeaveResponse}, @@ -34,15 +35,16 @@ use crate::{ }, }, gateway::{voice, GuildIdAware, SenderAware}, - lavalink::{self, LavalinkAware}, + lavalink::Lavalink, + LavalinkAndGuildIdAware, LavalinkAware, }; -fn users_in_voice(ctx: &impl CacheAware, channel_id: Id) -> Option { - ctx.cache() +pub fn users_in_voice(cx: &impl CacheAware, channel_id: Id) -> Option { + cx.cache() .voice_channel_states(channel_id) .map_or(Some(0), |voice_states| { let mut users = voice_states - .map(|v| ctx.cache().user(v.user_id())) + .map(|v| cx.cache().user(v.user_id())) .collect::>>()?; users.retain(|u| !u.bot); Some(users.len()) @@ -56,11 +58,11 @@ struct InactivityTimeoutContext { } impl InactivityTimeoutContext { - fn new_via(ctx: &(impl OwnedBotStateAware + SenderAware + GuildIdAware)) -> Self { + fn new_via(cx: &(impl OwnedBotStateAware + SenderAware + GuildIdAware)) -> Self { Self { - inner: ctx.bot_owned(), - sender: ctx.sender().clone(), - guild_id: ctx.guild_id(), + inner: cx.bot_owned(), + sender: cx.sender().clone(), + guild_id: cx.guild_id(), } } } @@ -77,8 +79,8 @@ impl CacheAware for InactivityTimeoutContext { } } -impl lavalink::LavalinkAware for InactivityTimeoutContext { - fn lavalink(&self) -> &lavalink::Lavalink { +impl LavalinkAware for InactivityTimeoutContext { + fn lavalink(&self) -> &Lavalink { self.inner.lavalink() } } @@ -95,6 +97,8 @@ impl GuildIdAware for InactivityTimeoutContext { } } +impl LavalinkAndGuildIdAware for InactivityTimeoutContext {} + async fn start_inactivity_timeout( ctx: InactivityTimeoutContext, channel_id: Id, @@ -108,13 +112,13 @@ async fn start_inactivity_timeout( ); for _ in 0..const_connection::INACTIVITY_TIMEOUT_POLL_N { - tokio::time::sleep(*const_connection::inactivity_timeout_poll_interval()).await; + tokio::time::sleep(const_connection::INACTIVITY_TIMEOUT_POLL_INTERVAL).await; if users_in_voice(&ctx, channel_id).is_some_and(|n| n >= 1) { return Ok(()); } } - let Some(connection) = ctx.lavalink().get_connection(guild_id) else { + let Some(connection) = ctx.get_connection() else { return Ok(()); }; connection.notify_change(); @@ -138,24 +142,25 @@ async fn start_inactivity_timeout( #[tracing::instrument(skip_all, name = "voice_state_update")] pub async fn handle_voice_state_update( ctx: &voice::Context, + connection_changed: bool, ) -> Result<(), HandleVoiceStateUpdateError> { let state = &ctx.inner; let maybe_old_state = ctx.old_voice_state(); let guild_id = ctx.guild_id(); - let lavalink = ctx.lavalink(); tracing::trace!("handling voice state update"); let (connected_channel_id, text_channel_id) = { - let Some(connection) = lavalink.get_connection(guild_id) else { + let Some(connection) = ctx.get_connection() else { + tracing::trace!("no active connection"); return Ok(()); }; - if connection.changed().await { - tracing::trace!("connection changed"); + if connection_changed { + tracing::trace!("received connection change notification"); return Ok(()); } - tracing::trace!("connection forced"); + tracing::trace!("no connection change notification"); (connection.channel_id, connection.text_channel_id) }; @@ -167,6 +172,14 @@ pub async fn handle_voice_state_update( && state.channel_id != Some(old_channel_id) && users_in_voice(ctx, connected_channel_id).is_some_and(|n| n == 0) { + if let Ok(player) = require::player(ctx) { + player.set_pause(true).await?; + ctx.http() + .create_message(text_channel_id) + .content("⚡▶ Paused `(Bot is not used by anyone)`") + .await?; + }; + traced::tokio_spawn(start_inactivity_timeout( InactivityTimeoutContext::new_via(ctx), connected_channel_id, @@ -225,17 +238,20 @@ async fn match_state_channel_id( empty: voice_is_empty, }; + if let Ok(player) = require::player(ctx) { + player.update_voice_channel(voice_is_empty).await?; + } let forcefully_moved_notice = if voice_is_empty { format!( "\n`(Bot was forcefully moved to an empty voice channel, and automatically disconnecting if no one else joins in` `)`", unix_time().as_secs() + u64::from(const_connection::INACTIVITY_TIMEOUT_SECS) ) } else { - "`(Bot was forcefully moved)`".into() + String::from("`(Bot was forcefully moved)`") }; let stage_emoji = match joined.kind { - JoinedChannelType::Stage => "🎭".into(), + JoinedChannelType::Stage => String::from("🎭"), JoinedChannelType::Voice => String::new(), }; @@ -253,12 +269,16 @@ async fn match_state_channel_id( )) .await?; + if let Some(mut connection) = ctx.get_connection_mut() { + connection.channel_id = channel_id; + } + if matches!(joined.kind, JoinedChannelType::Stage) { ctx.bot() .http() .update_current_user_voice_state(guild_id) .channel_id(channel_id) - .request_to_speak_timestamp(&rfc3339_time()) + .request_to_speak_timestamp(&iso8601_time()) .await?; } diff --git a/lyra/src/component/connection/join.rs b/lyra/src/component/connection/join.rs index fada778..c470de7 100644 --- a/lyra/src/component/connection/join.rs +++ b/lyra/src/component/connection/join.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, fmt::Display, sync::Arc}; -use lyra_ext::time::{rfc3339::rfc3339_time, unix::unix_time}; +use lyra_ext::{iso8601_time, unix_time}; use twilight_gateway::Event; use twilight_interactions::command::{CommandModel, CreateCommand}; use twilight_mention::Mention; @@ -18,7 +18,7 @@ use twilight_model::{ use crate::{ command::{ check, - macros::{bad, cant, nope, note, note_fol, out, sus_fol}, + macros::{bad, cant, nope, note, note_fol, out_or_fol, sus_fol}, model::{BotSlashCommand, CtxKind, GuildCtx, RespondViaMessage}, require::{self, InVoiceCachedVoiceState}, SlashCtx, @@ -41,7 +41,8 @@ use crate::{ Cache as CacheError, CommandResult, UserNotInVoice as UserNotInVoiceError, }, gateway::{GuildIdAware, SenderAware}, - lavalink::{Connection, LavalinkAware}, + lavalink::Connection, + LavalinkAware, }; pub(super) enum Response { @@ -101,17 +102,13 @@ type GetUsersVoiceChannelResult = Result<(Id, ChannelType, Option>), GetUsersVoiceChannelError>; fn get_users_voice_channel(ctx: &GuildCtx) -> GetUsersVoiceChannelResult { - let voice_state = ctx + let channel_id = ctx .cache() .voice_state(ctx.author_id(), ctx.guild_id()) - .ok_or(UserNotInVoiceError)?; - - let channel_id = voice_state.channel_id(); + .ok_or(UserNotInVoiceError)? + .channel_id(); let voice = ctx.cache().channel(channel_id).ok_or(CacheError)?; - let channel_type = voice.kind; - let channel_parent_id = voice.parent_id; - - Ok((channel_id, channel_type, channel_parent_id)) + Ok((channel_id, voice.kind, voice.parent_id)) } async fn impl_join( @@ -169,19 +166,19 @@ async fn connect_to( ) -> Result { check_user_is_stage_manager(channel_type, ctx)?; - let old_channel_id = ctx - .current_voice_state() - .map(|voice_state| { - let old_channel_id = voice_state.channel_id(); + let old_channel_id = match require::in_voice(ctx) { + Ok(ref in_voice) => { + let old_channel_id = in_voice.channel_id(); if old_channel_id == channel_id { - Err(error::InVoiceAlready(channel_id))?; + return Err(error::InVoiceAlready(channel_id).into()); } - check::noone_else_in(old_channel_id, ctx)?; + check::noone_else_in(in_voice.into(), ctx)?; - Ok::<_, ConnectToError>(old_channel_id) - }) - .transpose()?; + Some(old_channel_id) + } + Err(_) => None, + }; Ok(impl_connect_to( channel_id, @@ -206,7 +203,7 @@ async fn impl_connect_to( .bot_permissions_for(channel_id)? .contains(Permissions::CONNECT) { - Err(error::ConnectionForbidden(channel_id))?; + return Err(error::ConnectionForbidden(channel_id).into()); } check::user_allowed_to_use(channel_id, channel_parent_id, ctx).await?; @@ -215,43 +212,52 @@ async fn impl_connect_to( let voice_is_empty = users_in_voice(ctx, channel_id).ok_or(CacheError)? == 0; - let response = old_channel_id.map_or_else( - || { - let connection = Connection::new(channel_id, ctx.channel_id()); - connection.notify_change(); - ctx.lavalink().new_connection_with(guild_id, connection); - Response::Joined { - voice: joined, - empty: voice_is_empty, - } - }, - |from| { - // SAFETY: `old_channel_id` is of variant `Some`, meaning another connection exists, - // so `ctx.lavalink().get_connection_mut(guild_id).unwrap_unchecked()` is safe - let mut connection = unsafe { - ctx.lavalink() - .get_connection_mut(guild_id) - .unwrap_unchecked() - }; - connection.channel_id = channel_id; - connection.notify_change(); - Response::Moved { - from, - to: joined, - empty: voice_is_empty, - } - }, - ); + let lavalink = ctx.lavalink(); + let response = if let Some(from) = old_channel_id { + let mut connection = lavalink.try_get_connection_mut(guild_id)?; + connection.channel_id = channel_id; + connection.notify_change(); + drop(connection); + Response::Moved { + from, + to: joined, + empty: voice_is_empty, + } + } else { + let connection = Connection::new(channel_id, ctx.channel_id()); + connection.notify_change(); + lavalink.new_connection_with(guild_id, connection); + Response::Joined { + voice: joined, + empty: voice_is_empty, + } + }; ctx.sender() .command(&UpdateVoiceState::new(guild_id, channel_id, true, false))?; + if let Ok(player) = require::player(ctx) { + if old_channel_id.is_some() { + tracing::trace!("waiting for voice server update..."); + let _ = ctx + .bot() + .standby() + .wait_for_event(move |e: &Event| match e { + Event::VoiceServerUpdate(v) => v.guild_id == guild_id, + _ => false, + }) + .await; + tracing::trace!("voice server update received"); + player.update_voice_channel(voice_is_empty).await?; + } + } + if joined.kind == JoinedChannelType::Stage { ctx.bot() .http() .update_current_user_voice_state(guild_id) .channel_id(channel_id) - .request_to_speak_timestamp(&rfc3339_time()) + .request_to_speak_timestamp(&iso8601_time()) .await?; } @@ -322,12 +328,15 @@ async fn handle_response( let (joined, empty) = match response { Response::Joined { voice, empty } => { let stage = matches!(voice.kind, JoinedChannelType::Stage); - out!(stage_fmt(&format!("🖇️ {}", voice.id.mention()), stage), ?ctx); + out_or_fol!( + stage_fmt(&format!("🖇️ {}", voice.id.mention()), stage), + ?ctx + ); (voice, empty) } Response::Moved { from, to, empty } => { let stage = matches!(to.kind, JoinedChannelType::Stage); - out!( + out_or_fol!( stage_fmt( &format!("️📎🖇️ ~~{}~~ ➜ __{}__", from.mention(), to.id.mention()), stage, @@ -419,7 +428,7 @@ impl BotSlashCommand for Join { ctx ); } - Pfe::Other(e) => Err(e)?, + Pfe::Other(e) => Err(e.into()), } } } diff --git a/lyra/src/component/connection/leave.rs b/lyra/src/component/connection/leave.rs index 51e5fd3..fa39938 100644 --- a/lyra/src/component/connection/leave.rs +++ b/lyra/src/component/connection/leave.rs @@ -20,7 +20,8 @@ use crate::{ CommandResult, }, gateway::{GuildIdAware, SenderAware}, - lavalink::{self, Event, LavalinkAware}, + lavalink::Event, + LavalinkAware, }; pub(super) struct LeaveResponse(pub(super) Id); @@ -31,18 +32,18 @@ impl Display for LeaveResponse { } } -pub(super) fn disconnect(ctx: &(impl SenderAware + GuildIdAware)) -> Result<(), ChannelError> { - ctx.sender() - .command(&UpdateVoiceState::new(ctx.guild_id(), None, false, false))?; +pub(super) fn disconnect(cx: &(impl SenderAware + GuildIdAware)) -> Result<(), ChannelError> { + cx.sender() + .command(&UpdateVoiceState::new(cx.guild_id(), None, false, false))?; Ok(()) } pub(super) async fn pre_disconnect_cleanup( - ctx: &(impl GuildIdAware + lavalink::LavalinkAware + Sync), + cx: &(impl GuildIdAware + LavalinkAware + Sync), ) -> Result<(), PreDisconnectCleanupError> { - let guild_id = ctx.guild_id(); - let lavalink = ctx.lavalink(); + let guild_id = cx.guild_id(); + let lavalink = cx.lavalink(); if let Some(connection) = lavalink.get_connection(guild_id) { connection.dispatch(Event::QueueClear); @@ -57,9 +58,9 @@ async fn leave(ctx: &GuildCtx) -> Result { caut!("Not currently connected to a voice channel.", ctx); } - leave::NotInVoiceMatchedError::Other(e) => Err(e)?, + leave::NotInVoiceMatchedError::Other(e) => Err(e.into()), }, } } diff --git a/lyra/src/component/playback.rs b/lyra/src/component/playback.rs index 2321a55..dff8d5e 100644 --- a/lyra/src/component/playback.rs +++ b/lyra/src/component/playback.rs @@ -1,71 +1,70 @@ -// use anyhow::Result; -// use twilight_lavalink::model::{Pause, Seek, Stop}; -// use twilight_model::gateway::payload::incoming::MessageCreate; - -// use crate::{ -// commands::{models::App, Context}, -// lavalink::LavalinkAware, -// }; - -// pub async fn pause(ctx: Context) -> Result<()> { -// tracing::debug!( -// "pause command in channel {} by {}", -// ctx.channel_id(), -// ctx.author().name -// ); - -// let guild_id = ctx.guild_id().unwrap(); -// let player = ctx.lavalink().player(guild_id).await.unwrap(); -// let paused = player.paused(); -// player.send(Pause::from((guild_id, !paused)))?; - -// let action = if paused { "Unpaused " } else { "Paused" }; - -// ctx.respond(&format!("{action} the track")).await?; - -// Ok(()) -// } - -// pub async fn seek(ctx: Context) -> Result<()> { -// let bot = ctx.bot(); -// let (author, channel_id) = (ctx.author(), ctx.channel_id()); - -// tracing::debug!("seek command in channel {} by {}", channel_id, author.name); -// ctx.http() -// .create_message(channel_id) -// .content("Where in the track do you want to seek to (in seconds)?")? -// .await?; - -// let author_id = author.id; -// let msg = bot -// .standby() -// .wait_for_message(channel_id, move |new_msg: &MessageCreate| { -// new_msg.author.id == author_id -// }) -// .await?; -// let guild_id = ctx.guild_id().unwrap(); -// let position = msg.content.parse::()?; - -// let player = ctx.lavalink().player(guild_id).await.unwrap(); -// player.send(Seek::from((guild_id, position * 1000)))?; - -// ctx.respond(&format!("Seeked to {position}s")).await?; - -// Ok(()) -// } - -// pub async fn stop(ctx: Context) -> Result<()> { -// tracing::debug!( -// "stop command in channel {} by {}", -// ctx.channel_id(), -// ctx.author().name -// ); - -// let guild_id = ctx.guild_id().unwrap(); -// let player = ctx.lavalink().player(guild_id).await.unwrap(); -// player.send(Stop::from(guild_id))?; - -// ctx.respond("Stopped the track").await?; - -// Ok(()) -// } +mod back; +mod jump; +mod play_pause; +mod restart; +mod seek; +mod skip; + +pub use back::Back; +pub use jump::{Autocomplete as JumpAutocomplete, Jump}; +pub use play_pause::PlayPause; +pub use restart::Restart; +pub use seek::Seek; +pub use skip::Skip; + +use crate::{ + command::require, + core::model::{BotStateAware, CacheAware, HttpAware}, + error::component::playback::HandleVoiceStateUpdateError, + gateway::voice::Context, + LavalinkAndGuildIdAware, +}; + +#[tracing::instrument(skip_all, name = "voice_state_update")] +pub async fn handle_voice_state_update( + ctx: &Context, + connection_changed: bool, +) -> Result<(), HandleVoiceStateUpdateError> { + let state = ctx.inner.as_ref(); + let maybe_old_state = ctx.old_voice_state(); + + tracing::trace!("handling voice state update"); + let text_channel_id = { + let Some(connection) = ctx.get_connection() else { + tracing::trace!("no active connection"); + return Ok(()); + }; + + if connection_changed { + tracing::trace!("received connection change notification"); + return Ok(()); + } + tracing::trace!("no connection change notification"); + + connection.text_channel_id + }; + + let Ok(player) = require::player(ctx) else { + return Ok(()); + }; + + if state.user_id == ctx.bot().user_id() + && state.suppress + && maybe_old_state.is_some_and(|old_state| { + state.channel_id.is_some_and(|channel_id| { + channel_id == old_state.channel_id() + && ctx.cache().channel(channel_id).is_some_and(|channel| { + channel.kind == twilight_model::channel::ChannelType::GuildStageVoice + }) + }) && !old_state.suppress() + }) + { + player.set_pause(true).await?; + ctx.http() + .create_message(text_channel_id) + .content("⚡▶ Paused `(Bot was moved to audience)`") + .await?; + } + + Ok(()) +} diff --git a/lyra/src/component/playback/back.rs b/lyra/src/component/playback/back.rs new file mode 100644 index 0000000..9d09c50 --- /dev/null +++ b/lyra/src/component/playback/back.rs @@ -0,0 +1,43 @@ +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::command::{check, macros::out, model::BotSlashCommand, require}; + +/// Jumps to the track before the current one in the queue. Will wrap around if queue repeat is enabled. +#[derive(CreateCommand, CommandModel)] +#[command(name = "back")] +pub struct Back; + +impl BotSlashCommand for Back { + #[allow(clippy::significant_drop_tightening)] + async fn run(self, ctx: crate::command::SlashCtx) -> crate::error::CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let mut data_w = data.write().await; + let queue = require::queue_not_empty_mut(&mut data_w)?; + let mut txt; + + if let Ok(current_track) = require::current_track(queue) { + check::current_track_is_users(¤t_track, in_voice_with_user)?; + txt = format!("⏮️ ~~`{}`~~", current_track.track.data().info.title); + } else { + txt = String::new(); + } + + queue.downgrade_repeat_mode(); + queue.acquire_advance_lock(); + queue.recede(); + + // SAFETY: since the queue is not empty, receding must always yield a new current track + let item = unsafe { queue.current().unwrap_unchecked() }; + player.context.play_now(item.data()).await?; + + if txt.is_empty() { + txt = format!("⏮️ `{}`", item.data().info.title); + } + + out!(txt, ctx); + } +} diff --git a/lyra/src/component/playback/jump.rs b/lyra/src/component/playback/jump.rs new file mode 100644 index 0000000..f7951bb --- /dev/null +++ b/lyra/src/component/playback/jump.rs @@ -0,0 +1,27 @@ +use lyra_proc::{BotAutocompleteGroup, BotCommandGroup}; +use twilight_interactions::command::{CommandModel, CreateCommand}; + +mod backward; +mod first; +mod forward; +mod to; + +#[derive(CommandModel, CreateCommand, BotCommandGroup)] +#[command(name = "jump", desc = ".", dm_permission = false)] +pub enum Jump { + #[command(name = "to")] + To(to::To), + #[command(name = "forward")] + Forward(forward::Forward), + #[command(name = "backward")] + Backward(backward::Backward), + #[command(name = "first")] + First(first::First), +} + +#[derive(CommandModel, BotAutocompleteGroup)] +#[command(autocomplete = true)] +pub enum Autocomplete { + #[command(name = "to")] + To(to::Autocomplete), +} diff --git a/lyra/src/component/playback/jump/backward.rs b/lyra/src/component/playback/jump/backward.rs new file mode 100644 index 0000000..4928a95 --- /dev/null +++ b/lyra/src/component/playback/jump/backward.rs @@ -0,0 +1,58 @@ +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::command::{ + check, + macros::{bad, out}, + model::BotSlashCommand, + require, +}; + +/// Jumps to a new track at least two tracks earlier. +#[derive(CreateCommand, CommandModel)] +#[command(name = "backward")] +pub struct Backward { + /// Jump by how many tracks? + #[command(min_value = 2)] + tracks: i64, +} + +impl BotSlashCommand for Backward { + #[allow(clippy::significant_drop_tightening)] + async fn run(self, ctx: crate::command::SlashCtx) -> crate::error::CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let mut data_w = data.write().await; + let queue = require::queue_not_empty_mut(&mut data_w)?; + let current_track = require::current_track(queue)?; + check::current_track_is_users(¤t_track, in_voice_with_user)?; + + #[allow(clippy::cast_possible_truncation)] + let tracks = self.tracks.unsigned_abs() as usize; + let queue_index = queue.index(); + let Some(index) = queue_index.checked_sub(tracks) else { + if queue_index == 0 { + bad!("No where else to jump to", ctx); + } + bad!( + format!( + "Cannot jump past the start of the queue. Maximum backward jump is {} tracks.", + queue_index, + ), + ctx + ); + }; + + queue.downgrade_repeat_mode(); + queue.acquire_advance_lock(); + + let track = queue[index].data(); + let txt = format!("↩️ Jumped to `{}` (`#{}`)", track.info.title, index + 1); + player.context.play_now(track).await?; + + *queue.index_mut() = index; + out!(txt, ctx); + } +} diff --git a/lyra/src/component/playback/jump/first.rs b/lyra/src/component/playback/jump/first.rs new file mode 100644 index 0000000..4f5e158 --- /dev/null +++ b/lyra/src/component/playback/jump/first.rs @@ -0,0 +1,47 @@ +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::command::{ + check, + macros::{bad, out}, + model::BotSlashCommand, + require, +}; + +/// Jumps to the first track in the queue +#[derive(CreateCommand, CommandModel)] +#[command(name = "first")] +pub struct First; + +impl BotSlashCommand for First { + #[allow(clippy::significant_drop_tightening)] + async fn run(self, ctx: crate::command::SlashCtx) -> crate::error::CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let mut data_w = data.write().await; + let queue = require::queue_not_empty_mut(&mut data_w)?; + let current_track = require::current_track(queue)?; + check::current_track_is_users(¤t_track, in_voice_with_user)?; + + let queue_len = queue.len(); + if queue_len == 1 { + bad!("No where else to jump to", ctx); + } + + if queue.position().get() == 1 { + bad!("Cannot jump to the current track", ctx); + } + + queue.downgrade_repeat_mode(); + queue.acquire_advance_lock(); + + let track = queue[0].data(); + let txt = format!("⬅️ Jumped to `{}` (`#1`)", track.info.title); + player.context.play_now(track).await?; + + *queue.index_mut() = 0; + out!(txt, ctx); + } +} diff --git a/lyra/src/component/playback/jump/forward.rs b/lyra/src/component/playback/jump/forward.rs new file mode 100644 index 0000000..96cbb9c --- /dev/null +++ b/lyra/src/component/playback/jump/forward.rs @@ -0,0 +1,67 @@ +use std::num::NonZeroUsize; + +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::command::{ + check, + macros::{bad, out}, + model::BotSlashCommand, + require, +}; + +/// Jumps to a new track at least two tracks later. +#[derive(CreateCommand, CommandModel)] +#[command(name = "forward")] +pub struct Forward { + /// Jump by how many tracks? + #[command(min_value = 2)] + tracks: i64, +} + +impl BotSlashCommand for Forward { + #[allow(clippy::significant_drop_tightening)] + async fn run(self, ctx: crate::command::SlashCtx) -> crate::error::CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let mut data_w = data.write().await; + let queue = require::queue_not_empty_mut(&mut data_w)?; + let current_track = require::current_track(queue)?; + + #[allow(clippy::cast_possible_truncation)] + let jump = self.tracks.unsigned_abs() as usize; + let queue_len = queue.len(); + + let queue_position = queue.position(); + let new_position = queue_position.saturating_add(jump); + if new_position.get() > queue_len { + let maximum_jump = queue_len - queue_position.get(); + if maximum_jump == 0 { + bad!("No where else to jump to", ctx); + } + bad!( + format!( + "Cannot jump past the end of the queue. Maximum forward jump is {} tracks.", + maximum_jump, + ), + ctx + ); + } + + let skipped = + (current_track.position.get()..new_position.get()).filter_map(NonZeroUsize::new); + check::all_users_track(queue, skipped, in_voice_with_user)?; + + queue.downgrade_repeat_mode(); + queue.acquire_advance_lock(); + + let track = queue[new_position].data(); + let txt = format!("↪️ Jumped to `{}` (`#{}`)", track.info.title, new_position); + player.context.play_now(track).await?; + + *queue.index_mut() = new_position.get() - 1; + out!(txt, ctx); + } +} diff --git a/lyra/src/component/playback/jump/to.rs b/lyra/src/component/playback/jump/to.rs new file mode 100644 index 0000000..da048f3 --- /dev/null +++ b/lyra/src/component/playback/jump/to.rs @@ -0,0 +1,121 @@ +use std::{ + collections::HashSet, + num::{IntErrorKind, NonZeroUsize}, +}; + +use twilight_interactions::command::{AutocompleteValue, CommandModel, CreateCommand}; +use twilight_model::application::command::CommandOptionChoice; + +use crate::{ + command::{ + check, + macros::{bad, out}, + model::{BotAutocomplete, BotSlashCommand}, + require, + }, + component::queue::{ + generate_position_choices, generate_position_choices_from_fuzzy_match, + generate_position_choices_from_input, validate_input_position, + }, + core::model::CacheAware, + LavalinkAndGuildIdAware, +}; + +#[allow(clippy::significant_drop_tightening)] +async fn generate_skip_to_choices( + track: String, + cx: &(impl CacheAware + LavalinkAndGuildIdAware + Sync), +) -> Vec { + let Ok(player) = require::player(cx) else { + return Vec::new(); + }; + let data = player.data(); + let data_r = data.read().await; + let (queue, Some(queue_len)) = (data_r.queue(), NonZeroUsize::new(data_r.queue().len())) else { + return Vec::new(); + }; + + let excluded = HashSet::from([queue.position()]); + let queue_iter = queue.iter_positions_and_items(); + + match track.parse::() { + Ok(input) => { + generate_position_choices_from_input(input, queue_len, queue_iter, &excluded, cx) + } + Err(e) if matches!(e.kind(), IntErrorKind::Empty) => { + generate_position_choices(queue.position(), queue_len, queue_iter, &excluded, cx) + } + Err(_) => generate_position_choices_from_fuzzy_match(&track, queue_iter, &excluded, cx), + } +} + +#[derive(CommandModel)] +#[command(autocomplete = true)] +pub struct Autocomplete { + track: AutocompleteValue, +} + +impl BotAutocomplete for Autocomplete { + async fn execute( + self, + ctx: crate::command::AutocompleteCtx, + ) -> crate::error::command::AutocompleteResult { + let mut ctx = require::guild(ctx)?; + let AutocompleteValue::Focused(track) = self.track else { + // SAFETY: exactly one autocomplete option is focused, so this is unreachable + unsafe { std::hint::unreachable_unchecked() } + }; + + let choices = generate_skip_to_choices(track, &ctx).await; + Ok(ctx.autocomplete(choices).await?) + } +} + +/// Jumps to a new track in the queue, skipping all track in-between. +#[derive(CommandModel, CreateCommand)] +#[command(name = "to")] +pub struct To { + /// Which track? [track title / position in queue] + #[command(min_value = 1, autocomplete = true)] + track: i64, +} + +impl BotSlashCommand for To { + #[allow(clippy::significant_drop_tightening)] + async fn run(self, ctx: crate::command::SlashCtx) -> crate::error::CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let mut data_w = data.write().await; + let queue = require::queue_not_empty_mut(&mut data_w)?; + let current_track = require::current_track(queue)?; + check::current_track_is_users(¤t_track, in_voice_with_user)?; + + let queue_len = queue.len(); + if queue_len == 1 { + bad!("No where else to jump to", ctx); + } + + let input = self.track; + validate_input_position(input, queue_len)?; + + #[allow(clippy::cast_possible_truncation)] + let position = input.unsigned_abs() as usize; + if position == queue.position().get() { + bad!("Cannot jump to the current track", ctx); + } + + queue.downgrade_repeat_mode(); + queue.acquire_advance_lock(); + + let index = position - 1; + let track = queue[index].data(); + let txt = format!("↔️ Jumped to `{}` (`#{}`)", track.info.title, position); + player.context.play_now(track).await?; + + *queue.index_mut() = index; + out!(txt, ctx); + } +} diff --git a/lyra/src/component/playback/play_pause.rs b/lyra/src/component/playback/play_pause.rs new file mode 100644 index 0000000..c920f37 --- /dev/null +++ b/lyra/src/component/playback/play_pause.rs @@ -0,0 +1,37 @@ +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::{ + command::{check, macros::out, model::BotSlashCommand, require, SlashCtx}, + error::CommandResult, +}; + +/// Toggles the playback of the current track. +#[derive(CreateCommand, CommandModel)] +#[command(name = "play-pause", dm_permission = false)] +pub struct PlayPause; + +impl BotSlashCommand for PlayPause { + async fn run(self, ctx: SlashCtx) -> CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let data_r = data.read().await; + let queue = require::queue_not_empty(&data_r)?; + check::current_track_is_users(&require::current_track(queue)?, in_voice_with_user)?; + let pause = !data_r.paused(); + let message = if pause { + "▶️ Paused" + } else { + "⏸️ Resumed" + }; + drop(data_r); + + player + .set_pause_with(pause, &mut data.write().await) + .await?; + + out!(message, ctx); + } +} diff --git a/lyra/src/component/playback/restart.rs b/lyra/src/component/playback/restart.rs new file mode 100644 index 0000000..c69e52c --- /dev/null +++ b/lyra/src/component/playback/restart.rs @@ -0,0 +1,30 @@ +use std::time::Duration; + +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::command::{check, macros::out, model::BotSlashCommand, require}; + +/// Restarts the current track; Equivalent to seeking to 0:00. +#[derive(CreateCommand, CommandModel)] +#[command(name = "restart", dm_permission = false)] +pub struct Restart; + +impl BotSlashCommand for Restart { + async fn run(self, ctx: crate::command::SlashCtx) -> crate::error::CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let data_r = data.read().await; + let queue = require::queue_not_empty(&data_r)?; + let current_track = require::current_track(queue)?; + check::current_track_is_users(¤t_track, in_voice_with_user)?; + + drop(data_r); + player + .seek_to_with(Duration::ZERO, &mut data.write().await) + .await?; + out!("◀️ Restarted", ctx); + } +} diff --git a/lyra/src/component/playback/seek.rs b/lyra/src/component/playback/seek.rs new file mode 100644 index 0000000..62cc0b4 --- /dev/null +++ b/lyra/src/component/playback/seek.rs @@ -0,0 +1,17 @@ +use lyra_proc::BotCommandGroup; +use twilight_interactions::command::{CommandModel, CreateCommand}; + +mod backward; +mod forward; +mod to; + +#[derive(CommandModel, CreateCommand, BotCommandGroup)] +#[command(name = "seek", desc = ".", dm_permission = false)] +pub enum Seek { + #[command(name = "to")] + To(to::To), + #[command(name = "forward")] + Forward(forward::Forward), + #[command(name = "backward")] + Backward(backward::Backward), +} diff --git a/lyra/src/component/playback/seek/backward.rs b/lyra/src/component/playback/seek/backward.rs new file mode 100644 index 0000000..44f20c4 --- /dev/null +++ b/lyra/src/component/playback/seek/backward.rs @@ -0,0 +1,55 @@ +use std::time::Duration; + +use lyra_ext::pretty::duration_display::DurationDisplay; +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::command::{ + check, + macros::{bad, out}, + model::BotSlashCommand, + require, +}; + +/// Seeks the current track backward to a new position some time earlier. +#[derive(CreateCommand, CommandModel)] +#[command(name = "backward")] +pub struct Backward { + /// Seek by how many seconds? (If not given, 5 seconds) + #[command(min_value = 0)] + seconds: Option, +} + +impl BotSlashCommand for Backward { + async fn run(self, ctx: crate::command::SlashCtx) -> crate::error::CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let data_r = data.read().await; + let queue = require::queue_not_empty(&data_r)?; + check::current_track_is_users(&require::current_track(queue)?, in_voice_with_user)?; + + let secs = self.seconds.unwrap_or(5.); + if secs == 0. { + bad!("Seconds can not be zero", ctx); + } + + let old_timestamp = data_r.timestamp(); + drop(data_r); + + let timestamp = old_timestamp.saturating_sub(Duration::from_secs_f64(secs)); + player + .seek_to_with(timestamp, &mut data.write().await) + .await?; + + out!( + format!( + "⏪ ~~`{}`~~ ➜ **`{}`**", + old_timestamp.pretty_display(), + timestamp.pretty_display(), + ), + ctx + ); + } +} diff --git a/lyra/src/component/playback/seek/forward.rs b/lyra/src/component/playback/seek/forward.rs new file mode 100644 index 0000000..87382b4 --- /dev/null +++ b/lyra/src/component/playback/seek/forward.rs @@ -0,0 +1,68 @@ +use std::time::Duration; + +use lyra_ext::pretty::duration_display::DurationDisplay; +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::command::{ + check, + macros::{bad, out}, + model::BotSlashCommand, + require, +}; + +/// Seeks the current track forward to a new position some time later. +#[derive(CreateCommand, CommandModel)] +#[command(name = "forward")] +pub struct Forward { + /// Seek by how many seconds? (If not given, 10 seconds) + #[command(min_value = 0)] + seconds: Option, +} + +impl BotSlashCommand for Forward { + async fn run(self, ctx: crate::command::SlashCtx) -> crate::error::CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let data_r = data.read().await; + let queue = require::queue_not_empty(&data_r)?; + let current_track = require::current_track(queue)?; + check::current_track_is_users(¤t_track, in_voice_with_user)?; + + let secs = self.seconds.unwrap_or(10.); + if secs == 0. { + bad!("Seconds must not be zero", ctx); + } + + let old_timestamp = data_r.timestamp(); + let current_track_length = u128::from(current_track.track.data().info.length); + drop(data_r); + + let timestamp = old_timestamp + Duration::from_secs_f64(secs); + + if timestamp.as_millis() > current_track_length { + let remaining = timestamp.as_millis() - current_track_length; + bad!( + format!( + "Cannot seek past the end of the track. Maximum forward seek is {} seconds.", + remaining.div_ceil(1_000), + ), + ctx + ); + } + player + .seek_to_with(timestamp, &mut data.write().await) + .await?; + + out!( + format!( + "⏩ ~~`{}`~~ ➜ **`{}`**", + old_timestamp.pretty_display(), + timestamp.pretty_display(), + ), + ctx + ); + } +} diff --git a/lyra/src/component/playback/seek/to.rs b/lyra/src/component/playback/seek/to.rs new file mode 100644 index 0000000..7193f32 --- /dev/null +++ b/lyra/src/component/playback/seek/to.rs @@ -0,0 +1,96 @@ +use std::time::Duration; + +use lyra_ext::pretty::duration_display::{DurationDisplay, FromPrettyStr}; +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::{ + command::{ + check, + macros::{bad, out}, + model::BotSlashCommand, + require, + }, + component::playback::Restart, + core::model::InteractionClient, +}; + +/// Seeks the current track to a new position. +#[derive(CreateCommand, CommandModel)] +#[command(name = "to")] +pub struct To { + /// Seek to where? [Must be a timestamp like 1m23s or 4:56, or as the total seconds like 78s] + #[command(min_length = 1)] + timestamp: String, +} + +impl BotSlashCommand for To { + async fn run(self, ctx: crate::command::SlashCtx) -> crate::error::CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let data_r = data.read().await; + let queue = require::queue_not_empty(&data_r)?; + let current_track = require::current_track(queue)?; + check::current_track_is_users(¤t_track, in_voice_with_user)?; + + let timestamp_unchecked = self.timestamp; + let timestamp = if let Ok(secs) = timestamp_unchecked.parse::() { + if secs < 0. { + bad!("Timestamp as total seconds must be positive", ctx); + } + + Duration::from_secs_f64(secs) + } else if let Ok(duration) = Duration::from_pretty_str(×tamp_unchecked) { + duration + } else { + bad!( + format!( + "Invalid timestamp: `{}`; Timestamp must either be in the format like 1m23s or 4:56, or as the total seconds like 78s", + timestamp_unchecked, + ), + ctx + ); + }; + + let current_track_length = u128::from(current_track.track.data().info.length); + + if timestamp.is_zero() { + let restart = InteractionClient::mention_command::(); + + bad!( + format!( + "Timestamp must not be 0:00. To restart the track, use {} instead.", + restart + ), + ctx + ); + } + if timestamp.as_millis() > current_track_length { + bad!( + format!( + "Invalid timestamp: `{}`; Timestamp must be within the track length of `{}`", + timestamp.pretty_display(), + current_track_length.pretty_display(), + ), + ctx + ); + } + + let old_position = data_r.timestamp(); + drop(data_r); + + player + .seek_to_with(timestamp, &mut data.write().await) + .await?; + out!( + format!( + "🕹️ ~~`{}`~~ ➜ **`{}`**", + old_position.pretty_display(), + timestamp.pretty_display(), + ), + ctx + ); + } +} diff --git a/lyra/src/component/playback/skip.rs b/lyra/src/component/playback/skip.rs new file mode 100644 index 0000000..a275796 --- /dev/null +++ b/lyra/src/component/playback/skip.rs @@ -0,0 +1,35 @@ +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::command::{check, macros::out, model::BotSlashCommand, require}; + +/// Skip playing the current track. +#[derive(CreateCommand, CommandModel)] +#[command(name = "skip")] +pub struct Skip; + +impl BotSlashCommand for Skip { + #[allow(clippy::significant_drop_tightening)] + async fn run(self, ctx: crate::command::SlashCtx) -> crate::error::CommandResult { + let mut ctx = require::guild(ctx)?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; + let data = player.data(); + + let mut data_w = data.write().await; + let queue = require::queue_not_empty_mut(&mut data_w)?; + let current_track = require::current_track(queue)?; + check::current_track_is_users(¤t_track, in_voice_with_user)?; + let txt = format!("⏭️ ~~`{}`~~", current_track.track.data().info.title); + + queue.downgrade_repeat_mode(); + queue.acquire_advance_lock(); + queue.advance(); + if let Some(item) = queue.current() { + player.context.play_now(item.data()).await?; + } else { + player.context.stop_now().await?; + } + + out!(txt, ctx); + } +} diff --git a/lyra/src/component/queue.rs b/lyra/src/component/queue.rs index efc27c0..322048a 100644 --- a/lyra/src/component/queue.rs +++ b/lyra/src/component/queue.rs @@ -8,11 +8,16 @@ mod repeat; mod shuffle; pub use clear::Clear; +#[allow(clippy::module_name_repetitions)] pub use fair_queue::FairQueue; -use lyra_ext::pretty::{ - duration_display::PrettyDurationDisplay, join::PrettyJoiner, truncate::PrettyTruncator, +use lyra_ext::{ + num::usize_to_i64_truncating, + pretty::{duration_display::DurationDisplay, join::PrettyJoiner, truncate::PrettyTruncator}, }; -pub use play::{AddToQueue, Autocomplete as PlayAutocomplete, File as PlayFile, Play}; + +#[allow(clippy::module_name_repetitions)] +pub use play::AddToQueue; +pub use play::{Autocomplete as PlayAutocomplete, File as PlayFile, Play}; pub use r#move::{Autocomplete as MoveAutocomplete, Move}; pub use remove::{Autocomplete as RemoveAutocomplete, Remove}; pub use remove_range::{Autocomplete as RemoveRangeAutocomplete, RemoveRange}; @@ -29,12 +34,12 @@ use crate::{ command::{ macros::{note_fol, out}, model::{GuildCtx, RespondViaMessage}, - require::Player, + require::PlayerInterface, }, core::{ model::{CacheAware, InteractionClient}, r#const::{ - discord::COMMAND_CHOICES_LIMIT, misc::ADD_TRACKS_WRAP_LIMIT, text::fuzzy_matcher, + discord::COMMAND_CHOICES_LIMIT, misc::ADD_TRACKS_WRAP_LIMIT, text::FUZZY_MATCHER, }, }, error::{component::queue::RemoveTracksError, PositionOutOfRange as PositionOutOfRangeError}, @@ -44,12 +49,12 @@ use crate::{ fn generate_position_choice( position: NonZeroUsize, track: &QueueItem, - ctx: &impl CacheAware, + cx: &impl CacheAware, ) -> CommandOptionChoice { - let track_info = &track.track().info; + let track_info = &track.data().info; let track_length = Duration::from_millis(track_info.length); - let requester = ctx.cache().user(track.requester()).map_or_else( - || "Unknown User".into(), + let requester = cx.cache().user(track.requester()).map_or_else( + || String::from("Unknown User"), |u| { u.global_name .clone() @@ -68,16 +73,16 @@ fn generate_position_choice( track_info.corrected_title().pretty_truncate(53) ), name_localizations: None, - value: CommandOptionChoiceValue::Integer(position.get() as i64), + value: CommandOptionChoiceValue::Integer(usize_to_i64_truncating(position.get())), } } -fn generate_position_choices<'a>( +pub fn generate_position_choices<'a>( position: NonZeroUsize, queue_len: NonZeroUsize, queue_iter: impl Iterator + Clone, excluded: &HashSet, - ctx: &impl CacheAware, + cx: &impl CacheAware, ) -> Vec { impl_generate_position_choices( queue_iter @@ -85,7 +90,7 @@ fn generate_position_choices<'a>( .skip_while(|(p, _)| *p < position) .take(queue_len.get()), excluded, - ctx, + cx, ) } @@ -94,7 +99,7 @@ fn generate_position_choices_reversed<'a>( queue_len: NonZeroUsize, queue_iter: impl Clone + DoubleEndedIterator, excluded: &HashSet, - ctx: &impl CacheAware, + cx: &impl CacheAware, ) -> Vec { impl_generate_position_choices( queue_iter @@ -103,85 +108,86 @@ fn generate_position_choices_reversed<'a>( .skip_while(|(p, _)| *p > position) .take(queue_len.get()), excluded, - ctx, + cx, ) } fn impl_generate_position_choices<'a>( queue_iter: impl Iterator + Clone, excluded: &HashSet, - ctx: &impl CacheAware, + cx: &impl CacheAware, ) -> Vec { queue_iter .filter(|(p, _)| !excluded.contains(p)) .take(COMMAND_CHOICES_LIMIT) - .map(|(p, t)| generate_position_choice(p, t, ctx)) + .map(|(p, t)| generate_position_choice(p, t, cx)) .collect() } -fn generate_position_choices_from_input<'a>( +pub fn generate_position_choices_from_input<'a>( input: i64, queue_len: NonZeroUsize, queue_iter: impl Clone + DoubleEndedIterator, excluded: &HashSet, - ctx: &impl CacheAware, + cx: &impl CacheAware, ) -> Vec { normalize_queue_position(input, queue_len) .filter(|p| !excluded.contains(p)) .map_or_else(Vec::new, |position| { if input.is_positive() { - return generate_position_choices(position, queue_len, queue_iter, excluded, ctx); + return generate_position_choices(position, queue_len, queue_iter, excluded, cx); } - generate_position_choices_reversed(position, queue_len, queue_iter, excluded, ctx) + generate_position_choices_reversed(position, queue_len, queue_iter, excluded, cx) }) } -fn generate_position_choices_from_fuzzy_match<'a>( +pub fn generate_position_choices_from_fuzzy_match<'a>( focused: &str, queue_iter: impl Iterator, excluded: &HashSet, - ctx: &impl CacheAware, + cx: &impl CacheAware, ) -> Vec { let queue_iter = queue_iter .filter_map(|(p, t)| { - let track_info = &t.track().info; + let track_info = &t.data().info; let author = track_info.corrected_author(); let title = track_info.corrected_title(); let requester = t.requester(); Some(( p, t, - fuzzy_matcher().fuzzy_match(&format!("{requester} {author} {title}",), focused)?, + FUZZY_MATCHER.fuzzy_match(&format!("{requester} {author} {title}",), focused)?, )) }) .sorted_by_key(|(_, _, s)| -s) .map(|(p, t, _)| (p, t)); - impl_generate_position_choices(queue_iter, excluded, ctx) + impl_generate_position_choices(queue_iter, excluded, cx) } fn normalize_queue_position(position: i64, queue_len: NonZeroUsize) -> Option { - (1..=queue_len.get()) - .contains(&(position.unsigned_abs() as usize)) - .then(|| { - NonZeroUsize::new( - position - .is_positive() - .then(|| position.unsigned_abs() as usize) - .unwrap_or_else(|| queue_len.get() - position.unsigned_abs() as usize + 1), - ) - })? + #[allow(clippy::cast_possible_truncation)] + let position_usize = position.unsigned_abs() as usize; + + (1..=queue_len.get()).contains(&position_usize).then(|| { + NonZeroUsize::new( + position + .is_positive() + .then_some(position_usize) + .unwrap_or_else(|| queue_len.get() - position_usize + 1), + ) + })? } -fn validate_input_positions( - inputs: &[i64], +pub const fn validate_input_position( + input: i64, queue_len: usize, ) -> Result<(), PositionOutOfRangeError> { - if let Some(&position) = inputs.iter().find(|&i| !(1..=queue_len as i64).contains(i)) { + if 1 > input || input > usize_to_i64_truncating(queue_len) { return Err(if queue_len == 1 { - PositionOutOfRangeError::OnlyTrack(position) + PositionOutOfRangeError::OnlyTrack(input) } else { PositionOutOfRangeError::OutOfRange { - position, + position: input, queue_len, } }); @@ -190,19 +196,33 @@ fn validate_input_positions( Ok(()) } +fn validate_input_positions( + inputs: &[i64], + queue_len: usize, +) -> Result<(), PositionOutOfRangeError> { + inputs + .iter() + .try_for_each(|&input| validate_input_position(input, queue_len))?; + + Ok(()) +} + async fn remove_range( start: i64, end: i64, ctx: &mut GuildCtx, - player: &Player, + player: &PlayerInterface, ) -> Result<(), RemoveTracksError> { + #[allow(clippy::cast_possible_truncation)] + let (start_usize, end_usize) = (start.unsigned_abs() as usize, end.unsigned_abs() as usize); + let data = player.data(); let mut data_w = data.write().await; let queue = data_w.queue_mut(); - let range = (start - 1) as usize..=(end - 1) as usize; + let range = (start_usize - 1)..end_usize; let queue_len = queue.len(); - let positions_len = (end - start) as usize + 1; + let positions_len = (end_usize - start_usize) + 1; let queue_cleared = positions_len > 1 && positions_len == queue_len; let removed = if queue_cleared { queue.drain_all().collect::>() @@ -210,8 +230,8 @@ async fn remove_range( queue.drain(range).collect() }; - let positions = (start..=end) - .filter_map(|p| NonZeroUsize::new(p as usize)) + let positions = (start_usize..=end_usize) + .filter_map(NonZeroUsize::new) .collect(); drop(data_w); @@ -221,7 +241,7 @@ async fn remove_range( async fn remove( positions: Box<[NonZeroUsize]>, ctx: &mut GuildCtx, - player: &Player, + player: &PlayerInterface, ) -> Result<(), RemoveTracksError> { let data = player.data(); let mut data_w = data.write().await; @@ -240,12 +260,13 @@ async fn remove( impl_remove(positions, removed, queue_cleared, ctx, player).await } +#[allow(clippy::significant_drop_tightening)] async fn impl_remove( positions: Box<[NonZeroUsize]>, removed: Vec, queue_cleared: bool, ctx: &mut GuildCtx, - player: &Player, + player: &PlayerInterface, ) -> Result<(), RemoveTracksError> { let data = player.data(); let mut data_w = data.write().await; @@ -256,8 +277,8 @@ async fn impl_remove( 0 => String::new(), 1..=ADD_TRACKS_WRAP_LIMIT => removed .into_iter() - .map(|t| format!("`{}`", t.into_track().info.corrected_title())) - .collect::>() + .map(|t| format!("`{}`", t.into_data().info.corrected_title())) + .collect::>() .pretty_join_with_and(), _ => format!("`{removed_len} tracks`"), }; @@ -269,22 +290,21 @@ async fn impl_remove( _ => "**`≡-`**", }; - let current = queue.position(); + // SAFETY: `queue.index() + 1` is non-zero + let current = unsafe { NonZeroUsize::new_unchecked(queue.index() + 1) }; let before_current = positions.partition_point(|&i| i < current); *queue.index_mut() -= positions[..before_current].len(); if positions.binary_search(¤t).is_ok() { - queue.adjust_repeat_mode(); - let next = queue.current().map(|t| t.track().clone()); - - queue - .with_advance_lock_and_stopped(&player.context, |p| async move { - if let Some(ref next) = next { - p.play(next).await?; - } - Ok(()) - }) - .await?; + queue.downgrade_repeat_mode(); + let next = queue.current().map(QueueItem::data); + + if let Some(next) = next { + queue.acquire_advance_lock(); + player.context.play_now(next).await?; + } else { + player.acquire_advance_lock_and_stop_with(queue).await?; + } } out!(format!("{} Removed {}", minus, removed_text), ?ctx); diff --git a/lyra/src/component/queue/clear.rs b/lyra/src/component/queue/clear.rs index 5ae4952..bbe87b1 100644 --- a/lyra/src/component/queue/clear.rs +++ b/lyra/src/component/queue/clear.rs @@ -10,7 +10,9 @@ use crate::{ require, }, error::CommandResult, - lavalink::{Event, LavalinkAware}, + gateway::GuildIdAware, + lavalink::Event, + LavalinkAware, }; /// Clears the queue @@ -19,31 +21,31 @@ use crate::{ pub struct Clear; impl BotSlashCommand for Clear { + #[allow(clippy::significant_drop_tightening)] async fn run(self, ctx: SlashCtx) -> CommandResult { let mut ctx = require::guild(ctx)?; let in_voice = require::in_voice(&ctx)?.and_unsuppressed()?; - let connection = ctx.lavalink().connection_from(&in_voice); - let in_voice_with_user = check::in_voice_with_user(in_voice)?; - let player = require::player(&ctx)?.and_queue_not_empty().await?; + let connection = ctx.lavalink().try_get_connection(ctx.guild_id())?; + let in_voice_with_user = check::user_in(in_voice)?; + let player = require::player(&ctx)?; let data = player.data(); { let data_r = data.read().await; - let queue = data_r.queue(); + let queue = require::queue_not_empty(&data_r)?; let positions = (1..=queue.len()).filter_map(NonZeroUsize::new); - check::all_users_track(positions, in_voice_with_user, queue, &ctx)?; + check::all_users_track(queue, positions, in_voice_with_user)?; - queue.stop_with_advance_lock(&player.context).await?; + player.acquire_advance_lock_and_stop_with(queue).await?; connection.dispatch(Event::QueueClear); drop(connection); } { let mut data_w = data.write().await; - let queue = data_w.queue_mut(); - queue.clear(); + data_w.queue_mut().clear(); } - out!("💥 Cleared the queue", ctx); + out!("⏹️ Cleared the queue", ctx); } } diff --git a/lyra/src/component/queue/fair_queue.rs b/lyra/src/component/queue/fair_queue.rs index b35cb09..e3ef03e 100644 --- a/lyra/src/component/queue/fair_queue.rs +++ b/lyra/src/component/queue/fair_queue.rs @@ -21,10 +21,12 @@ impl BotSlashCommand for FairQueue { let mut ctx = require::guild(ctx)?; check::user_is_dj(&ctx)?; let _ = require::in_voice(&ctx)?.and_with_someone_else()?; - let player = require::player(&ctx)?.and_queue_not_empty().await?; + let data = require::player(&ctx)?.data(); - let data = player.data(); - let indexer_type = data.read().await.queue().indexer_type(); + let data_r = data.read().await; + let queue = require::queue_not_empty(&data_r)?; + let indexer_type = queue.indexer_type(); + drop(data_r); match indexer_type { IndexerType::Fair => { diff --git a/lyra/src/component/queue/move.rs b/lyra/src/component/queue/move.rs index d077327..4acc55e 100644 --- a/lyra/src/component/queue/move.rs +++ b/lyra/src/component/queue/move.rs @@ -13,7 +13,8 @@ use crate::{ component::queue::normalize_queue_position, core::model::CacheAware, error::{command::AutocompleteResult, CommandResult}, - lavalink::{CorrectTrackInfo, PlayerAware}, + lavalink::CorrectTrackInfo, + LavalinkAndGuildIdAware, }; enum MoveAutocompleteOptionType { @@ -28,11 +29,12 @@ struct MoveAutocompleteOptions { kind: MoveAutocompleteOptionType, } -async fn generate_move_autocomplete_choices( +#[allow(clippy::significant_drop_tightening)] +async fn generate_move_choices( options: &MoveAutocompleteOptions, - ctx: &(impl PlayerAware + CacheAware + Sync), + cx: &(impl LavalinkAndGuildIdAware + CacheAware + Sync), ) -> Vec { - let Ok(player) = require::player(ctx) else { + let Ok(player) = require::player(cx) else { return Vec::new(); }; let data = player.data(); @@ -41,10 +43,7 @@ async fn generate_move_autocomplete_choices( return Vec::new(); }; - let queue_iter = queue - .iter() - .enumerate() - .filter_map(|(i, t)| NonZeroUsize::new(i + 1).map(|i| (i, t))); + let queue_iter = queue.iter_positions_and_items(); let excluded = match options.kind { MoveAutocompleteOptionType::TrackFocused | MoveAutocompleteOptionType::PositionFocused => { @@ -67,9 +66,9 @@ async fn generate_move_autocomplete_choices( }; match options.focused.parse::() { - Ok(input) => super::generate_position_choices_from_input( - input, queue_len, queue_iter, &excluded, ctx, - ), + Ok(input) => { + super::generate_position_choices_from_input(input, queue_len, queue_iter, &excluded, cx) + } Err(e) if matches!(e.kind(), std::num::IntErrorKind::Empty) => match options.kind { MoveAutocompleteOptionType::TrackFocused | MoveAutocompleteOptionType::TrackFocusedPositionCompleted(_) => { @@ -78,13 +77,13 @@ async fn generate_move_autocomplete_choices( queue_len, queue_iter, &excluded, - ctx, + cx, ) } MoveAutocompleteOptionType::PositionFocused | MoveAutocompleteOptionType::PositionFocusedTrackCompleted(_) => { super::generate_position_choices_reversed( - queue_len, queue_len, queue_iter, &excluded, ctx, + queue_len, queue_len, queue_iter, &excluded, cx, ) } }, @@ -92,7 +91,7 @@ async fn generate_move_autocomplete_choices( &options.focused, queue_iter, &excluded, - ctx, + cx, ), } } @@ -131,7 +130,7 @@ impl BotAutocomplete for Autocomplete { focused: focused.into_boxed_str(), kind, }; - let choices = generate_move_autocomplete_choices(&options, &ctx).await; + let choices = generate_move_choices(&options, &ctx).await; Ok(ctx.autocomplete(choices).await?) } } @@ -149,15 +148,15 @@ pub struct Move { } impl BotSlashCommand for Move { + #[allow(clippy::significant_drop_tightening)] async fn run(self, ctx: SlashCtx) -> CommandResult { let mut ctx = require::guild(ctx)?; - let in_voice_with_user = - check::in_voice_with_user(require::in_voice(&ctx)?.and_unsuppressed()?)?; - let player = require::player(&ctx)?.and_queue_not_empty().await?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; let data = player.data(); let mut data_w = data.write().await; - let queue = data_w.queue_mut(); + let queue = require::queue_not_empty_mut(&mut data_w)?; let queue_len = queue.len(); if queue_len == 1 { @@ -167,7 +166,8 @@ impl BotSlashCommand for Move { ); } - super::validate_input_positions(&[self.track, self.position], queue_len)?; + super::validate_input_position(self.track, queue_len)?; + super::validate_input_position(self.position, queue_len)?; if self.track == self.position { bad!( @@ -176,17 +176,24 @@ impl BotSlashCommand for Move { ); } + #[allow(clippy::cast_possible_truncation)] + let (track_usize, position_usize) = ( + self.track.unsigned_abs() as usize, + self.position.unsigned_abs() as usize, + ); + // SAFETY: `self.track as usize` is in range [1, +inf), so it is non-zero - let position = unsafe { NonZeroUsize::new_unchecked(self.position as usize) }; - check::users_track(position, in_voice_with_user, queue, &ctx)?; + let position = unsafe { NonZeroUsize::new_unchecked(position_usize) }; + let track = &queue[position]; + check::track_is_users(track, position, in_voice_with_user)?; // SAFETY: `self.track as usize` is in range [1, +inf), so it is non-zero - let track_position = unsafe { NonZeroUsize::new_unchecked(self.track as usize) }; + let track_position = unsafe { NonZeroUsize::new_unchecked(track_usize) }; let queue_position = queue.position(); // SAFETY: `track_position.get() - 1` has been validated to be in-bounds, so this unwrap is safe let track = unsafe { queue.remove(track_position.get() - 1).unwrap_unchecked() }; - let track_title = track.track().info.corrected_title(); + let track_title = track.data().info.corrected_title(); let message = format!("⤴️ Moved `{track_title}` to position **`{position}`**"); let insert_position = position.get() - 1; diff --git a/lyra/src/component/queue/play.rs b/lyra/src/component/queue/play.rs index 97232a1..b6af833 100644 --- a/lyra/src/component/queue/play.rs +++ b/lyra/src/component/queue/play.rs @@ -12,9 +12,7 @@ use lavalink_rs::{ use linkify::{LinkFinder, LinkKind}; use lyra_ext::{ as_grapheme::AsGrapheme, - pretty::{ - duration_display::PrettyDurationDisplay, join::PrettyJoiner, truncate::PrettyTruncator, - }, + pretty::{duration_display::DurationDisplay, join::PrettyJoiner, truncate::PrettyTruncator}, }; use twilight_interactions::command::{ AutocompleteValue, CommandModel, CommandOption, CreateCommand, CreateOption, @@ -28,7 +26,7 @@ use twilight_util::builder::command::CommandBuilder; use crate::{ command::{ - macros::{bad, crit, out_or_fol, what}, + macros::{bad, bad_or_fol, crit_or_fol, out_or_fol, what_or_fol}, model::{BotAutocomplete, BotMessageCommand, BotSlashCommand, GuildCtx, RespondViaMessage}, require, util, AutocompleteCtx, MessageCtx, SlashCtx, }, @@ -42,10 +40,8 @@ use crate::{ CommandResult, LoadFailed as LoadFailedError, }, gateway::GuildIdAware, - lavalink::{ - CorrectPlaylistInfo, CorrectTrackInfo, LavalinkAware, UnwrappedPlayerData, - UnwrappedPlayerInfoUri, - }, + lavalink::{CorrectPlaylistInfo, CorrectTrackInfo, UnwrappedData, UnwrappedPlayerInfoUri}, + LavalinkAware, }; struct LoadTrackContext { @@ -54,10 +50,10 @@ struct LoadTrackContext { } impl LoadTrackContext { - fn new_via(ctx: &(impl GuildIdAware + LavalinkAware)) -> Self { + fn new_via(cx: &(impl GuildIdAware + LavalinkAware)) -> Self { Self { - guild_id: ctx.guild_id(), - lavalink: ctx.lavalink().clone_inner(), + guild_id: cx.guild_id(), + lavalink: cx.lavalink().clone_inner(), } } } @@ -240,7 +236,7 @@ impl BotAutocomplete for Autocomplete { }) .map(|q| { let source = self.source.unwrap_or_default(); - (!regex::url().is_match(&q)) + (!regex::URL.is_match(&q)) .then(|| format!("{}{}", source.value(), q).into_boxed_str()) .unwrap_or_else(|| q.into_boxed_str()) }) @@ -315,7 +311,7 @@ impl BotAutocomplete for Autocomplete { choices } - TrackLoadType::Error => Err(LoadFailedError(query))?, + TrackLoadType::Error => return Err(LoadFailedError(query).into()), TrackLoadType::Empty => Vec::new(), }; @@ -327,6 +323,7 @@ async fn play( ctx: &mut GuildCtx, queries: impl IntoIterator> + Send, ) -> Result<(), play::Error> { + ctx.defer().await?; let load_ctx = LoadTrackContext::new_via(ctx); match load_ctx.process_many(queries).await { Ok(results) => { @@ -388,10 +385,11 @@ async fn play( let total_tracks = Vec::from(results); // SAFETY: at least one tracks must be loaded, so this unwrap is safe - let first_track = unsafe { total_tracks.first().cloned().unwrap_unchecked() }; + let first_track = unsafe { total_tracks.first().unwrap_unchecked() }; let player = util::auto_new_player(ctx).await?; + player.play(first_track).await?; player .data_unwrapped() .write() @@ -399,20 +397,18 @@ async fn play( .queue_mut() .enqueue(total_tracks, ctx.author_id()); - player.play(&first_track).await?; - out_or_fol!(format!("{} Added {}", plus, enqueued_text), ctx); } Err(e) => match e { LoadTrackProcessManyError::Query(query) => match query { QueryError::LoadFailed(LoadFailedError(query)) => { - crit!(format!("Failed to load tracks for query: `{}`", query), ctx); + crit_or_fol!(format!("Failed to load tracks for query: `{}`", query), ctx); } QueryError::NoMatches(query) => { - what!(format!("No matches found for query: `{}`", query), ctx); + what_or_fol!(format!("No matches found for query: `{}`", query), ctx); } QueryError::SearchResult(query) => { - bad!( + bad_or_fol!( format!( "Given query is not a URL: `{}`. Try using the command's autocomplete to search for tracks.", query @@ -421,7 +417,7 @@ async fn play( ); } }, - LoadTrackProcessManyError::Lavalink(e) => Err(e)?, + LoadTrackProcessManyError::Lavalink(e) => Err(e.into()), }, } } diff --git a/lyra/src/component/queue/remove.rs b/lyra/src/component/queue/remove.rs index 17cc6ca..f836fbe 100644 --- a/lyra/src/component/queue/remove.rs +++ b/lyra/src/component/queue/remove.rs @@ -15,15 +15,16 @@ use crate::{ }, core::model::CacheAware, error::{command::AutocompleteResult, CommandResult}, - lavalink::PlayerAware, + LavalinkAndGuildIdAware, }; +#[allow(clippy::significant_drop_tightening)] async fn generate_remove_choices( focused: &str, finished: Vec, - ctx: &(impl CacheAware + PlayerAware + Sync), + cx: &(impl CacheAware + LavalinkAndGuildIdAware + Sync), ) -> Vec { - let Ok(player) = require::player(ctx) else { + let Ok(player) = require::player(cx) else { return Vec::new(); }; let data = player.data(); @@ -37,24 +38,17 @@ async fn generate_remove_choices( .filter_map(|i| super::normalize_queue_position(i, queue_len)) .collect::>(); - let queue_iter = queue - .iter() - .enumerate() - .filter_map(|(i, t)| NonZeroUsize::new(i + 1).map(|i| (i, t))); + let queue_iter = queue.iter_positions_and_items(); match focused.parse::() { - Ok(input) => super::generate_position_choices_from_input( - input, queue_len, queue_iter, &excluded, ctx, - ), - Err(e) if matches!(e.kind(), IntErrorKind::Empty) => super::generate_position_choices( - queue.position(), - queue_len, - queue_iter, - &excluded, - ctx, - ), + Ok(input) => { + super::generate_position_choices_from_input(input, queue_len, queue_iter, &excluded, cx) + } + Err(e) if matches!(e.kind(), IntErrorKind::Empty) => { + super::generate_position_choices(queue.position(), queue_len, queue_iter, &excluded, cx) + } Err(_) => { - super::generate_position_choices_from_fuzzy_match(focused, queue_iter, &excluded, ctx) + super::generate_position_choices_from_fuzzy_match(focused, queue_iter, &excluded, cx) } } } @@ -128,14 +122,12 @@ pub struct Remove { impl BotSlashCommand for Remove { async fn run(self, ctx: SlashCtx) -> CommandResult { let mut ctx = require::guild(ctx)?; - let in_voice_with_user = - check::in_voice_with_user(require::in_voice(&ctx)?.and_unsuppressed()?)?; - let player = require::player(&ctx)?.and_queue_not_empty().await?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; let data = player.data(); let data_r = data.read().await; - let queue = data_r.queue(); - let queue_len = queue.len(); + let queue = require::queue_not_empty(&data_r)?; let inputs = [ Some(self.track), @@ -149,14 +141,15 @@ impl BotSlashCommand for Remove { .unique() .collect::>(); - super::validate_input_positions(&inputs, queue_len)?; + super::validate_input_positions(&inputs, queue.len())?; + #[allow(clippy::cast_possible_truncation)] let mut positions = inputs .iter() - .filter_map(|p| NonZeroUsize::new(*p as usize)) + .filter_map(|&p| NonZeroUsize::new(p.unsigned_abs() as usize)) .collect::>(); - check::all_users_track(positions.iter().copied(), in_voice_with_user, queue, &ctx)?; + check::all_users_track(queue, positions.iter().copied(), in_voice_with_user)?; positions.sort_unstable(); diff --git a/lyra/src/component/queue/remove_range.rs b/lyra/src/component/queue/remove_range.rs index d5fcf36..cd44917 100644 --- a/lyra/src/component/queue/remove_range.rs +++ b/lyra/src/component/queue/remove_range.rs @@ -3,6 +3,7 @@ use std::{ num::{IntErrorKind, NonZeroUsize}, }; +use lyra_ext::num::usize_to_i64_truncating; use twilight_interactions::command::{AutocompleteValue, CommandModel, CreateCommand}; use twilight_model::application::command::CommandOptionChoice; @@ -16,7 +17,7 @@ use crate::{ component::queue::Remove, core::model::{CacheAware, InteractionClient}, error::{command::AutocompleteResult, CommandResult}, - lavalink::PlayerAware, + LavalinkAndGuildIdAware, }; enum RemoveRangeAutocompleteOptionsType { @@ -31,11 +32,12 @@ struct RemoveRangeAutocompleteOptions { kind: RemoveRangeAutocompleteOptionsType, } +#[allow(clippy::significant_drop_tightening)] async fn generate_remove_range_autocomplete_choices( options: &RemoveRangeAutocompleteOptions, - ctx: &(impl CacheAware + PlayerAware + Sync), + cx: &(impl CacheAware + LavalinkAndGuildIdAware + Sync), ) -> Vec { - let Ok(player) = require::player(ctx) else { + let Ok(player) = require::player(cx) else { return Vec::new(); }; let data = player.data(); @@ -44,10 +46,7 @@ async fn generate_remove_range_autocomplete_choices( return Vec::new(); }; - let queue_iter = queue - .iter() - .enumerate() - .filter_map(|(i, t)| NonZeroUsize::new(i + 1).map(|i| (i, t))); + let queue_iter = queue.iter_positions_and_items(); let excluded = match options.kind { RemoveRangeAutocompleteOptionsType::StartFocused @@ -71,9 +70,9 @@ async fn generate_remove_range_autocomplete_choices( }; match options.focused.parse::() { - Ok(input) => super::generate_position_choices_from_input( - input, queue_len, queue_iter, &excluded, ctx, - ), + Ok(input) => { + super::generate_position_choices_from_input(input, queue_len, queue_iter, &excluded, cx) + } Err(e) if matches!(e.kind(), IntErrorKind::Empty) => match options.kind { RemoveRangeAutocompleteOptionsType::StartFocused | RemoveRangeAutocompleteOptionsType::StartFocusedEndCompleted(_) => { @@ -82,13 +81,13 @@ async fn generate_remove_range_autocomplete_choices( queue_len, queue_iter, &excluded, - ctx, + cx, ) } RemoveRangeAutocompleteOptionsType::EndFocused | RemoveRangeAutocompleteOptionsType::EndFocusedStartCompleted(_) => { super::generate_position_choices_reversed( - queue_len, queue_len, queue_iter, &excluded, ctx, + queue_len, queue_len, queue_iter, &excluded, cx, ) } }, @@ -96,7 +95,7 @@ async fn generate_remove_range_autocomplete_choices( &options.focused, queue_iter, &excluded, - ctx, + cx, ), } } @@ -155,15 +154,14 @@ pub struct RemoveRange { impl BotSlashCommand for RemoveRange { async fn run(self, ctx: SlashCtx) -> CommandResult { let mut ctx = require::guild(ctx)?; - let in_voice_with_user = - check::in_voice_with_user(require::in_voice(&ctx)?.and_unsuppressed()?)?; - let player = require::player(&ctx)?.and_queue_not_empty().await?; + let in_voice_with_user = check::user_in(require::in_voice(&ctx)?.and_unsuppressed()?)?; + let player = require::player(&ctx)?; let data = player.data(); let data_r = data.read().await; - let queue = data_r.queue(); - let queue_len = queue.len(); + let queue = require::queue_not_empty(&data_r)?; + let queue_len = queue.len(); if queue_len == 1 { let remove = InteractionClient::mention_command::(); @@ -173,10 +171,11 @@ impl BotSlashCommand for RemoveRange { ); } - super::validate_input_positions(&[self.start, self.end], queue_len)?; + super::validate_input_position(self.start, queue_len)?; + super::validate_input_position(self.end, queue_len)?; if self.end <= self.start { - let message = if self.end == queue_len as i64 { + let message = if self.end == usize_to_i64_truncating(queue_len) { format!( "Invalid starting position: `{}`; Starting position must be from `1` to `{}`.", self.start, @@ -194,8 +193,10 @@ impl BotSlashCommand for RemoveRange { bad!(message, ctx); } - let positions = (self.start..=self.end).filter_map(|p| NonZeroUsize::new(p as usize)); - check::all_users_track(positions, in_voice_with_user, queue, &ctx)?; + #[allow(clippy::cast_possible_truncation)] + let positions = + (self.start..=self.end).filter_map(|p| NonZeroUsize::new(p.unsigned_abs() as usize)); + check::all_users_track(queue, positions, in_voice_with_user)?; drop(data_r); Ok(super::remove_range(self.start, self.end, &mut ctx, &player).await?) diff --git a/lyra/src/component/queue/repeat.rs b/lyra/src/component/queue/repeat.rs index ca15af8..e55bac6 100644 --- a/lyra/src/component/queue/repeat.rs +++ b/lyra/src/component/queue/repeat.rs @@ -2,16 +2,16 @@ use twilight_interactions::command::{CommandModel, CommandOption, CreateCommand, use crate::{ command::{ - check, + check::{self, ResolveWithPoll, StartPoll}, macros::out_or_upd, model::BotSlashCommand, poll::Topic, - require::{self, PartialInVoice}, - SlashCtx, + require, SlashCtx, }, error::CommandResult, gateway::GuildIdAware, - lavalink::{self, DelegateMethods, LavalinkAware}, + lavalink::{DelegateMethods, Event, RepeatMode as LavalinkRepeatMode}, + LavalinkAware, }; #[derive(CommandOption, CreateOption)] @@ -24,7 +24,7 @@ enum RepeatMode { Track, } -impl From for lavalink::RepeatMode { +impl From for LavalinkRepeatMode { fn from(value: RepeatMode) -> Self { match value { RepeatMode::Off => Self::Off, @@ -51,30 +51,31 @@ impl BotSlashCommand for Repeat { mode.into() } else { let mode = match ctx.lavalink().get_player_data(guild_id) { - Some(data) => data.write().await.queue().repeat_mode(), - None => lavalink::RepeatMode::Off, + Some(data) => data.read().await.queue().repeat_mode(), + None => LavalinkRepeatMode::Off, }; mode.next() } }; let in_voice = require::in_voice(&ctx)?; - let in_voice_cacheless = PartialInVoice::from(&in_voice); - let player = require::player(&ctx)?.and_queue_not_empty().await?; - check::in_voice_with_user(in_voice)? - .only_else_poll(Topic::Repeat(mode))? - .start(&mut ctx) + let player = require::player(&ctx)?; + let data = player.data(); + + let data_r = data.read().await; + require::queue_not_empty(&data_r)?; + drop(data_r); + + check::user_in(in_voice)? + .only() + .or_else_try_resolve_with(Topic::Repeat(mode))? + .and_then_start(&mut ctx) .await?; ctx.lavalink() - .connection_from(&in_voice_cacheless) - .dispatch(lavalink::Event::QueueRepeat); - player - .data() - .write() - .await - .queue_mut() - .set_repeat_mode(mode); + .try_get_connection(guild_id)? + .dispatch(Event::QueueRepeat); + data.write().await.queue_mut().set_repeat_mode(mode); let txt = &format!("{} {}", mode.emoji(), mode); out_or_upd!(txt, ctx); diff --git a/lyra/src/component/queue/shuffle.rs b/lyra/src/component/queue/shuffle.rs index 4854c95..67b1d8a 100644 --- a/lyra/src/component/queue/shuffle.rs +++ b/lyra/src/component/queue/shuffle.rs @@ -19,12 +19,14 @@ pub struct Shuffle; impl BotSlashCommand for Shuffle { async fn run(self, ctx: SlashCtx) -> CommandResult { let mut ctx = require::guild(ctx)?; - check::in_voice_with_user(require::in_voice(&ctx)?)?.only()?; - let player = require::player(&ctx)?.and_queue_not_empty().await?; - + check::user_in(require::in_voice(&ctx)?)?.only()?; + let player = require::player(&ctx)?; let data = player.data(); + let data_r = data.read().await; - let indexer_type = data_r.queue().indexer_type(); + let queue = require::queue_not_empty(&data_r)?; + let indexer_type = queue.indexer_type(); + drop(data_r); match indexer_type { IndexerType::Shuffled => { diff --git a/lyra/src/component/tuning.rs b/lyra/src/component/tuning.rs index 86b0877..a182295 100644 --- a/lyra/src/component/tuning.rs +++ b/lyra/src/component/tuning.rs @@ -15,18 +15,18 @@ use crate::{ command::{ check, model::{CtxKind, GuildCtx}, - require::{self, InVoice, Player}, + require::{self, InVoice, PlayerInterface}, }, core::model::{BotStateAware, HttpAware}, - error::CommandError, gateway::{voice, GuildIdAware}, - lavalink::{DelegateMethods, LavalinkAware}, + lavalink::DelegateMethods, + CommandError, LavalinkAware, }; #[inline] fn check_user_is_dj_and_require_unsuppressed_player( ctx: &GuildCtx, -) -> Result<(InVoice, Player), CommandError> { +) -> Result<(InVoice, PlayerInterface), CommandError> { check::user_is_dj(ctx)?; let in_voice = require::in_voice(ctx)?.and_unsuppressed()?; let player = require::player(ctx)?; @@ -45,7 +45,7 @@ fn unmuting_checks(ctx: &GuildCtx) -> Result, -) -> Result<(InVoice, Player), CommandError> { +) -> Result<(InVoice, PlayerInterface), CommandError> { check::user_is_dj(ctx)?; let in_voice = require::in_voice(ctx)?; let player = require::player(ctx)?; @@ -61,7 +61,7 @@ trait UpdateFilter { async fn update_filter(&self, update: impl ApplyFilter + Send + Sync) -> LavalinkResult<()>; } -impl UpdateFilter for Player { +impl UpdateFilter for PlayerInterface { async fn update_filter(&self, update: impl ApplyFilter + Send + Sync) -> LavalinkResult<()> { let old_filter = self.info().await?.filters.unwrap_or_default(); diff --git a/lyra/src/component/tuning/equaliser.rs b/lyra/src/component/tuning/equaliser.rs index 5a0f4a4..c119571 100644 --- a/lyra/src/component/tuning/equaliser.rs +++ b/lyra/src/component/tuning/equaliser.rs @@ -20,6 +20,7 @@ impl SetEqualiser { equaliser.map(|o| o.filter(|o| (o - Self::DEFAULT_GAIN).abs() > ERR_MARGIN)); equaliser.iter().any(Option::is_some).then(|| { Self(core::array::from_fn(|i| Equalizer { + #[allow(clippy::cast_possible_truncation)] band: i as u8, gain: equaliser[i].unwrap_or(Self::DEFAULT_GAIN), })) diff --git a/lyra/src/component/tuning/equaliser/preset.rs b/lyra/src/component/tuning/equaliser/preset.rs index 3d200aa..e3d37ea 100644 --- a/lyra/src/component/tuning/equaliser/preset.rs +++ b/lyra/src/component/tuning/equaliser/preset.rs @@ -14,6 +14,7 @@ impl From for SetEqualiser { let gains = value.gains(); Self(core::array::from_fn(|i| { lavalink_rs::model::player::Equalizer { + #[allow(clippy::cast_possible_truncation)] band: i as u8, gain: gains[i], } diff --git a/lyra/src/component/tuning/filter/pitch.rs b/lyra/src/component/tuning/filter/pitch.rs index 2c55955..0c5a9df 100644 --- a/lyra/src/component/tuning/filter/pitch.rs +++ b/lyra/src/component/tuning/filter/pitch.rs @@ -11,7 +11,7 @@ use lavalink_rs::{ use lyra_proc::BotCommandGroup; use twilight_interactions::command::{CommandModel, CreateCommand}; -use crate::{command::require::Player, lavalink::Pitch as PitchModel}; +use crate::{command::require::PlayerInterface, lavalink::Pitch as PitchModel}; enum Tier { Default, @@ -40,7 +40,7 @@ impl PitchModel { } async fn shift_pitch( - player: &Player, + player: &PlayerInterface, half_tones: NonZeroI64, ) -> LavalinkResult<(PitchModel, PitchModel)> { let old_filter = player.info().await?.filters.unwrap_or_default(); diff --git a/lyra/src/component/tuning/filter/pitch/set.rs b/lyra/src/component/tuning/filter/pitch/set.rs index 0beb344..c51cc03 100644 --- a/lyra/src/component/tuning/filter/pitch/set.rs +++ b/lyra/src/component/tuning/filter/pitch/set.rs @@ -75,7 +75,7 @@ impl BotSlashCommand for Set { let (_, player) = check_user_is_dj_and_require_unsuppressed_player(&ctx)?; let Some(update) = SetPitch::new(self.multiplier) else { - bad!("Multiplier must not be 0", ctx); + bad!("Multiplier must not be zero", ctx); }; let multiplier = update.multiplier(); diff --git a/lyra/src/component/tuning/speed.rs b/lyra/src/component/tuning/speed.rs index fdd88da..fa7d7e1 100644 --- a/lyra/src/component/tuning/speed.rs +++ b/lyra/src/component/tuning/speed.rs @@ -1,11 +1,15 @@ -use lavalink_rs::model::player::{Filters, Timescale}; +use lavalink_rs::{ + error::LavalinkResult, + model::player::{Filters, Timescale}, +}; use twilight_interactions::command::{CommandModel, CreateCommand}; use crate::{ command::{ macros::{bad, out}, model::BotSlashCommand, - require, SlashCtx, + require::{self, PlayerInterface}, + SlashCtx, }, component::tuning::{check_user_is_dj_and_require_unsuppressed_player, UpdateFilter}, error::CommandResult, @@ -89,6 +93,14 @@ impl ApplyFilter for SpeedFilter { } } +impl PlayerInterface { + async fn set_speed(&self, update: SpeedFilter) -> LavalinkResult<()> { + self.data().write().await.set_speed(update.multiplier()); + self.update_filter(update).await?; + Ok(()) + } +} + /// Sets the playback speed #[derive(CommandModel, CreateCommand)] #[command(name = "speed", dm_permission = false)] @@ -107,12 +119,12 @@ impl BotSlashCommand for Speed { let Some(update) = SpeedFilter::new(self.multiplier, self.pitch_shift.unwrap_or_default()) else { - bad!("Multiplier must not be 0", ctx); + bad!("Multiplier must not be zero", ctx); }; let multiplier = update.multiplier(); let emoji = update.tier().emoji(); - player.update_filter(update).await?; + player.set_speed(update).await?; out!( format!("{emoji} Set the playback speed to `{multiplier}`×."), diff --git a/lyra/src/component/tuning/volume/down.rs b/lyra/src/component/tuning/volume/down.rs index 7331791..8586fcb 100644 --- a/lyra/src/component/tuning/volume/down.rs +++ b/lyra/src/component/tuning/volume/down.rs @@ -8,7 +8,7 @@ use crate::{ core::model::{BotStateAware, HttpAware}, error::CommandResult, gateway::GuildIdAware, - lavalink::LavalinkAware, + LavalinkAware, }; /// Decrease the playback volume @@ -23,15 +23,16 @@ pub struct Down { impl BotSlashCommand for Down { async fn run(self, ctx: SlashCtx) -> CommandResult { let mut ctx = require::guild(ctx)?; - let (in_voice, player) = check_user_is_dj_and_require_unsuppressed_player(&ctx)?; + let (_, player) = check_user_is_dj_and_require_unsuppressed_player(&ctx)?; let guild_id = ctx.guild_id(); let data = player.data(); let old_percent = data.read().await.volume(); + #[allow(clippy::cast_possible_truncation)] let maybe_new_percent = old_percent .get() - .checked_sub(self.percent.unwrap_or(10) as u16) + .checked_sub(self.percent.unwrap_or(10).unsigned_abs() as u16) .and_then(NonZeroU16::new); let emoji = super::volume_emoji(maybe_new_percent); @@ -44,7 +45,7 @@ impl BotSlashCommand for Down { super::clipping_warning(new_percent), ) } else { - ctx.lavalink().connection_mut_from(&in_voice).mute = true; + ctx.lavalink().try_get_connection_mut(guild_id)?.mute = true; ctx.http() .update_guild_member(guild_id, ctx.bot().user_id()) .mute(true) diff --git a/lyra/src/component/tuning/volume/set.rs b/lyra/src/component/tuning/volume/set.rs index 94d98b1..a0036a5 100644 --- a/lyra/src/component/tuning/volume/set.rs +++ b/lyra/src/component/tuning/volume/set.rs @@ -23,16 +23,14 @@ impl BotSlashCommand for Set { let (_, player) = check_user_is_dj_and_require_unsuppressed_player(&ctx)?; // SAFETY: `self.percent as u16` is in range [1, 1_000], so it is non-zero - let percent = unsafe { NonZeroU16::new_unchecked(self.percent as u16) }; + #[allow(clippy::cast_possible_truncation)] + let percent = unsafe { NonZeroU16::new_unchecked(self.percent.unsigned_abs() as u16) }; player.context.set_volume(percent.get()).await?; player.data().write().await.set_volume(percent); let emoji = super::volume_emoji(Some(percent)); let warning = super::clipping_warning(percent); - out!( - format!("{emoji} Set playback volume to `{percent}`%{warning}."), - ctx - ); + out!(format!("{emoji} `{percent}`%{warning}"), ctx); } } diff --git a/lyra/src/component/tuning/volume/toggle_mute.rs b/lyra/src/component/tuning/volume/toggle_mute.rs index a214878..c663ac7 100644 --- a/lyra/src/component/tuning/volume/toggle_mute.rs +++ b/lyra/src/component/tuning/volume/toggle_mute.rs @@ -6,7 +6,7 @@ use crate::{ core::model::{BotStateAware, HttpAware}, error::CommandResult, gateway::GuildIdAware, - lavalink::LavalinkAware, + LavalinkAware, }; /// Toggles server muting the bot @@ -17,10 +17,10 @@ pub struct ToggleMute; impl BotSlashCommand for ToggleMute { async fn run(self, ctx: SlashCtx) -> CommandResult { let mut ctx = require::guild(ctx)?; - let in_voice = unmuting_checks(&ctx)?; + let _ = unmuting_checks(&ctx)?; let guild_id = ctx.guild_id(); - let mut connection = ctx.lavalink().connection_mut_from(&in_voice); + let mut connection = ctx.lavalink().try_get_connection_mut(guild_id)?; let mute = !connection.mute; ctx.http() diff --git a/lyra/src/component/tuning/volume/up.rs b/lyra/src/component/tuning/volume/up.rs index 927a9ec..9e644c6 100644 --- a/lyra/src/component/tuning/volume/up.rs +++ b/lyra/src/component/tuning/volume/up.rs @@ -12,7 +12,7 @@ use crate::{ core::model::{BotStateAware, HttpAware}, error::CommandResult, gateway::GuildIdAware, - lavalink::LavalinkAware, + LavalinkAware, }; /// Increase the playback volume @@ -26,18 +26,20 @@ pub struct Up { impl BotSlashCommand for Up { async fn run(self, ctx: SlashCtx) -> CommandResult { + // SAFETY: `1_000` is non-zero + const MAX_PERCENT: NonZeroU16 = unsafe { NonZeroU16::new_unchecked(1_000) }; + let mut ctx = require::guild(ctx)?; - let (in_voice, player) = check_user_is_dj_and_require_player(&ctx)?; + let (_, player) = check_user_is_dj_and_require_player(&ctx)?; let lavalink = ctx.lavalink(); let guild_id = ctx.guild_id(); let data = player.data(); - let percent_u16 = self.percent.unwrap_or(10) as u16; + #[allow(clippy::cast_possible_truncation)] + let percent_u16 = self.percent.unwrap_or(10).unsigned_abs() as u16; - // SAFETY: `1_000` is non-zero - let max_percent = unsafe { NonZeroU16::new_unchecked(1_000) }; - let (old_percent_str, new_percent) = if lavalink.connection_from(&in_voice).mute { - lavalink.connection_mut_from(&in_voice).mute = false; + let (old_percent_str, new_percent) = if lavalink.try_get_connection(guild_id)?.mute { + lavalink.try_get_connection_mut(guild_id)?.mute = false; ctx.http() .update_guild_member(guild_id, ctx.bot().user_id()) .mute(false) @@ -51,20 +53,20 @@ impl BotSlashCommand for Up { } else { let old_percent = data.read().await.volume(); - if old_percent >= max_percent { + if old_percent >= MAX_PERCENT { note!("Already at max playback volume.", ctx); } ( format!("`{old_percent}%`"), - old_percent.saturating_add(percent_u16).min(max_percent), + old_percent.saturating_add(percent_u16).min(MAX_PERCENT), ) }; let emoji = super::volume_emoji(Some(new_percent)); let warning = super::clipping_warning(new_percent); - let maxed_note = (new_percent == max_percent) + let maxed_note = (new_percent == MAX_PERCENT) .then_some(" (`Max`)") .unwrap_or_default(); diff --git a/lyra/src/core/const.rs b/lyra/src/core/const.rs index efbbbd8..53edb40 100644 --- a/lyra/src/core/const.rs +++ b/lyra/src/core/const.rs @@ -1,5 +1,5 @@ pub mod metadata { - use std::sync::OnceLock; + use std::sync::LazyLock; const VERSION: &str = env!("CARGO_PKG_VERSION"); const COPYRIGHT: &str = env!("CARGO_PKG_LICENSE"); @@ -54,68 +54,45 @@ pub mod metadata { "%cargo_opt_level", ]; - pub fn banner() -> &'static str { - static BANNER: OnceLock<&'static str> = OnceLock::new(); - BANNER.get_or_init(|| { - use aho_corasick::AhoCorasick; - - let rdr = include_str!("../../../assets/lyra2-ascii.ans"); - let mut wtr = Vec::new(); - - let ac = AhoCorasick::new(METADATA_PATTERNS).expect("METADATA_PATTERNS is valid"); - ac.try_stream_replace_all(rdr.as_bytes(), &mut wtr, &METADATA_REPLACEMENTS) - .expect("searching is infallible"); - // SAFETY: since `rdr` is utf-8, `wtr` must also be utf-8 - unsafe { String::from_utf8_unchecked(wtr).leak() } - }) - } + pub static BANNER: LazyLock<&'static str> = LazyLock::new(|| { + use aho_corasick::AhoCorasick; + + let rdr = include_str!("../../../assets/lyra2-ascii.ans"); + let mut wtr = Vec::new(); + + let ac = AhoCorasick::new(METADATA_PATTERNS).expect("METADATA_PATTERNS is valid"); + ac.try_stream_replace_all(rdr.as_bytes(), &mut wtr, &METADATA_REPLACEMENTS) + .expect("searching is infallible"); + // SAFETY: since `rdr` is utf-8, `wtr` must also be utf-8 + unsafe { String::from_utf8_unchecked(wtr).leak() } + }); } pub mod connection { - use std::{sync::OnceLock, time::Duration}; + use std::time::Duration; pub const INACTIVITY_TIMEOUT_SECS: u16 = 600; pub const INACTIVITY_TIMEOUT_POLL_N: u8 = 10; - pub fn connection_changed_timeout() -> &'static Duration { - static CONNECTION_CHANGED_TIMEOUT: OnceLock = OnceLock::new(); - CONNECTION_CHANGED_TIMEOUT.get_or_init(|| Duration::from_millis(500)) - } - - pub fn get_lavalink_connection_info_timeout() -> &'static Duration { - static GET_LAVALINK_CONNECTION_INFO_TIMEOUT: OnceLock = OnceLock::new(); - GET_LAVALINK_CONNECTION_INFO_TIMEOUT.get_or_init(|| Duration::from_millis(2_000)) - } - - pub fn inactivity_timeout_poll_interval() -> &'static Duration { - static INACTIVITY_TIMEOUT_POLL_INTERVAL: OnceLock = OnceLock::new(); - INACTIVITY_TIMEOUT_POLL_INTERVAL.get_or_init(|| { - Duration::from_secs( - u64::from(INACTIVITY_TIMEOUT_SECS) / u64::from(INACTIVITY_TIMEOUT_POLL_N), - ) - }) - } + pub const CHANGED_TIMEOUT: Duration = Duration::from_millis(250); + pub const GET_LAVALINK_CONNECTION_INFO_TIMEOUT: Duration = Duration::from_millis(2_000); + pub const INACTIVITY_TIMEOUT_POLL_INTERVAL: Duration = + Duration::from_secs(INACTIVITY_TIMEOUT_SECS as u64 / INACTIVITY_TIMEOUT_POLL_N as u64); } pub mod misc { - use std::{sync::OnceLock, time::Duration}; + use std::time::Duration; pub const ADD_TRACKS_WRAP_LIMIT: usize = 3; pub const WAIT_FOR_NOT_SUPPRESSED_TIMEOUT_SECS: u8 = 30; - pub fn wait_for_bot_events_timeout() -> &'static Duration { - static WAIT_FOR_BOT_EVENTS_TIMEOUT: OnceLock = OnceLock::new(); - WAIT_FOR_BOT_EVENTS_TIMEOUT.get_or_init(|| Duration::from_millis(1_000)) - } - - pub fn destructive_command_confirmation_timeout() -> &'static Duration { - static DESTRUCTIVE_COMMAND_CONFIRMATION_TIMEOUT: OnceLock = OnceLock::new(); - DESTRUCTIVE_COMMAND_CONFIRMATION_TIMEOUT.get_or_init(|| Duration::from_secs(60)) - } + pub const WAIT_FOR_BOT_EVENTS_TIMEOUT: Duration = Duration::from_millis(1_000); + pub const DESTRUCTIVE_COMMAND_CONFIRMATION_TIMEOUT: Duration = Duration::from_secs(60); + pub const QUEUE_ADVANCE_LOCKED_TIMEOUT: Duration = Duration::from_millis(250); } pub mod text { - use std::sync::OnceLock; + use std::sync::LazyLock; use fuzzy_matcher::skim::SkimMatcherV2; @@ -125,26 +102,20 @@ pub mod text { pub const EMPTY_EMBED_FIELD: &str = "`-Empty-`"; pub const NO_ROWS_AFFECTED_MESSAGE: &str = "🔐 No changes were made."; - pub fn fuzzy_matcher() -> &'static SkimMatcherV2 { - static FUZZY_MATCHER: OnceLock = OnceLock::new(); - FUZZY_MATCHER.get_or_init(SkimMatcherV2::default) - } + pub static FUZZY_MATCHER: LazyLock = LazyLock::new(SkimMatcherV2::default); } pub mod regex { - use std::sync::OnceLock; + use std::sync::LazyLock; use regex::Regex; - pub fn url() -> &'static Regex { - static URL: OnceLock = OnceLock::new(); - URL.get_or_init(|| { - Regex::new( - r"(https://www\.|http://www\.|https://|http://)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?/[a-zA-Z0-9]{2,}|((https://www\.|http://www\.|https://|http://)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?)|(https://www\.|http://www\.|https://|http://)?[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})?" - ) - .expect("regex is valid") - }) - } + pub static URL: LazyLock = LazyLock::new(|| { + Regex::new( + r"(https://www\.|http://www\.|https://|http://)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?/[a-zA-Z0-9]{2,}|((https://www\.|http://www\.|https://|http://)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?)|(https://www\.|http://www\.|https://|http://)?[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})?" + ) + .expect("regex is valid") + }); } pub mod exit_code { diff --git a/lyra/src/core/model.rs b/lyra/src/core/model.rs index 67f95a4..274f83a 100644 --- a/lyra/src/core/model.rs +++ b/lyra/src/core/model.rs @@ -1,3 +1,4 @@ +mod emoji; mod interaction; use std::{ @@ -10,25 +11,25 @@ use std::{ use dashmap::DashMap; use sqlx::{Pool, Postgres}; +use tokio::sync::OnceCell; use twilight_cache_inmemory::InMemoryCache; use twilight_gateway::ShardId; use twilight_http::Client; use twilight_model::{ - guild::Permissions, - id::{marker::UserMarker, Id}, - oauth::Application, + guild::{Emoji, Permissions}, + id::{ + marker::{ApplicationMarker, UserMarker}, + Id, + }, user::CurrentUser, }; use twilight_standby::Standby; -use crate::{ - error::core::DeserializeBodyFromHttpError, - lavalink::{self, Lavalink}, -}; +use crate::{error::core::DeserializeBodyFromHttpError, lavalink::Lavalink, LavalinkAware}; pub use self::interaction::{ - Client as InteractionClient, Interface as InteractionInterface, MessageResponse, - UnitFollowupResult, UnitRespondResult, + AcknowledgementAware, Client as InteractionClient, Interface as InteractionInterface, + MessageResponse, UnitFollowupResult, UnitRespondResult, }; pub struct Config { @@ -130,15 +131,17 @@ pub trait HttpAware { pub struct BotState { cache: InMemoryCache, - http: Client, + http: Arc, standby: Standby, lavalink: Lavalink, db: Pool, info: BotInfo, + application_id: OnceCell>, + application_emojis: OnceCell<&'static [Emoji]>, } impl BotState { - pub fn new(db: Pool, http: Client, lavalink: Lavalink) -> Self { + pub fn new(db: Pool, http: Arc, lavalink: Lavalink) -> Self { let info = BotInfo { started: Instant::now(), guild_counter: GuildCounter::new(), @@ -151,6 +154,8 @@ impl BotState { lavalink, db, info, + application_id: OnceCell::new(), + application_emojis: OnceCell::new(), } } @@ -166,13 +171,33 @@ impl BotState { &self.info } - async fn app(&self) -> Result { - Ok(self.http.current_user_application().await?.model().await?) + pub async fn application_id( + &self, + ) -> Result, DeserializeBodyFromHttpError> { + self.application_id + .get_or_try_init(|| async { + let application = self.http.current_user_application().await?.model().await?; + Ok(application.id) + }) + .await + .copied() } - pub async fn interaction(&self) -> Result { - let client = self.http.interaction(self.app().await?.id); + pub async fn application_emojis( + &self, + ) -> Result<&'static [Emoji], DeserializeBodyFromHttpError> { + self.application_emojis + .get_or_try_init(|| async { + let application_id = self.application_id().await?; + let req = self.http.get_application_emojis(application_id); + Ok(&*req.await?.models().await?.leak()) + }) + .await + .copied() + } + pub async fn interaction(&self) -> Result { + let client = self.http.interaction(self.application_id().await?); Ok(InteractionClient::new(client)) } @@ -188,7 +213,7 @@ impl BotState { } } -impl lavalink::LavalinkAware for BotState { +impl LavalinkAware for BotState { fn lavalink(&self) -> &Lavalink { &self.lavalink } diff --git a/lyra/src/core/model/emoji.rs b/lyra/src/core/model/emoji.rs new file mode 100644 index 0000000..77c695c --- /dev/null +++ b/lyra/src/core/model/emoji.rs @@ -0,0 +1,50 @@ +use std::sync::OnceLock; + +use twilight_model::channel::message::EmojiReactionType; + +use crate::error::core::DeserializeBodyFromHttpError; + +use super::BotState; + +macro_rules! generate_emojis { + ($ (($name: ident, $default: expr)) ,* $(,)? ) => {$( + pub async fn $name( + bot: &BotState, + ) -> Result<&'static EmojiReactionType, DeserializeBodyFromHttpError> { + ::paste::paste! { + static [<$name:upper>]: OnceLock = OnceLock::new(); + if let Some(emoji) = [<$name:upper>].get() { + return Ok(emoji); + } + } + + let emojis = bot.application_emojis().await?; + let emoji = emojis.iter().find(|e| e.name == stringify!($name)); + let reaction = emoji.map_or( + { + EmojiReactionType::Unicode { + name: String::from($default), + } + }, + |emoji| EmojiReactionType::Custom { + animated: emoji.animated, + id: emoji.id, + name: Some(emoji.name.clone()), + }, + ); + ::paste::paste!(Ok([<$name:upper>].get_or_init(|| reaction))) + } + )*}; +} + +generate_emojis![ + (shuffle_off, "🔀"), + (shuffle_on, "🔀"), + (previous, "⏮️"), + (play, "▶️"), + (pause, "⏸️"), + (next, "⏭️"), + (repeat_off, "➡️"), + (repeat_all, "🔁"), + (repeat_track, "🔂"), +]; diff --git a/lyra/src/core/model/interaction.rs b/lyra/src/core/model/interaction.rs index e1f775c..07bea27 100644 --- a/lyra/src/core/model/interaction.rs +++ b/lyra/src/core/model/interaction.rs @@ -1,3 +1,5 @@ +use std::fmt::{Display, Write}; + use twilight_http::{request::application::interaction::UpdateResponse, Response}; use twilight_model::{ application::{command::CommandOptionChoice, interaction::Interaction}, @@ -7,7 +9,7 @@ use twilight_model::{ }, http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}, id::{ - marker::{InteractionMarker, MessageMarker}, + marker::{CommandMarker, InteractionMarker, MessageMarker}, Id, }, }; @@ -15,12 +17,18 @@ use twilight_util::builder::InteractionResponseDataBuilder; use crate::{ command::{ - declare::{message_commands, slash_commands, POPULATED_COMMANDS_MAP}, + declare::{MESSAGE_COMMANDS, POPULATED_COMMANDS_MAP, SLASH_COMMANDS}, model::CommandInfoAware, }, error::core::{FollowupResult, RegisterGlobalCommandsError, RespondResult}, }; +pub type MessageResponse = Response; +pub type UnitRespondResult = RespondResult<()>; +pub type MessageRespondResult = RespondResult; +pub type UnitFollowupResult = FollowupResult<()>; +pub type MessageFollowupResult = FollowupResult; + pub struct Client<'a>(twilight_http::client::InteractionClient<'a>); pub struct Interface<'a> { @@ -82,13 +90,15 @@ impl Interface<'_> { self.inner.0.update_response(self.interaction_token()) } - pub async fn update_no_components_embeds(&self, content: &str) -> MessageFollowupResult { - Ok(self - .update() + pub async fn update_no_components_embeds( + &self, + content: impl Into + Send, + ) -> MessageRespondResult { + self.update() .components(None) .embeds(None) - .content(Some(content)) - .await?) + .content(Some(&content.into())) + .await } pub async fn update_message_embeds_only( @@ -99,7 +109,15 @@ impl Interface<'_> { Ok(self.update_message_with(Some(data)).await?) } - pub async fn ephem(&self, content: impl Into + Send) -> MessageRespondResult { + pub async fn respond(&self, content: impl Into + Send) -> MessageRespondResult { + let data = Self::base_response_data_builder().content(content).build(); + self.respond_with(Some(data)).await + } + + pub async fn respond_ephemeral( + &self, + content: impl Into + Send, + ) -> MessageRespondResult { let data = Self::base_response_data_builder() .content(content) .flags(MessageFlags::EPHEMERAL) @@ -107,22 +125,25 @@ impl Interface<'_> { self.respond_with(Some(data)).await } - pub async fn followup(&self, content: &str) -> MessageFollowupResult { + pub async fn followup(&self, content: impl Into + Send) -> MessageFollowupResult { Ok(self .inner .0 .create_followup(self.interaction_token()) - .content(content) + .content(&content.into()) .await?) } - pub async fn followup_ephem(&self, content: &str) -> MessageFollowupResult { + pub async fn followup_ephemeral( + &self, + content: impl Into + Send, + ) -> MessageFollowupResult { Ok(self .inner .0 .create_followup(self.interaction_token()) .flags(MessageFlags::EPHEMERAL) - .content(content) + .content(&content.into()) .await?) } @@ -182,6 +203,36 @@ impl Interface<'_> { Ok(()) } + async fn defer_as(&self, ephemeral: bool) -> UnitRespondResult { + let mut data = Self::base_response_data_builder(); + if ephemeral { + data = data.flags(MessageFlags::EPHEMERAL); + } + + self.inner + .0 + .create_response( + self.interaction_id, + self.interaction_token(), + &InteractionResponse { + kind: InteractionResponseType::DeferredChannelMessageWithSource, + data: data.build().into(), + }, + ) + .await?; + Ok(()) + } + + #[inline] + pub async fn defer(&self) -> UnitRespondResult { + self.defer_as(false).await + } + + #[inline] + pub async fn defer_ephem(&self) -> UnitRespondResult { + self.defer_as(true).await + } + pub async fn update_followup( &self, message_id: Id, @@ -196,6 +247,109 @@ impl Interface<'_> { } } +pub trait AcknowledgementAware { + type FollowupError; + type RespondError; + type RespondOrFollowupError: From + From; + + fn acknowledged(&self) -> bool; + async fn respond( + &mut self, + content: impl Into + Send, + ) -> Result; + async fn respond_ephemeral( + &mut self, + content: impl Into + Send, + ) -> Result; + async fn update( + &self, + content: impl Into + Send, + ) -> Result; + async fn followup( + &self, + content: impl Into + Send, + ) -> Result; + async fn followup_ephemeral( + &self, + content: impl Into + Send, + ) -> Result; + + async fn respond_or_update( + &mut self, + content: impl Into + Send, + ) -> Result { + if self.acknowledged() { + return self.update(&content.into()).await; + } + self.respond(content).await + } + async fn respond_or_followup( + &mut self, + content: impl Into + Send, + ) -> Result { + if self.acknowledged() { + return Ok(self.followup(&content.into()).await?); + } + + Ok(self.respond(content).await?) + } + async fn respond_ephemeral_or_followup( + &mut self, + content: impl Into + Send, + ) -> Result { + if self.acknowledged() { + return Ok(self.followup_ephemeral(&content.into()).await?); + } + + Ok(self.respond_ephemeral(content).await?) + } +} + +impl AcknowledgementAware for (Interface<'_>, bool) { + type FollowupError = crate::error::core::FollowupError; + type RespondError = twilight_http::Error; + type RespondOrFollowupError = crate::error::core::FollowupError; + + fn acknowledged(&self) -> bool { + self.1 + } + + async fn respond_ephemeral( + &mut self, + content: impl Into + Send, + ) -> Result { + self.0.respond_ephemeral(content).await + } + + async fn respond( + &mut self, + content: impl Into + Send, + ) -> Result { + self.0.respond(content).await + } + + async fn update( + &self, + content: impl Into + Send, + ) -> Result { + self.0.update_no_components_embeds(content).await + } + + async fn followup( + &self, + content: impl Into + Send, + ) -> Result { + self.0.followup(content).await + } + + async fn followup_ephemeral( + &self, + content: impl Into + Send, + ) -> Result { + self.0.followup_ephemeral(content).await + } +} + impl<'a> Client<'a> { pub const fn new(client: twilight_http::client::InteractionClient<'a>) -> Self { Self(client) @@ -212,9 +366,7 @@ impl<'a> Client<'a> { pub async fn register_global_commands(&self) -> Result<(), RegisterGlobalCommandsError> { let commands = self .0 - .set_global_commands( - &[slash_commands().as_slice(), message_commands().as_slice()].concat(), - ) + .set_global_commands(&[SLASH_COMMANDS.as_slice(), MESSAGE_COMMANDS.as_slice()].concat()) .await? .models() .await?; @@ -222,7 +374,7 @@ impl<'a> Client<'a> { POPULATED_COMMANDS_MAP.get_or_init(|| { commands .into_iter() - .map(|c| (c.name.clone().into(), c)) + .map(|c| (&*c.name.clone().leak(), c)) .collect() }); @@ -231,21 +383,22 @@ impl<'a> Client<'a> { pub fn populated_command( ) -> &'static twilight_model::application::command::Command { + let name = T::name(); POPULATED_COMMANDS_MAP .get() .unwrap_or_else(|| panic!("`POPULATED_COMMANDS_MAP` is not yet populated")) - .get(T::name()) - .unwrap_or_else(|| panic!("command not found: {}", T::name())) + .get(name) + .unwrap_or_else(|| panic!("command not found: {name}")) } - pub fn mention_command() -> Box { + pub fn mention_command() -> MentionCommand { let cmd = Self::populated_command::(); - let name = &cmd.name; + let name = cmd.name.clone().into(); let id = cmd .id .unwrap_or_else(|| panic!("`POPULATED_COMMANDS_MAP` is not yet populated")); - format!("").into_boxed_str() + MentionCommand::new(name, id) } #[inline] @@ -266,8 +419,25 @@ impl<'a> Client<'a> { } } -pub type MessageResponse = Response; -pub type UnitRespondResult = RespondResult<()>; -pub type MessageRespondResult = RespondResult; -pub type UnitFollowupResult = FollowupResult<()>; -pub type MessageFollowupResult = FollowupResult; +pub struct MentionCommand { + name: Box, + id: Id, +} + +impl MentionCommand { + pub const fn new(name: Box, id: Id) -> Self { + Self { name, id } + } +} + +impl Display for MentionCommand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("')?; + + Ok(()) + } +} diff --git a/lyra/src/core/traced.rs b/lyra/src/core/traced.rs index 11542f1..988ec77 100644 --- a/lyra/src/core/traced.rs +++ b/lyra/src/core/traced.rs @@ -1,13 +1,11 @@ -use std::{error::Error, fmt::Debug, future::Future}; +use std::{error::Error, future::Future}; use tokio::task::JoinHandle; use tracing::Instrument; -pub fn tokio_spawn(fut: F) -> JoinHandle<()> -where - E: Error + Debug, - F: Future> + Send + 'static, -{ +pub fn tokio_spawn( + fut: impl Future> + Send + 'static, +) -> JoinHandle<()> { tokio::spawn( async move { if let Err(error) = fut.await { diff --git a/lyra/src/error.rs b/lyra/src/error.rs index 1db7468..360c9e9 100644 --- a/lyra/src/error.rs +++ b/lyra/src/error.rs @@ -5,7 +5,6 @@ pub mod gateway; pub mod lavalink; pub mod runner; -pub use command::Error as CommandError; pub use command::Result as CommandResult; use thiserror::Error; @@ -15,8 +14,11 @@ use twilight_model::id::{ Id, }; -pub trait EPrint: std::error::Error + std::fmt::Debug { - fn eprint(&self) -> String; +use crate::command::require::PartialInVoice; + +pub trait PrettyErrorDisplay<'a> { + type Displayer: std::fmt::Display; + fn pretty_display(&'a self) -> Self::Displayer; } #[derive(Error, Debug)] @@ -66,14 +68,25 @@ pub struct InVoiceAlready(pub Id); pub struct InVoiceWithoutUser(pub Id); #[derive(Error, Debug)] -#[error("bot is already in voice and someone else also is: {}", .0)] -pub struct InVoiceWithSomeoneElse(pub Id); +#[error("bot is already in voice and someone else also is: {}", .0.channel_id())] +pub struct InVoiceWithSomeoneElse(pub PartialInVoice); + +impl<'a> PrettyErrorDisplay<'a> for InVoiceWithSomeoneElse { + type Displayer = PrettyInVoiceWithSomeoneElseDisplayer<'a>; + + fn pretty_display(&'a self) -> Self::Displayer { + PrettyInVoiceWithSomeoneElseDisplayer(self) + } +} -impl EPrint for InVoiceWithSomeoneElse { - fn eprint(&self) -> String { - format!( +pub struct PrettyInVoiceWithSomeoneElseDisplayer<'a>(&'a InVoiceWithSomeoneElse); + +impl std::fmt::Display for PrettyInVoiceWithSomeoneElseDisplayer<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, "There are someone else in {}; You need to be a ***DJ*** to do that.", - self.0.mention(), + self.0 .0.channel_id().mention(), ) } } @@ -107,8 +120,18 @@ pub struct NotPlaying; #[error("queue is not seekable")] pub struct QueueNotSeekable; -impl EPrint for QueueNotSeekable { - fn eprint(&self) -> String { +impl<'a> PrettyErrorDisplay<'a> for QueueNotSeekable { + type Displayer = PrettyQueueNotSeekableDisplayer; + + fn pretty_display(&'a self) -> Self::Displayer { + PrettyQueueNotSeekableDisplayer + } +} + +pub struct PrettyQueueNotSeekableDisplayer; + +impl std::fmt::Display for PrettyQueueNotSeekableDisplayer { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { todo!() } } @@ -122,14 +145,26 @@ pub struct NotUsersTrack { pub channel_id: Id, } -impl EPrint for NotUsersTrack { - fn eprint(&self) -> String { - format!( +impl<'a> PrettyErrorDisplay<'a> for NotUsersTrack { + type Displayer = PrettyNotUsersTrackDisplayer<'a>; + + fn pretty_display(&'a self) -> Self::Displayer { + PrettyNotUsersTrackDisplayer(self) + } +} + +pub struct PrettyNotUsersTrackDisplayer<'a>(&'a NotUsersTrack); + +impl std::fmt::Display for PrettyNotUsersTrackDisplayer<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let inner = self.0; + write!( + f, "`{}` (`#{}`) was requested by {} and you're not the only person in {}; You'll need to be a ***DJ*** to do that.", - self.title, - self.position, - self.requester.mention(), - self.channel_id.mention(), + inner.title, + inner.position, + inner.requester.mention(), + inner.channel_id.mention(), ) } } @@ -164,7 +199,7 @@ pub struct PrettifiedTimestampParse; #[derive(Error, Debug)] #[error("error running the bot starter: {}", .0)] -pub enum RunError { +pub enum Run { ColorEyre(#[from] color_eyre::Report), Dotenvy(#[from] dotenvy::Error), StartError(#[from] runner::StartError), @@ -173,3 +208,11 @@ pub enum RunError { #[derive(Error, Debug)] #[error("not in a guild")] pub struct NotInGuild; + +#[derive(Error, Debug)] +#[error("confirmation timed out")] +pub struct ConfirmationTimedOut; + +#[derive(Error, Debug)] +#[error("unrecognised voice connection")] +pub struct UnrecognisedConnection; diff --git a/lyra/src/error/command.rs b/lyra/src/error/command.rs index eb975e5..43598db 100644 --- a/lyra/src/error/command.rs +++ b/lyra/src/error/command.rs @@ -1,6 +1,7 @@ pub mod check; pub mod declare; pub mod poll; +pub mod require; pub mod util; #[derive(thiserror::Error, Debug)] @@ -17,6 +18,13 @@ pub enum FollowupError { Followup(#[from] super::core::FollowupError), } +#[derive(thiserror::Error, Debug)] +#[error("creating a response or followup failed: {}", 0)] +pub enum RespondOrFollowupError { + Respond(#[from] RespondError), + Followup(#[from] FollowupError), +} + #[derive(thiserror::Error, Debug)] #[error("command failed: {}", .0)] pub enum Error { @@ -27,10 +35,10 @@ pub enum Error { NotInVoice(#[from] super::NotInVoice), InVoiceWithoutUser(#[from] super::InVoiceWithoutUser), QueueEmpty(#[from] super::QueueEmpty), - CheckNotSuppressed(#[from] check::NotSuppressedError), + RequireUnsuppressed(#[from] require::UnsuppressedError), CheckUsersTrack(#[from] check::UsersTrackError), UserNotDj(#[from] super::UserNotDj), - InVoiceWithSomeoneElse(#[from] check::InVoiceWithSomeoneElseError), + RequireInVoiceWithSomeoneElse(#[from] require::InVoiceWithSomeoneElseError), PositionOutOfRange(#[from] super::PositionOutOfRange), CheckRun(#[from] check::RunError), Respond(#[from] RespondError), @@ -38,9 +46,6 @@ pub enum Error { PromptForConfirmation(#[from] util::PromptForConfirmationError), Join(#[from] super::component::connection::join::ResidualError), Leave(#[from] super::component::connection::leave::ResidualError), - WithAdvanceLockAndStopped( - #[from] super::component::queue::remove::WithAdvanceLockAndStoppedError, - ), Play(#[from] super::component::queue::play::Error), DeserializeBodyFromHttp(#[from] super::core::DeserializeBodyFromHttpError), RemoveTracks(#[from] super::component::queue::RemoveTracksError), @@ -51,109 +56,111 @@ pub enum Error { CheckUserOnlyIn(#[from] check::UserOnlyInError), Cache(#[from] super::Cache), HandlePoll(#[from] check::HandlePollError), + NotPlaying(#[from] super::NotPlaying), + Paused(#[from] super::Paused), + UnrecognisedConnection(#[from] super::UnrecognisedConnection), } pub enum FlattenedError<'a> { - UserNotAccessManager(&'a super::UserNotAccessManager), - Sqlx(&'a sqlx::Error), - TaskJoin(&'a tokio::task::JoinError), - EmbedValidation(&'a twilight_validate::embed::EmbedValidationError), - NotInVoice(&'a super::NotInVoice), InVoiceWithoutUser(&'a super::InVoiceWithoutUser), - QueueEmpty(&'a super::QueueEmpty), - PositionOutOfRange(&'a super::PositionOutOfRange), - Cache(&'a super::Cache), Suppressed(&'a super::Suppressed), NotUsersTrack(&'a super::NotUsersTrack), - UserNotDj(&'a super::UserNotDj), InVoiceWithoutSomeoneElse(&'a super::InVoiceWithoutSomeoneElse), - NotPlaying(&'a super::NotPlaying), - Paused(&'a super::Paused), - Stopped(&'a super::Stopped), InVoiceWithSomeoneElse(&'a super::InVoiceWithSomeoneElse), QueueNotSeekable(&'a super::QueueNotSeekable), AnotherPollOngoing(&'a check::AnotherPollOngoingError), - TwilightHttp(&'a twilight_http::Error), - DeserializeBody(&'a twilight_http::response::DeserializeBodyError), - EventSend(&'a tokio::sync::broadcast::error::SendError), - EventRecv(&'a tokio::sync::broadcast::error::RecvError), - ImageSourceUrl(&'a twilight_util::builder::embed::image_source::ImageSourceUrlError), - MessageValidation(&'a twilight_validate::message::MessageValidationError), + PositionOutOfRange(&'a super::PositionOutOfRange), PollLoss(&'a check::PollLossError), PollVoided(&'a check::PollVoidedError), - StandbyCanceled(&'a twilight_standby::future::Canceled), - Confirmation(&'a util::ConfirmationError), - GatewaySend(&'a twilight_gateway::error::ChannelError), AutoJoinSuppressed(&'a util::AutoJoinSuppressedError), AutoJoinAttemptFailed(&'a super::AutoJoinAttemptFailed), - Lavalink(&'a lavalink_rs::error::LavalinkError), - NoPlayer(&'a super::lavalink::NoPlayerError), - NotInGuild(&'a super::NotInGuild), + UserNotAccessManager, + Sqlx, + TaskJoin, + EmbedValidation, + NotInVoice, + QueueEmpty, + Cache, + UserNotDj, + NotPlaying, + Paused, + Stopped, + TwilightHttp, + DeserializeBody, + EventSend, + EventRecv, + ImageSourceUrl, + MessageValidation, + StandbyCanceled, + ConfirmationTimedOut, + GatewaySend, + Lavalink, + NoPlayer, + NotInGuild, + UnrecognisedConnection, } pub use FlattenedError as Fe; impl<'a> Fe<'a> { - const fn from_core_followup_error(error: &'a super::core::FollowupError) -> Fe<'a> { + const fn from_core_followup_error(error: &'a super::core::FollowupError) -> Self { match error { - super::core::FollowupError::TwilightHttp(e) => Self::TwilightHttp(e), - super::core::FollowupError::MessageValidation(e) => Self::MessageValidation(e), + super::core::FollowupError::TwilightHttp(_) => Self::TwilightHttp, + super::core::FollowupError::MessageValidation(_) => Self::MessageValidation, } } - const fn from_check_not_suppressed_error(error: &'a check::NotSuppressedError) -> Fe<'a> { + const fn from_require_unsuppressed_error(error: &'a require::UnsuppressedError) -> Self { match error { - check::NotSuppressedError::Cache(e) => Self::Cache(e), - check::NotSuppressedError::Suppressed(e) => Self::Suppressed(e), + require::UnsuppressedError::Cache(_) => Self::Cache, + require::UnsuppressedError::Suppressed(e) => Self::Suppressed(e), } } const fn from_deserialize_body_from_http_error( error: &'a super::core::DeserializeBodyFromHttpError, - ) -> Fe<'a> { + ) -> Self { match error { - super::core::DeserializeBodyFromHttpError::TwilightHttp(e) => Self::TwilightHttp(e), - super::core::DeserializeBodyFromHttpError::DeserializeBody(e) => { - Self::DeserializeBody(e) - } + super::core::DeserializeBodyFromHttpError::TwilightHttp(_) => Self::TwilightHttp, + super::core::DeserializeBodyFromHttpError::DeserializeBody(_) => Self::DeserializeBody, } } - const fn from_users_track_error(error: &'a check::UsersTrackError) -> Fe<'a> { + const fn from_users_track_error(error: &'a check::UsersTrackError) -> Self { match error { - check::UsersTrackError::Cache(e) => Self::Cache(e), + check::UsersTrackError::Cache(_) => Self::Cache, check::UsersTrackError::NotUsersTrack(e) => Self::NotUsersTrack(e), } } - const fn from_in_voice_with_someone_else_error( - error: &'a check::InVoiceWithSomeoneElseError, + const fn from_require_in_voice_with_someone_else_error( + error: &'a require::InVoiceWithSomeoneElseError, ) -> Self { match error { - check::InVoiceWithSomeoneElseError::Cache(e) => Self::Cache(e), - check::InVoiceWithSomeoneElseError::InVoiceWithoutSomeoneElse(e) => { + require::InVoiceWithSomeoneElseError::Cache(_) => Self::Cache, + require::InVoiceWithSomeoneElseError::InVoiceWithoutSomeoneElse(e) => { Self::InVoiceWithoutSomeoneElse(e) } } } - const fn from_run(error: &'a check::RunError) -> Fe<'a> { + const fn from_run(error: &'a check::RunError) -> Self { match error { - check::RunError::NotInVoice(e) => Self::NotInVoice(e), - check::RunError::QueueEmpty(e) => Self::QueueEmpty(e), - check::RunError::NotPlaying(e) => Self::NotPlaying(e), + check::RunError::NotInVoice(_) => Self::NotInVoice, + check::RunError::QueueEmpty(_) => Self::QueueEmpty, + check::RunError::NotPlaying(_) => Self::NotPlaying, + check::RunError::Cache(_) => Self::Cache, + check::RunError::Paused(_) => Self::Paused, + check::RunError::Stopped(_) => Self::Stopped, check::RunError::InVoiceWithoutUser(e) => Self::InVoiceWithoutUser(e), - check::RunError::Cache(e) => Self::Cache(e), - check::RunError::Paused(e) => Self::Paused(e), - check::RunError::Stopped(e) => Self::Stopped(e), - check::RunError::NotSuppressed(e) => Self::from_check_not_suppressed_error(e), + check::RunError::NotSuppressed(e) => Self::from_require_unsuppressed_error(e), check::RunError::HandleInVoiceWithSomeoneElse(e) => { Self::from_handle_in_voice_with_someone_else_error(e) } } } - const fn from_vote_resolvable(error: &'a check::PollResolvableError) -> Fe<'a> { + const fn from_vote_resolvable(error: &'a check::PollResolvableError) -> Self { match error { check::PollResolvableError::InVoiceWithSomeoneElse(e) => { Self::InVoiceWithSomeoneElse(e) @@ -163,26 +170,26 @@ impl<'a> Fe<'a> { } } - const fn from_update_embed(error: &'a poll::UpdateEmbedError) -> Fe<'a> { + const fn from_update_embed(error: &'a poll::UpdateEmbedError) -> Self { match error { - poll::UpdateEmbedError::Http(e) => Self::TwilightHttp(e), - poll::UpdateEmbedError::EmbedValidation(e) => Self::EmbedValidation(e), - poll::UpdateEmbedError::MessageValidation(e) => Self::MessageValidation(e), + poll::UpdateEmbedError::Http(_) => Self::TwilightHttp, + poll::UpdateEmbedError::EmbedValidation(_) => Self::EmbedValidation, + poll::UpdateEmbedError::MessageValidation(_) => Self::MessageValidation, poll::UpdateEmbedError::Followup(e) => Self::from_core_followup_error(e), } } - const fn from_generate_embed(error: &'a poll::GenerateEmbedError) -> Fe<'a> { + const fn from_generate_embed(error: &'a poll::GenerateEmbedError) -> Self { match error { - poll::GenerateEmbedError::ImageSourceUrl(e) => Self::ImageSourceUrl(e), - poll::GenerateEmbedError::EmbedValidation(e) => Self::EmbedValidation(e), + poll::GenerateEmbedError::ImageSourceUrl(_) => Self::ImageSourceUrl, + poll::GenerateEmbedError::EmbedValidation(_) => Self::EmbedValidation, } } - const fn from_wait_for_votes(error: &'a poll::WaitForVotesError) -> Fe<'a> { + const fn from_wait_for_votes(error: &'a poll::WaitForVotesError) -> Self { match error { - poll::WaitForVotesError::TwilightHttp(e) => Self::TwilightHttp(e), - poll::WaitForVotesError::EventRecv(e) => Self::EventRecv(e), + poll::WaitForVotesError::TwilightHttp(_) => Self::TwilightHttp, + poll::WaitForVotesError::EventRecv(_) => Self::EventRecv, poll::WaitForVotesError::DeserializeBodyFromHttp(e) => { Self::from_deserialize_body_from_http_error(e) } @@ -190,23 +197,23 @@ impl<'a> Fe<'a> { } } - const fn from_start_poll(error: &'a poll::StartPollError) -> Fe<'a> { + const fn from_start_poll(error: &'a poll::StartPollError) -> Self { match error { - poll::StartPollError::Cache(e) => Self::Cache(e), - poll::StartPollError::DeserializeBody(e) => Self::DeserializeBody(e), + poll::StartPollError::Cache(_) => Self::Cache, + poll::StartPollError::DeserializeBody(_) => Self::DeserializeBody, poll::StartPollError::Respond(e) => Self::from_respond(e), poll::StartPollError::GenerateEmbed(e) => Self::from_generate_embed(e), poll::StartPollError::WaitForVotes(e) => Self::from_wait_for_votes(e), } } - const fn from_handle_poll(error: &'a check::HandlePollError) -> Fe<'a> { + const fn from_handle_poll(error: &'a check::HandlePollError) -> Self { match error { + check::HandlePollError::EventRecv(_) => Self::EventRecv, + check::HandlePollError::EventSend(_) => Self::EventSend, check::HandlePollError::AnotherPollOngoing(e) => Self::AnotherPollOngoing(e), - check::HandlePollError::EventSend(e) => Self::EventSend(e), check::HandlePollError::PollLoss(e) => Self::PollLoss(e), check::HandlePollError::PollVoided(e) => Self::PollVoided(e), - check::HandlePollError::EventRecv(e) => Self::EventRecv(e), check::HandlePollError::StartPoll(e) => Self::from_start_poll(e), check::HandlePollError::DeserializeBodyFromHttp(e) => { Self::from_deserialize_body_from_http_error(e) @@ -216,7 +223,7 @@ impl<'a> Fe<'a> { const fn from_handle_in_voice_with_someone_else_error( error: &'a check::HandleInVoiceWithSomeoneElseError, - ) -> Fe<'a> { + ) -> Self { match error { check::HandleInVoiceWithSomeoneElseError::PollResolvable(e) => { Self::from_vote_resolvable(e) @@ -225,16 +232,16 @@ impl<'a> Fe<'a> { } } - const fn from_respond(error: &'a RespondError) -> Fe<'a> { + const fn from_respond(error: &'a RespondError) -> Self { match error { - RespondError::TwilightHttp(e) => Self::TwilightHttp(e), + RespondError::TwilightHttp(_) => Self::TwilightHttp, RespondError::DeserializeBodyFromHttp(e) => { Self::from_deserialize_body_from_http_error(e) } } } - const fn from_followup(error: &'a FollowupError) -> Fe<'a> { + const fn from_followup(error: &'a FollowupError) -> Self { match error { FollowupError::DeserializeBodyFromHttp(e) => { Self::from_deserialize_body_from_http_error(e) @@ -243,31 +250,38 @@ impl<'a> Fe<'a> { } } - const fn from_prompt_for_confirmation(error: &'a util::PromptForConfirmationError) -> Fe<'a> { + const fn from_respond_or_followup(error: &'a RespondOrFollowupError) -> Self { match error { - util::PromptForConfirmationError::StandbyCanceled(e) => Self::StandbyCanceled(e), - util::PromptForConfirmationError::Confirmation(e) => Self::Confirmation(e), + RespondOrFollowupError::Respond(e) => Self::from_respond(e), + RespondOrFollowupError::Followup(e) => Self::from_followup(e), + } + } + + const fn from_prompt_for_confirmation(error: &'a util::PromptForConfirmationError) -> Self { + match error { + util::PromptForConfirmationError::StandbyCanceled(_) => Self::StandbyCanceled, + util::PromptForConfirmationError::ConfirmationTimedout(_) => Self::ConfirmationTimedOut, util::PromptForConfirmationError::Respond(e) => Self::from_respond(e), } } - const fn from_access_calculator_build(error: &'a check::AccessCalculatorBuildError) -> Fe<'a> { + const fn from_access_calculator_build(error: &'a check::AccessCalculatorBuildError) -> Self { match error { - check::AccessCalculatorBuildError::Sqlx(e) => Self::Sqlx(e), - check::AccessCalculatorBuildError::TaskJoin(e) => Self::TaskJoin(e), + check::AccessCalculatorBuildError::Sqlx(_) => Self::Sqlx, + check::AccessCalculatorBuildError::TaskJoin(_) => Self::TaskJoin, } } - const fn from_check_user_only_in(error: &'a check::UserOnlyInError) -> Fe<'a> { + const fn from_check_user_only_in(error: &'a check::UserOnlyInError) -> Self { match error { - check::UserOnlyInError::Cache(e) => Self::Cache(e), + check::UserOnlyInError::Cache(_) => Self::Cache, check::UserOnlyInError::InVoiceWithSomeoneElse(e) => Self::InVoiceWithSomeoneElse(e), } } const fn from_check_user_allowed_residual( error: &'a super::component::connection::join::ResidualUserAllowedError, - ) -> Fe<'a> { + ) -> Self { match error { super::component::connection::join::ResidualUserAllowedError::AccessCalculatorBuild( e, @@ -277,20 +291,21 @@ impl<'a> Fe<'a> { const fn from_impl_connect_to_residual( error: &'a super::component::connection::join::ResidualImplConnectToError, - ) -> Fe<'a> { + ) -> Self { match error { - super::component::connection::join::ResidualImplConnectToError::Cache(e) => { - Self::Cache(e) - } - super::component::connection::join::ResidualImplConnectToError::GatewaySend(e) => { - Self::GatewaySend(e) + super::component::connection::join::ResidualImplConnectToError::Cache(_) => Self::Cache, + super::component::connection::join::ResidualImplConnectToError::GatewaySend(_) => { + Self::GatewaySend } - super::component::connection::join::ResidualImplConnectToError::TwilightHttp(e) => { - Self::TwilightHttp(e) + super::component::connection::join::ResidualImplConnectToError::TwilightHttp(_) => { + Self::TwilightHttp } - super::component::connection::join::ResidualImplConnectToError::Lavalink(e) => { - Self::Lavalink(e) + super::component::connection::join::ResidualImplConnectToError::Lavalink(_) => { + Self::Lavalink } + super::component::connection::join::ResidualImplConnectToError::UnrecognisedConnection(_) => { + Self::UnrecognisedConnection + }, super::component::connection::join::ResidualImplConnectToError::CheckUserAllowed(e) => { Self::from_check_user_allowed_residual(e) } @@ -299,7 +314,7 @@ impl<'a> Fe<'a> { const fn from_connect_to_residual( error: &'a super::component::connection::join::ResidualConnectToError, - ) -> Fe<'a> { + ) -> Self { match error { super::component::connection::join::ResidualConnectToError::CheckUserOnlyIn(e) => { Self::from_check_user_only_in(e) @@ -312,17 +327,17 @@ impl<'a> Fe<'a> { const fn from_get_users_voice_channel_residual( error: &'a super::component::connection::join::ResidualGetUsersVoiceChannelError, - ) -> Fe<'a> { + ) -> Self { match error { - super::component::connection::join::ResidualGetUsersVoiceChannelError::Cache(e) => { - Self::Cache(e) + super::component::connection::join::ResidualGetUsersVoiceChannelError::Cache(_) => { + Self::Cache } } } const fn from_impl_join_residual( error: &'a super::component::connection::join::ResidualImplJoinError, - ) -> Fe<'a> { + ) -> Self { match error { super::component::connection::join::ResidualImplJoinError::GetUsersVoiceChannel(e) => { Self::from_get_users_voice_channel_residual(e) @@ -335,14 +350,14 @@ impl<'a> Fe<'a> { const fn from_handle_response( error: &'a super::component::connection::join::HandleResponseError, - ) -> Fe<'a> { + ) -> Self { match error { - super::component::connection::join::HandleResponseError::Cache(e) => Self::Cache(e), - super::component::connection::join::HandleResponseError::DeserializeBody(e) => { - Self::DeserializeBody(e) + super::component::connection::join::HandleResponseError::Cache(_) => Self::Cache, + super::component::connection::join::HandleResponseError::DeserializeBody(_) => { + Self::DeserializeBody } - super::component::connection::join::HandleResponseError::Respond(e) => { - Self::from_respond(e) + super::component::connection::join::HandleResponseError::RespondOrFollowup(e) => { + Self::from_respond_or_followup(e) } super::component::connection::join::HandleResponseError::Followup(e) => { Self::from_followup(e) @@ -352,7 +367,7 @@ impl<'a> Fe<'a> { const fn from_join_residual( error: &'a super::component::connection::join::ResidualError, - ) -> Fe<'a> { + ) -> Self { match error { super::component::connection::join::ResidualError::ImplJoin(e) => { Self::from_impl_join_residual(e) @@ -365,27 +380,28 @@ impl<'a> Fe<'a> { const fn from_pre_disconnect_cleanup( error: &'a super::component::connection::leave::PreDisconnectCleanupError, - ) -> Fe<'a> { + ) -> Self { match error { - super::component::connection::leave::PreDisconnectCleanupError::EventSend(e) => { - Self::EventSend(e) + super::component::connection::leave::PreDisconnectCleanupError::EventSend(_) => { + Self::EventSend } - super::component::connection::leave::PreDisconnectCleanupError::Lavalink(e) => { - Self::Lavalink(e) + super::component::connection::leave::PreDisconnectCleanupError::Lavalink(_) => { + Self::Lavalink } } } const fn from_leave_residual( error: &'a super::component::connection::leave::ResidualError, - ) -> Fe<'a> { + ) -> Self { match error { + super::component::connection::leave::ResidualError::GatewaySend(_) => Self::GatewaySend, + super::component::connection::leave::ResidualError::UnrecognisedConnection(_) => { + Self::UnrecognisedConnection + } super::component::connection::leave::ResidualError::InVoiceWithoutUser(e) => { Self::InVoiceWithoutUser(e) } - super::component::connection::leave::ResidualError::GatewaySend(e) => { - Self::GatewaySend(e) - } super::component::connection::leave::ResidualError::CheckUserOnlyIn(e) => { Self::from_check_user_only_in(e) } @@ -395,21 +411,11 @@ impl<'a> Fe<'a> { } } - const fn from_with_advance_lock_and_stopped( - error: &'a super::component::queue::remove::WithAdvanceLockAndStoppedError, - ) -> Fe<'a> { - match error { - super::component::queue::remove::WithAdvanceLockAndStoppedError::Lavalink(e) => { - Self::Lavalink(e) - } - } - } - const fn from_handle_suppressed_auto_join( error: &'a util::HandleSuppressedAutoJoinError, - ) -> Fe<'a> { + ) -> Self { match error { - util::HandleSuppressedAutoJoinError::DeserializeBody(e) => Self::DeserializeBody(e), + util::HandleSuppressedAutoJoinError::DeserializeBody(_) => Self::DeserializeBody, util::HandleSuppressedAutoJoinError::FollowUp(e) => Self::from_followup(e), util::HandleSuppressedAutoJoinError::AutoJoinSuppressed(e) => { Self::AutoJoinSuppressed(e) @@ -421,7 +427,7 @@ impl<'a> Fe<'a> { error: &'a util::ResidualGetUsersVoiceChannelError, ) -> Self { match error { - util::ResidualGetUsersVoiceChannelError::Cache(e) => Self::Cache(e), + util::ResidualGetUsersVoiceChannelError::Cache(_) => Self::Cache, } } @@ -435,10 +441,10 @@ impl<'a> Fe<'a> { const fn from_impl_connect_to_residual_2(error: &'a util::ResidualImplConnectToError) -> Self { match error { - util::ResidualImplConnectToError::Lavalink(e) => Self::Lavalink(e), - util::ResidualImplConnectToError::Cache(e) => Self::Cache(e), - util::ResidualImplConnectToError::GatewaySend(e) => Self::GatewaySend(e), - util::ResidualImplConnectToError::TwilightHttp(e) => Self::TwilightHttp(e), + util::ResidualImplConnectToError::Lavalink(_) => Self::Lavalink, + util::ResidualImplConnectToError::Cache(_) => Self::Cache, + util::ResidualImplConnectToError::GatewaySend(_) => Self::GatewaySend, + util::ResidualImplConnectToError::TwilightHttp(_) => Self::TwilightHttp, util::ResidualImplConnectToError::CheckUserAllowed(e) => { Self::from_check_user_allowed_residual_2(e) } @@ -464,7 +470,7 @@ impl<'a> Fe<'a> { } } - const fn from_auto_join_attempt(error: &'a util::AutoJoinAttemptError) -> Fe<'a> { + const fn from_auto_join_attempt(error: &'a util::AutoJoinAttemptError) -> Self { match error { util::AutoJoinAttemptError::Failed(e) => Self::AutoJoinAttemptFailed(e), util::AutoJoinAttemptError::ImplAutoJoin(e) => Self::from_impl_auto_join_residual(e), @@ -474,13 +480,13 @@ impl<'a> Fe<'a> { const fn from_auto_join_or_check_in_voice_with_user( error: &'a util::AutoJoinOrCheckInVoiceWithUserError, - ) -> Fe<'a> { + ) -> Self { match error { util::AutoJoinOrCheckInVoiceWithUserError::InVoiceWithoutUser(e) => { Self::InVoiceWithoutUser(e) } - util::AutoJoinOrCheckInVoiceWithUserError::CheckNotSuppressed(e) => { - Self::from_check_not_suppressed_error(e) + util::AutoJoinOrCheckInVoiceWithUserError::RequireUnsuppressed(e) => { + Self::from_require_unsuppressed_error(e) } util::AutoJoinOrCheckInVoiceWithUserError::HandleSuppressedAutoJoin(e) => { Self::from_handle_suppressed_auto_join(e) @@ -491,25 +497,25 @@ impl<'a> Fe<'a> { } } - const fn from_play(error: &'a super::component::queue::play::Error) -> Fe<'a> { + const fn from_play(error: &'a super::component::queue::play::Error) -> Self { match error { - super::component::queue::play::Error::Lavalink(e) => Self::Lavalink(e), - super::component::queue::play::Error::CheckNotSuppressed(e) => { - Self::from_check_not_suppressed_error(e) + super::component::queue::play::Error::Lavalink(_) => Self::Lavalink, + super::component::queue::play::Error::RequireUnsuppressed(e) => { + Self::from_require_unsuppressed_error(e) } super::component::queue::play::Error::Respond(e) => Self::from_respond(e), - super::component::queue::play::Error::Followup(e) => Self::from_followup(e), + super::component::queue::play::Error::RespondOrFollowup(e) => { + Self::from_respond_or_followup(e) + } super::component::queue::play::Error::AutoJoinOrCheckInVoiceWithUser(e) => { Self::from_auto_join_or_check_in_voice_with_user(e) } } } - const fn from_remove_tracks(error: &'a super::component::queue::RemoveTracksError) -> Fe<'a> { + const fn from_remove_tracks(error: &'a super::component::queue::RemoveTracksError) -> Self { match error { - super::component::queue::RemoveTracksError::TryWithAdvanceLock(e) => { - Self::from_with_advance_lock_and_stopped(e) - } + super::component::queue::RemoveTracksError::Lavalink(_) => Self::Lavalink, super::component::queue::RemoveTracksError::Respond(e) => Self::from_respond(e), super::component::queue::RemoveTracksError::Followup(e) => Self::from_followup(e), super::component::queue::RemoveTracksError::DeserializeBodyFromHttp(e) => { @@ -522,30 +528,34 @@ impl<'a> Fe<'a> { impl Error { pub const fn flatten_as(&self) -> Fe<'_> { match self { - Self::UserNotAccessManager(e) => Fe::UserNotAccessManager(e), - Self::Sqlx(e) => Fe::Sqlx(e), - Self::TaskJoin(e) => Fe::TaskJoin(e), - Self::EmbedValidation(e) => Fe::EmbedValidation(e), - Self::NotInVoice(e) => Fe::NotInVoice(e), - Self::InVoiceWithoutUser(e) => Fe::InVoiceWithoutUser(e), - Self::QueueEmpty(e) => Fe::QueueEmpty(e), + Self::UserNotAccessManager(_) => Fe::UserNotAccessManager, + Self::Sqlx(_) => Fe::Sqlx, + Self::TaskJoin(_) => Fe::TaskJoin, + Self::EmbedValidation(_) => Fe::EmbedValidation, + Self::NotInVoice(_) => Fe::NotInVoice, + Self::QueueEmpty(_) => Fe::QueueEmpty, + Self::UserNotDj(_) => Fe::UserNotDj, + Self::TwilightHttp(_) => Fe::TwilightHttp, + Self::Lavalink(_) => Fe::Lavalink, + Self::NoPlayer(_) => Fe::NoPlayer, + Self::NotInGuild(_) => Fe::NotInGuild, + Self::Cache(_) => Fe::Cache, + Self::NotPlaying(_) => Fe::NotPlaying, + Self::Paused(_) => Fe::Paused, + Self::UnrecognisedConnection(_) => Fe::UnrecognisedConnection, Self::PositionOutOfRange(e) => Fe::PositionOutOfRange(e), - Self::UserNotDj(e) => Fe::UserNotDj(e), - Self::TwilightHttp(e) => Fe::TwilightHttp(e), - Self::Lavalink(e) => Fe::Lavalink(e), - Self::NoPlayer(e) => Fe::NoPlayer(e), - Self::NotInGuild(e) => Fe::NotInGuild(e), - Self::Cache(e) => Fe::Cache(e), - Self::CheckNotSuppressed(e) => Fe::from_check_not_suppressed_error(e), + Self::InVoiceWithoutUser(e) => Fe::InVoiceWithoutUser(e), + Self::RequireUnsuppressed(e) => Fe::from_require_unsuppressed_error(e), Self::CheckUsersTrack(e) => Fe::from_users_track_error(e), - Self::InVoiceWithSomeoneElse(e) => Fe::from_in_voice_with_someone_else_error(e), + Self::RequireInVoiceWithSomeoneElse(e) => { + Fe::from_require_in_voice_with_someone_else_error(e) + } Self::CheckRun(e) => Fe::from_run(e), Self::Respond(e) => Fe::from_respond(e), Self::Followup(e) => Fe::from_followup(e), Self::PromptForConfirmation(e) => Fe::from_prompt_for_confirmation(e), Self::Join(e) => Fe::from_join_residual(e), Self::Leave(e) => Fe::from_leave_residual(e), - Self::WithAdvanceLockAndStopped(e) => Fe::from_with_advance_lock_and_stopped(e), Self::Play(e) => Fe::from_play(e), Self::DeserializeBodyFromHttp(e) => Fe::from_deserialize_body_from_http_error(e), Self::RemoveTracks(e) => Fe::from_remove_tracks(e), diff --git a/lyra/src/error/command/check.rs b/lyra/src/error/command/check.rs index d558e29..e0e973d 100644 --- a/lyra/src/error/command/check.rs +++ b/lyra/src/error/command/check.rs @@ -2,9 +2,13 @@ use thiserror::Error; use crate::error::{ self, Cache as CacheError, InVoiceWithoutUser as InVoiceWithoutUserError, NotInVoice, - NotUsersTrack as NotUsersTrackError, QueueNotSeekable as QueueNotSeekableError, + NotUsersTrack as NotUsersTrackError, PrettyErrorDisplay, PrettyInVoiceWithSomeoneElseDisplayer, + PrettyNotUsersTrackDisplayer, PrettyQueueNotSeekableDisplayer, + QueueNotSeekable as QueueNotSeekableError, }; +use super::require::UnsuppressedError; + #[derive(Error, Debug)] #[error(transparent)] pub enum AccessCalculatorBuildError { @@ -19,13 +23,6 @@ pub enum UserAllowedError { UserNotAllowed(#[from] error::UserNotAllowed), } -#[derive(Error, Debug)] -#[error(transparent)] -pub enum InVoiceWithSomeoneElseError { - Cache(#[from] CacheError), - InVoiceWithoutSomeoneElse(#[from] error::InVoiceWithoutSomeoneElse), -} - #[derive(Error, Debug)] #[error(transparent)] pub enum UserOnlyInError { @@ -48,13 +45,6 @@ pub enum InVoiceWithUserOnlyError { UserOnlyIn(#[from] UserOnlyInError), } -#[derive(Error, Debug)] -#[error(transparent)] -pub enum NotSuppressedError { - Cache(#[from] CacheError), - Suppressed(#[from] error::Suppressed), -} - #[derive(Error, Debug)] #[error(transparent)] pub enum CurrentlyPlayingUsersTrackError { @@ -93,12 +83,38 @@ impl From for PollResolvableError { } } -impl crate::error::EPrint for PollResolvableError { - fn eprint(&self) -> String { +impl<'a> PrettyErrorDisplay<'a> for PollResolvableError { + type Displayer = PrettyPollResolvableErrorDisplayer<'a>; + + fn pretty_display(&'a self) -> Self::Displayer { match self { - Self::InVoiceWithSomeoneElse(e) => e.eprint(), - Self::QueueNotSeekable(e) => e.eprint(), - Self::NotUsersTrack(e) => e.eprint(), + Self::InVoiceWithSomeoneElse(e) => { + PrettyPollResolvableErrorDisplayer::InVoiceWithSomeoneElse( + PrettyInVoiceWithSomeoneElseDisplayer(e), + ) + } + Self::QueueNotSeekable(_) => PrettyPollResolvableErrorDisplayer::QueueNotSeekable( + PrettyQueueNotSeekableDisplayer, + ), + Self::NotUsersTrack(e) => { + PrettyPollResolvableErrorDisplayer::NotUsersTrack(PrettyNotUsersTrackDisplayer(e)) + } + } + } +} + +pub enum PrettyPollResolvableErrorDisplayer<'a> { + InVoiceWithSomeoneElse(PrettyInVoiceWithSomeoneElseDisplayer<'a>), + NotUsersTrack(PrettyNotUsersTrackDisplayer<'a>), + QueueNotSeekable(PrettyQueueNotSeekableDisplayer), +} + +impl<'a> std::fmt::Display for PrettyPollResolvableErrorDisplayer<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PrettyPollResolvableErrorDisplayer::InVoiceWithSomeoneElse(e) => e.fmt(f), + PrettyPollResolvableErrorDisplayer::NotUsersTrack(e) => e.fmt(f), + PrettyPollResolvableErrorDisplayer::QueueNotSeekable(e) => e.fmt(f), } } } @@ -122,19 +138,36 @@ pub enum AlternateVoteResponse { #[error("poll was voided")] pub struct PollVoidedError(pub crate::command::poll::VoidingEvent); -impl crate::error::EPrint for PollVoidedError { - fn eprint(&self) -> String { +impl<'a> PrettyErrorDisplay<'a> for PollVoidedError { + type Displayer = PrettyVoidedErrorDisplayer; + + fn pretty_display(&'a self) -> Self::Displayer { match self.0 { crate::command::poll::VoidingEvent::QueueClear => { - String::from("the queue had been cleared") + PrettyVoidedErrorDisplayer::QueueClear } crate::command::poll::VoidingEvent::QueueRepeat => { - String::from("the queue had been set to repeat in another manner") + PrettyVoidedErrorDisplayer::QueueRepeat } } } } +pub enum PrettyVoidedErrorDisplayer { + QueueClear, + QueueRepeat, +} + +impl std::fmt::Display for PrettyVoidedErrorDisplayer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let data = match self { + Self::QueueClear => "the queue had been cleared", + Self::QueueRepeat => "the queue had been set to repeat in another manner", + }; + f.write_str(data) + } +} + #[derive(Error, Debug)] #[error(transparent)] pub enum HandlePollError { @@ -181,7 +214,7 @@ pub enum HandleInVoiceWithSomeoneElseError { pub enum RunError { NotInVoice(#[from] NotInVoice), QueueEmpty(#[from] error::QueueEmpty), - NotSuppressed(#[from] NotSuppressedError), + NotSuppressed(#[from] UnsuppressedError), NotPlaying(#[from] error::NotPlaying), InVoiceWithoutUser(#[from] InVoiceWithoutUserError), HandleInVoiceWithSomeoneElse(#[from] HandleInVoiceWithSomeoneElseError), diff --git a/lyra/src/error/command/declare.rs b/lyra/src/error/command/declare.rs index 35703c1..8030364 100644 --- a/lyra/src/error/command/declare.rs +++ b/lyra/src/error/command/declare.rs @@ -14,13 +14,13 @@ pub enum CommandExecuteError { UnknownCommand(PartialCommandData), } -pub enum FlattenedUntilUserNotAllowedCommandExecuteError<'a> { - Sqlx(&'a sqlx::Error), - TaskJoin(&'a tokio::task::JoinError), - UserNotAllowed(&'a crate::error::UserNotAllowed), - InteractionParse(&'a twilight_interactions::error::ParseError), - UnknownCommand(&'a PartialCommandData), - Command(&'a super::Error), +pub enum FlattenedUntilUserNotAllowedCommandExecuteError { + Sqlx, + TaskJoin, + UserNotAllowed, + InteractionParse, + UnknownCommand, + Command, } pub use FlattenedUntilUserNotAllowedCommandExecuteError as Fuunacee; @@ -30,14 +30,14 @@ impl CommandExecuteError { match self { Self::CheckUserAllowed(e) => match e { super::check::UserAllowedError::AccessCalculatorBuild(e) => match e { - super::check::AccessCalculatorBuildError::Sqlx(e) => Fuunacee::Sqlx(e), - super::check::AccessCalculatorBuildError::TaskJoin(e) => Fuunacee::TaskJoin(e), + super::check::AccessCalculatorBuildError::Sqlx(_) => Fuunacee::Sqlx, + super::check::AccessCalculatorBuildError::TaskJoin(_) => Fuunacee::TaskJoin, }, - super::check::UserAllowedError::UserNotAllowed(e) => Fuunacee::UserNotAllowed(e), + super::check::UserAllowedError::UserNotAllowed(_) => Fuunacee::UserNotAllowed, }, - Self::InteractionParse(e) => Fuunacee::InteractionParse(e), - Self::UnknownCommand(c) => Fuunacee::UnknownCommand(c), - Self::Command(e) => Fuunacee::Command(e), + Self::InteractionParse(_) => Fuunacee::InteractionParse, + Self::UnknownCommand(_) => Fuunacee::UnknownCommand, + Self::Command(_) => Fuunacee::Command, } } } diff --git a/lyra/src/error/command/require.rs b/lyra/src/error/command/require.rs new file mode 100644 index 0000000..2456cbd --- /dev/null +++ b/lyra/src/error/command/require.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub enum UnsuppressedError { + Cache(#[from] crate::error::Cache), + Suppressed(#[from] crate::error::Suppressed), +} + +#[derive(Error, Debug)] +#[error(transparent)] +pub enum InVoiceWithSomeoneElseError { + Cache(#[from] crate::error::Cache), + InVoiceWithoutSomeoneElse(#[from] crate::error::InVoiceWithoutSomeoneElse), +} diff --git a/lyra/src/error/command/util.rs b/lyra/src/error/command/util.rs index 760764b..760dc90 100644 --- a/lyra/src/error/command/util.rs +++ b/lyra/src/error/command/util.rs @@ -6,15 +6,7 @@ use twilight_model::id::{marker::MessageMarker, Id}; pub enum PromptForConfirmationError { StandbyCanceled(#[from] twilight_standby::future::Canceled), Respond(#[from] super::RespondError), - Confirmation(#[from] ConfirmationError), -} - -#[derive(Error, Debug)] -pub enum ConfirmationError { - #[error("confirmation cancelled")] - Cancelled, - #[error("confirmation timed out")] - TimedOut, + ConfirmationTimedout(#[from] crate::error::ConfirmationTimedOut), } #[derive(thiserror::Error, Debug)] @@ -122,6 +114,11 @@ impl AutoJoinAttemptError { ), )) } + crate::error::component::connection::join::ImplConnectToError::UnrecognisedConnection(_) => { + // SAFETY: if an auto-join was performed, then the `require::in_voice(_)` call was unsuccessful, + // which is impossible as this error will only be raised if there is an unrecognised connection found. + unsafe { std::hint::unreachable_unchecked() } + }, crate::error::component::connection::join::ImplConnectToError::CheckUserAllowed(e) => { Self::from_check_user_allowed(e) } @@ -168,7 +165,7 @@ impl crate::error::component::connection::join::AutoJoinError { #[error(transparent)] pub enum AutoJoinOrCheckInVoiceWithUserError { InVoiceWithoutUser(#[from] crate::error::InVoiceWithoutUser), - CheckNotSuppressed(#[from] super::check::NotSuppressedError), + RequireUnsuppressed(#[from] super::require::UnsuppressedError), AutoJoinAttempt(#[from] AutoJoinAttemptError), HandleSuppressedAutoJoin(#[from] HandleSuppressedAutoJoinError), } diff --git a/lyra/src/error/component.rs b/lyra/src/error/component.rs index a3ca6ed..6556d26 100644 --- a/lyra/src/error/component.rs +++ b/lyra/src/error/component.rs @@ -1,2 +1,3 @@ pub mod connection; +pub mod playback; pub mod queue; diff --git a/lyra/src/error/component/connection.rs b/lyra/src/error/component/connection.rs index fc1ce96..b2d3c5e 100644 --- a/lyra/src/error/component/connection.rs +++ b/lyra/src/error/component/connection.rs @@ -26,6 +26,7 @@ pub mod join { GatewaySend(#[from] twilight_gateway::error::ChannelError), TwilightHttp(#[from] twilight_http::Error), Lavalink(#[from] lavalink_rs::error::LavalinkError), + UnrecognisedConnection(#[from] crate::error::UnrecognisedConnection), } #[derive(thiserror::Error, Debug)] @@ -62,9 +63,9 @@ pub mod join { #[error(transparent)] pub enum HandleResponseError { Cache(#[from] crate::error::Cache), - Respond(#[from] crate::error::command::RespondError), DeserializeBody(#[from] twilight_http::response::DeserializeBodyError), Followup(#[from] crate::error::command::FollowupError), + RespondOrFollowup(#[from] crate::error::command::RespondOrFollowupError), } #[derive(thiserror::Error, Debug)] @@ -125,6 +126,7 @@ pub mod join { GatewaySend(#[from] twilight_gateway::error::ChannelError), TwilightHttp(#[from] twilight_http::Error), Lavalink(#[from] lavalink_rs::error::LavalinkError), + UnrecognisedConnection(#[from] crate::error::UnrecognisedConnection), } #[derive(thiserror::Error, Debug)] @@ -150,9 +152,9 @@ pub mod join { HandleResponseError::Cache(e) => { Self::Other(ResidualError::HandleResponse(HandleResponseError::Cache(e))) } - HandleResponseError::Respond(e) => Self::Other(ResidualError::HandleResponse( - HandleResponseError::Respond(e), - )), + HandleResponseError::RespondOrFollowup(e) => Self::Other( + ResidualError::HandleResponse(HandleResponseError::RespondOrFollowup(e)), + ), HandleResponseError::DeserializeBody(e) => Self::Other( ResidualError::HandleResponse(HandleResponseError::DeserializeBody(e)), ), @@ -203,6 +205,13 @@ pub mod join { ResidualImplConnectToError::Lavalink(e), )), )), + ImplConnectToError::UnrecognisedConnection(e) => { + Self::Other(ResidualError::ImplJoin(ResidualImplJoinError::ConnectTo( + ResidualConnectToError::ImplConnectTo( + ResidualImplConnectToError::UnrecognisedConnection(e), + ), + ))) + } ImplConnectToError::CheckUserAllowed(e) => Self::from_check_user_allowed(e), } } @@ -266,6 +275,7 @@ pub mod leave { CheckUserOnlyIn(#[from] crate::error::command::check::UserOnlyInError), PreDisconnectCleanup(#[from] PreDisconnectCleanupError), GatewaySend(#[from] twilight_gateway::error::ChannelError), + UnrecognisedConnection(#[from] crate::error::UnrecognisedConnection), } impl Error { @@ -284,6 +294,9 @@ pub mod leave { Self::GatewaySend(e) => { NotInVoiceMatchedError::Other(ResidualError::GatewaySend(e)) } + Self::UnrecognisedConnection(e) => { + NotInVoiceMatchedError::Other(ResidualError::UnrecognisedConnection(e)) + } } } } @@ -300,6 +313,7 @@ pub mod leave { CheckUserOnlyIn(#[from] crate::error::command::check::UserOnlyInError), PreDisconnectCleanupError(#[from] PreDisconnectCleanupError), GatewaySend(#[from] twilight_gateway::error::ChannelError), + UnrecognisedConnection(#[from] crate::error::UnrecognisedConnection), } } @@ -323,6 +337,7 @@ pub enum HandleVoiceStateUpdateError { MessageValidation(#[from] twilight_validate::message::MessageValidationError), MatchStateChannelID(#[from] MatchStateChannelIdError), PreDisconnectCleanup(#[from] leave::PreDisconnectCleanupError), + Lavalink(#[from] lavalink_rs::error::LavalinkError), } #[derive(Error, Debug)] @@ -331,4 +346,5 @@ pub enum MatchStateChannelIdError { Http(#[from] twilight_http::Error), MessageValidation(#[from] twilight_validate::message::MessageValidationError), Cache(#[from] crate::error::Cache), + Lavalink(#[from] lavalink_rs::error::LavalinkError), } diff --git a/lyra/src/error/component/playback.rs b/lyra/src/error/component/playback.rs new file mode 100644 index 0000000..a0a3ea1 --- /dev/null +++ b/lyra/src/error/component/playback.rs @@ -0,0 +1,8 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[error("handling `VoiceStateUpdate` failed: {:?}", .0)] +pub enum HandleVoiceStateUpdateError { + Lavalink(#[from] lavalink_rs::error::LavalinkError), + TwilightHttp(#[from] twilight_http::Error), +} diff --git a/lyra/src/error/component/queue.rs b/lyra/src/error/component/queue.rs index f524a07..c74b226 100644 --- a/lyra/src/error/component/queue.rs +++ b/lyra/src/error/component/queue.rs @@ -19,9 +19,9 @@ pub mod play { #[derive(thiserror::Error, Debug)] #[error("playing failed: {:?}", .0)] pub enum Error { - CheckNotSuppressed(#[from] crate::error::command::check::NotSuppressedError), + RequireUnsuppressed(#[from] crate::error::command::require::UnsuppressedError), Respond(#[from] crate::error::command::RespondError), - Followup(#[from] crate::error::command::FollowupError), + RespondOrFollowup(#[from] crate::error::command::RespondOrFollowupError), AutoJoinOrCheckInVoiceWithUser( #[from] crate::error::command::util::AutoJoinOrCheckInVoiceWithUserError, ), @@ -29,22 +29,12 @@ pub mod play { } } -pub mod remove { - use thiserror::Error; - - #[derive(Error, Debug)] - #[error(transparent)] - pub enum WithAdvanceLockAndStoppedError { - Lavalink(#[from] lavalink_rs::error::LavalinkError), - } -} - use thiserror::Error; #[derive(Error, Debug)] pub enum RemoveTracksError { #[error(transparent)] - TryWithAdvanceLock(#[from] remove::WithAdvanceLockAndStoppedError), + Lavalink(#[from] lavalink_rs::error::LavalinkError), #[error(transparent)] Respond(#[from] crate::error::command::RespondError), #[error(transparent)] diff --git a/lyra/src/error/gateway.rs b/lyra/src/error/gateway.rs index 62a8366..33f43f2 100644 --- a/lyra/src/error/gateway.rs +++ b/lyra/src/error/gateway.rs @@ -1,12 +1,5 @@ use thiserror::Error; -#[derive(Error, Debug)] -#[error("handling confirmation error failed: {:?}", .0)] -pub enum MatchConfirmationError { - Http(#[from] twilight_http::Error), - Followup(#[from] super::core::FollowupError), -} - #[derive(Error, Debug)] pub enum ProcessError { #[error(transparent)] @@ -24,9 +17,11 @@ pub enum ProcessError { #[error(transparent)] Cache(#[from] super::Cache), #[error(transparent)] - HandleVoiceStateUpdate(#[from] super::component::connection::HandleVoiceStateUpdateError), + ConnectionHandleVoiceStateUpdate( + #[from] super::component::connection::HandleVoiceStateUpdateError, + ), #[error(transparent)] - MatchConfirmation(#[from] MatchConfirmationError), + PlaybackHandleVoiceStateUpdate(#[from] super::component::playback::HandleVoiceStateUpdateError), #[error(transparent)] Respond(#[from] super::command::RespondError), #[error("error executing command `/{}`: {:?}", .name, .source)] diff --git a/lyra/src/error/lavalink.rs b/lyra/src/error/lavalink.rs index a556ca2..9d93050 100644 --- a/lyra/src/error/lavalink.rs +++ b/lyra/src/error/lavalink.rs @@ -8,6 +8,8 @@ pub struct NoPlayerError; #[error("processing lavalink event failed: {:?}", .0)] pub enum ProcessError { Lavalink(#[from] lavalink_rs::error::LavalinkError), + TwilightHttp(#[from] twilight_http::Error), + Sqlx(#[from] sqlx::Error), } pub type ProcessResult = Result<(), ProcessError>; diff --git a/lyra/src/gateway/guild.rs b/lyra/src/gateway/guild.rs index c646bbc..406b9ba 100644 --- a/lyra/src/gateway/guild.rs +++ b/lyra/src/gateway/guild.rs @@ -1,5 +1,9 @@ +use lyra_ext::num::u64_to_i64_truncating; use twilight_gateway::ShardId; -use twilight_model::gateway::payload::incoming::{GuildCreate, GuildDelete}; +use twilight_model::{ + gateway::payload::incoming::{GuildCreate, GuildDelete}, + guild::Guild, +}; use super::model::Process; use crate::{ @@ -16,9 +20,12 @@ pub(super) struct CreateContext<'a> { impl CreateContext<'_> { async fn increment_guild_count(&self) -> Result<(), sqlx::Error> { /* FIXME: wait until twilight stop deserializing missing `Guild::unavailable` to false: - https://github.com/twilight-rs/twilight/pull/2330 + https://github.com/twilight-rs/twilight/issues/2372 */ - if !self.inner.unavailable { + if let GuildCreate::Available(Guild { + unavailable: false, .. + }) = self.inner + { return Ok(()); } @@ -30,7 +37,7 @@ impl CreateContext<'_> { NOT EXISTS ( SELECT 1 FROM guild_configs WHERE id = $1 );", - self.inner.id.get() as i64 + u64_to_i64_truncating(self.inner.id().get()) ) .execute(self.bot.db()) .await?; diff --git a/lyra/src/gateway/interaction.rs b/lyra/src/gateway/interaction.rs index fec9e72..d26f273 100644 --- a/lyra/src/gateway/interaction.rs +++ b/lyra/src/gateway/interaction.rs @@ -1,5 +1,6 @@ use std::{hint::unreachable_unchecked, sync::Arc}; +use tokio::sync::oneshot; use twilight_gateway::{Latency, MessageSender}; use twilight_mention::Mention; use twilight_model::{ @@ -13,8 +14,12 @@ use twilight_model::{ use super::model::Process; use crate::{ command::{ - macros::{bad, cant, caut, crit, err, hid, nope, note, out_upd, sus, sus_fol}, + macros::{ + bad, bad_or_fol, cant_or_fol, caut, crit, crit_or_fol, err, hid, nope, nope_or_fol, + note, out_upd, sus, sus_fol, + }, model::NonPingInteraction, + require, util::MessageLinkAware, AutocompleteCtx, MessageCtx, SlashCtx, }, @@ -24,7 +29,7 @@ use crate::{ BotState, InteractionClient, InteractionInterface, OwnedBotState, UnitFollowupResult, UnitRespondResult, }, - r#const::exit_code::{DUBIOUS, FORBIDDEN, WARNING}, + r#const::exit_code::{DUBIOUS, PROHIBITED, WARNING}, }, error::{ command::{ @@ -32,14 +37,15 @@ use crate::{ AlternateVoteResponse, AnotherPollOngoingError, PollLossError, PollLossErrorKind, }, declare::{CommandExecuteError, Fuunacee}, - util::{AutoJoinSuppressedError, ConfirmationError}, - Error as CommandError, Fe, RespondError, + util::AutoJoinSuppressedError, + Error as CommandError, Fe, }, - gateway::{MatchConfirmationError, ProcessError, ProcessResult}, - AutoJoinAttemptFailed as AutoJoinAttemptFailedError, EPrint, - PositionOutOfRange as PositionOutOfRangeError, Suppressed as SuppressedError, + gateway::{ProcessError, ProcessResult}, + AutoJoinAttemptFailed as AutoJoinAttemptFailedError, + PositionOutOfRange as PositionOutOfRangeError, PrettyErrorDisplay, + Suppressed as SuppressedError, }, - lavalink::LavalinkAware, + LavalinkAware, }; pub(super) struct Context { @@ -76,7 +82,9 @@ impl Process for Context { } // SAFETY: `self.inner.kind` is `MessageComponent`, so this is safe InteractionType::MessageComponent => unsafe { self.process_as_component() }.await, - InteractionType::ModalSubmit | InteractionType::Ping => Ok(()), // ignored + // SAFETY: `self.inner.kind` is `ModalSubmit`, so this is safe + InteractionType::ModalSubmit => unsafe { self.process_as_modal() }.await, + InteractionType::Ping => Ok(()), // ignored _ => unimplemented!(), } } @@ -97,6 +105,7 @@ impl Context { let inner_guild_id = self.inner.guild_id; // SAFETY: interaction type is not `Ping`, so `channel` is present let channel_id = unsafe { self.inner.channel_id_unchecked() }; + let (tx, mut rx) = oneshot::channel::<()>(); let result = match data.kind { CommandType::ChatInput => { @@ -106,11 +115,11 @@ impl Context { bot.clone(), self.latency, self.sender, + tx, ) .execute(*data) .await } - CommandType::User => todo!(), CommandType::Message => { MessageCtx::from_partial_data( self.inner, @@ -118,18 +127,22 @@ impl Context { bot.clone(), self.latency, self.sender, + tx, ) .execute(*data) .await } + CommandType::User => todo!(), _ => unimplemented!(), }; if let Some(guild_id) = inner_guild_id { - if let Some(mut connection) = bot.lavalink().get_connection_mut(guild_id) { - if connection.text_channel_id != channel_id { - connection.text_channel_id = channel_id; - } + let lavalink = bot.lavalink(); + if let Some(mut connection) = lavalink.get_connection_mut(guild_id) { + connection.text_channel_id = channel_id; + } + if let Ok(player) = require::player(&(lavalink, guild_id)) { + player.data().write().await.set_text_channel_id(channel_id); } } @@ -137,17 +150,18 @@ impl Context { return Ok(()); }; + let acknowledged = rx.try_recv().is_ok(); match source.flatten_until_user_not_allowed_as() { - Fuunacee::UserNotAllowed(_) => { + Fuunacee::UserNotAllowed => { nope!("You are not allowed to use commands in this context.", i); } - Fuunacee::Command(_) => { + Fuunacee::Command => { let CommandExecuteError::Command(error) = source else { // SAFETY: `source.flatten_until_user_not_allowed_as()` is `Fuunacee::Command(_)`, // so the unflattened source error must be `CommandExecuteError::Command(_)` unsafe { unreachable_unchecked() } }; - match_error(error, name, i).await + match_error(error, name, acknowledged, i).await } _ => { crit!(format!( @@ -160,17 +174,20 @@ impl Context { async unsafe fn process_as_autocomplete(mut self) -> ProcessResult { let Some(InteractionData::ApplicationCommand(data)) = self.inner.data.take() else { - // SAFETY: + // SAFETY: interaction is of type `ApplicationCommandAutocomplete`, + // so `self.inner.data.take()` will always be `InteractionData::ApplicationCommand(_)` unsafe { unreachable_unchecked() } }; let name = data.name.clone().into(); + let (tx, _) = oneshot::channel::<()>(); let Err(source) = ::from_partial_data( self.inner, &data, self.bot, self.latency, self.sender, + tx, ) .execute(*data) .await @@ -182,8 +199,27 @@ impl Context { } #[allow(clippy::unused_async)] - async unsafe fn process_as_component(self) -> ProcessResult { + async unsafe fn process_as_component(mut self) -> ProcessResult { + let Some(InteractionData::MessageComponent(data)) = self.inner.data.take() else { + // SAFETY: interaction is of type `MessageComponent`, + // so `self.inner.data.take()` will always be `InteractionData::MessageComponent(_)` + unsafe { unreachable_unchecked() } + }; + tracing::trace!(?data); // TODO: implement controller + + Ok(()) + } + + #[allow(clippy::unused_async)] + async unsafe fn process_as_modal(mut self) -> ProcessResult { + let Some(InteractionData::ModalSubmit(data)) = self.inner.data.take() else { + // SAFETY: interaction is of type `ModalSubmit`, + // so `self.inner.data.take()` will always be `InteractionData::ModalSubmit(_)` + unsafe { unreachable_unchecked() } + }; + tracing::trace!(?data); + Ok(()) } } @@ -191,24 +227,28 @@ impl Context { async fn match_error( error: CommandError, command_name: Box, + acknowledged: bool, i: InteractionInterface<'_>, ) -> Result<(), ProcessError> { match error.flatten_as() { - Fe::Cache(_) => { + Fe::Cache => { tracing::warn!("cache error: {:#?}", error); - crit!("Something isn't working at the moment, try again later.", i); + crit_or_fol!( + "Something isn't working at the moment, try again later.", + (i, acknowledged) + ); } - Fe::UserNotDj(_) => { + Fe::UserNotDj => { nope!("You need to be a ***DJ*** to do that.", i); } - Fe::UserNotAccessManager(_) => { + Fe::UserNotAccessManager => { nope!("You need to be an ***Access Manager*** to do that.", i); } // Fe::UserNotPlaylistManager(_) => { // nope!("You need to be a ***Playlist Manager*** to do that.", i); // } - Fe::NotInVoice(_) => { + Fe::NotInVoice => { let join = InteractionClient::mention_command::(); let play = InteractionClient::mention_command::(); caut!( @@ -220,49 +260,66 @@ async fn match_error( ); } Fe::InVoiceWithoutUser(e) => { - nope!( + nope_or_fol!( format!( "You are not with the bot in {}; You need to be a ***DJ*** to do that.", e.0.mention(), ), - i + (i, acknowledged) ); } Fe::InVoiceWithSomeoneElse(e) => { - nope!(e.eprint(), i); + nope!(e.pretty_display(), i); } Fe::InVoiceWithoutSomeoneElse(e) => { bad!(format!("Not enough people are in {}.", e.0.mention()), i); } - Fe::Suppressed(e) => Ok(match_suppressed(e, i).await?), + Fe::Suppressed(e) => Ok(match_suppressed(e, (i, acknowledged)).await?), Fe::AutoJoinSuppressed(e) => Ok(match_autojoin_suppressed(e, i).await?), - Fe::AutoJoinAttemptFailed(e) => Ok(match_autojoin_attempt_failed(e, i).await?), - Fe::Stopped(_) => todo!(), - Fe::NotPlaying(_) => todo!(), - Fe::Paused(_) => todo!(), + Fe::AutoJoinAttemptFailed(e) => { + Ok(match_autojoin_attempt_failed(e, (i, acknowledged)).await?) + } + Fe::Stopped => todo!(), + Fe::NotPlaying => { + bad!("Currently not playing anything.", i); + } + Fe::Paused => { + bad!("Currently paused.", i); + } Fe::QueueNotSeekable(e) => { - nope!(e.eprint(), i); + nope!(e.pretty_display(), i); } - Fe::QueueEmpty(_) => { + Fe::QueueEmpty => { bad!("The queue is currently empty.", i); } Fe::PositionOutOfRange(e) => Ok(match_position_out_of_range(e, i).await?), Fe::NotUsersTrack(e) => { - nope!(e.eprint(), i); + nope!(e.pretty_display(), i); } Fe::AnotherPollOngoing(e) => Ok(match_another_poll_ongoing(e, i).await?), Fe::PollLoss(e) => Ok(match_poll_loss(e, i).await?), Fe::PollVoided(e) => { out_upd!( - format!("{WARNING} This poll has been voided as: {}.", e.eprint()), + format!( + "{WARNING} This poll has been voided as: {}.", + e.pretty_display() + ), i ); } - Fe::Confirmation(e) => Ok(match_confirmation(e, i).await?), - Fe::NoPlayer(_) => { + Fe::ConfirmationTimedOut => { + sus_fol!("Confirmation timed out.", i); + } + Fe::NoPlayer => { let play = InteractionClient::mention_command::(); caut!(format!("Not yet played anything. Use {} first.", play), i); } + Fe::UnrecognisedConnection => { + crit!( + "The bot wasn't disconnected properly last session. Please wait for it to automatically leave the voice channel, then try again.", + i + ); + } _ => { err!(format!("Something went wrong: ```rs\n{error:#?}```"), ?i); Err(ProcessError::CommandExecute { @@ -275,14 +332,14 @@ async fn match_error( async fn match_suppressed( error: &SuppressedError, - i: InteractionInterface<'_>, -) -> UnitRespondResult { + mut ia: (InteractionInterface<'_>, bool), +) -> UnitFollowupResult { match error { SuppressedError::Muted => { - bad!("Currently server muted.", i); + bad_or_fol!("Currently server muted.", ia); } SuppressedError::NotSpeaker => { - bad!("Not currently a speaker in this stage channel.", i); + bad_or_fol!("Not currently a speaker in this stage channel.", ia); } } } @@ -309,33 +366,33 @@ async fn match_autojoin_suppressed( async fn match_autojoin_attempt_failed( error: &AutoJoinAttemptFailedError, - i: InteractionInterface<'_>, -) -> Result<(), RespondError> { + mut ia: (InteractionInterface<'_>, bool), +) -> UnitFollowupResult { match error { AutoJoinAttemptFailedError::UserNotInVoice(_) => { let join = InteractionClient::mention_command::(); - bad!( + bad_or_fol!( format!( "Please join a voice channel, or use {} to connect to a channel.", join ), - i + ia ); } AutoJoinAttemptFailedError::UserNotAllowed(_) => { - nope!("Attempting to join your currently connected channel failed as you are not allowed to use the bot here.", i); + nope_or_fol!("Attempting to join your currently connected channel failed as you are not allowed to use the bot here.", ia); } AutoJoinAttemptFailedError::Forbidden(e) => { - cant!( + cant_or_fol!( format!( "Attempting to join {} failed due to insufficient permissions.", e.0.mention() ), - i + ia ); } AutoJoinAttemptFailedError::UserNotStageManager(_) => { - nope!("Attempting to join your currently connected stage failed as you are not a **Stage Manager**.", i); + nope_or_fol!("Attempting to join your currently connected stage failed as you are not a **Stage Manager**.", ia); } } } @@ -363,20 +420,6 @@ async fn match_position_out_of_range( bad!(message, i); } -async fn match_confirmation( - error: &ConfirmationError, - i: InteractionInterface<'_>, -) -> Result<(), MatchConfirmationError> { - match error { - ConfirmationError::Cancelled => { - note!("Cancelled executing this command.", i); - } - ConfirmationError::TimedOut => { - sus_fol!("Confirmation timed out.", i); - } - } -} - async fn match_another_poll_ongoing( error: &AnotherPollOngoingError, i: InteractionInterface<'_>, @@ -417,5 +460,8 @@ async fn match_poll_loss(error: &PollLossError, i: InteractionInterface<'_>) -> PollLossErrorKind::SupersededLossViaDj => "The poll was superseded to lose by a DJ: ", }; - out_upd!(format!("{FORBIDDEN} {source_txt}{}", source.eprint()), i); + out_upd!( + format!("{PROHIBITED} {source_txt}{}", source.pretty_display()), + i + ); } diff --git a/lyra/src/gateway/model.rs b/lyra/src/gateway/model.rs index af64cd7..b7e443b 100644 --- a/lyra/src/gateway/model.rs +++ b/lyra/src/gateway/model.rs @@ -29,16 +29,16 @@ pub struct LastCachedStates { impl LastCachedStates { pub fn new(cache: &InMemoryCache, event: &Event) -> Self { let voice_state = match event { - Event::VoiceStateUpdate(event) => cache - .voice_state( - event.user_id, - // SAFETY: this bot cannot join DM voice calls, - // meaning all voice states will be from a guild voice channel, - // so `e.guild_id` is present - unsafe { event.guild_id.unwrap_unchecked() }, - ) - .as_deref() - .cloned(), + Event::VoiceStateUpdate(event) => { + // SAFETY: this bot cannot join DM voice calls, + // meaning all voice states will be from a guild voice channel, + // so `e.guild_id` is present + let guild_id = unsafe { event.guild_id.unwrap_unchecked() }; + cache + .voice_state(event.user_id, guild_id) + .as_deref() + .cloned() + } _ => None, }; diff --git a/lyra/src/gateway/shard.rs b/lyra/src/gateway/shard.rs index f608cdb..0d35c7b 100644 --- a/lyra/src/gateway/shard.rs +++ b/lyra/src/gateway/shard.rs @@ -1,3 +1,4 @@ +use lyra_ext::num::u64_to_i64_truncating; use tokio::task::JoinSet; use twilight_gateway::ShardId; use twilight_model::gateway::payload::incoming::Ready; @@ -41,7 +42,7 @@ impl Process for ReadyContext<'_> { self.inner.guilds.iter().for_each(|g| { let db = self.bot.db().clone(); - let guild_id = g.id.get() as i64; + let guild_id = u64_to_i64_truncating(g.id.get()); set.spawn(async move { sqlx::query!( "INSERT INTO guild_configs diff --git a/lyra/src/gateway/voice.rs b/lyra/src/gateway/voice.rs index b9f00a5..99c8ef8 100644 --- a/lyra/src/gateway/voice.rs +++ b/lyra/src/gateway/voice.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use futures::TryFutureExt; use twilight_model::gateway::payload::incoming::VoiceStateUpdate; use twilight_cache_inmemory::{model::CachedVoiceState, InMemoryCache}; @@ -8,12 +9,13 @@ use twilight_http::Client; use twilight_model::id::marker::GuildMarker; use twilight_model::id::Id; -use crate::component::{connection, tuning}; -use crate::core::model::{BotState, BotStateAware, CacheAware, HttpAware, OwnedBotStateAware}; -use crate::error::gateway::ProcessResult; use crate::{ + component::{connection, playback, tuning}, + core::model::{BotState, BotStateAware, CacheAware, HttpAware, OwnedBotStateAware}, + error::gateway::{ProcessError, ProcessResult}, gateway::{GuildIdAware, SenderAware}, - lavalink::{Lavalink, LavalinkAware}, + lavalink::Lavalink, + LavalinkAndGuildIdAware, LavalinkAware, }; use super::{LastCachedStates, Process}; @@ -92,11 +94,21 @@ impl GuildIdAware for Context { } } +impl LavalinkAndGuildIdAware for Context {} + impl Process for Context { async fn process(self) -> ProcessResult { - connection::handle_voice_state_update(&self).await?; - tuning::handle_voice_state_update(&self).await?; - + let connection_changed = match self.get_connection() { + Some(connection) => connection.changed().await, + None => false, + }; + + tokio::try_join![ + connection::handle_voice_state_update(&self, connection_changed) + .map_err(ProcessError::from), + playback::handle_voice_state_update(&self, connection_changed).map_err(Into::into), + tuning::handle_voice_state_update(&self).map_err(Into::into), + ]?; Ok(()) } } diff --git a/lyra/src/lavalink.rs b/lyra/src/lavalink.rs index 4a1c06a..d176b3e 100644 --- a/lyra/src/lavalink.rs +++ b/lyra/src/lavalink.rs @@ -6,10 +6,10 @@ mod track; pub use self::{ model::{ - wait_for_with, ClientAware as LavalinkAware, Connection, CorrectPlaylistInfo, - CorrectTrackInfo, DelegateMethods, Event, EventRecvResult, IndexerType, Lavalink, Pitch, - PlayerAware, PlayerDataRwLockArc, Queue, QueueItem, RepeatMode, UnwrappedPlayerData, - UnwrappedPlayerInfoUri, + wait_for_with, ClientAndGuildIdAware, ClientAware, ClientData, Connection, + CorrectPlaylistInfo, CorrectTrackInfo, DelegateMethods, Event, EventRecvResult, + IndexerType, Lavalink, OwnedPlayerData, Pitch, PlayerDataRead, PlayerDataWrite, Queue, + QueueItem, RepeatMode, UnwrappedData, UnwrappedPlayerInfoUri, }, process::handlers, }; diff --git a/lyra/src/lavalink/model.rs b/lyra/src/lavalink/model.rs index 428b0c9..141d8a2 100644 --- a/lyra/src/lavalink/model.rs +++ b/lyra/src/lavalink/model.rs @@ -4,7 +4,7 @@ mod pitch; mod queue; mod queue_indexer; -use std::{num::NonZeroU16, sync::Arc}; +use std::{num::NonZeroU16, sync::Arc, time::Duration}; use lavalink_rs::{ client::LavalinkClient, @@ -12,15 +12,19 @@ use lavalink_rs::{ model::{player::ConnectionInfo, track::TrackInfo}, player_context::PlayerContext, }; -use tokio::sync::RwLock; +use lyra_ext::time::track_timestamp::TrackTimestamp; +use sqlx::{Pool, Postgres}; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use twilight_http::Client; use twilight_model::id::{ - marker::{GuildMarker, MessageMarker}, + marker::{ChannelMarker, GuildMarker}, Id, }; use crate::{ command::require::{InVoice, PartialInVoice}, - core::r#const, + core::{model::HttpAware, r#const}, + error::UnrecognisedConnection, gateway::GuildIdAware, }; @@ -34,33 +38,61 @@ pub use self::{ queue_indexer::IndexerType, }; -pub type PlayerDataRwLockArc = Arc>; +type PlayerData = RwLock; +pub type OwnedPlayerData = Arc; +pub type PlayerDataRead<'a> = RwLockReadGuard<'a, RawPlayerData>; +pub type PlayerDataWrite<'a> = RwLockWriteGuard<'a, RawPlayerData>; +pub type OwnedClientData = Arc; pub trait ClientAware { fn lavalink(&self) -> &Lavalink; } -pub trait PlayerAware: ClientAware + GuildIdAware { +pub trait ClientAndGuildIdAware: ClientAware + GuildIdAware { fn get_player(&self) -> Option { self.lavalink().get_player_context(self.guild_id()) } + + fn get_connection(&self) -> Option { + self.lavalink().get_connection(self.guild_id()) + } + + fn get_connection_mut(&self) -> Option { + self.lavalink().get_connection_mut(self.guild_id()) + } +} + +type ClientRefAndGuildId<'a> = (&'a Lavalink, Id); + +impl ClientAware for ClientRefAndGuildId<'_> { + fn lavalink(&self) -> &Lavalink { + self.0 + } +} +impl GuildIdAware for ClientRefAndGuildId<'_> { + fn guild_id(&self) -> Id { + self.1 + } } +impl ClientAndGuildIdAware for ClientRefAndGuildId<'_> {} -pub struct PlayerData { +pub struct RawPlayerData { queue: Queue, volume: NonZeroU16, pitch: Pitch, - now_playing_message_id: Option>, + track_timestamp: TrackTimestamp, + text_channel_id: Id, } -impl PlayerData { - pub const fn new() -> Self { +impl RawPlayerData { + pub fn new(text_channel_id: Id) -> Self { Self { + text_channel_id, // SAFETY: `100` is non-zero volume: unsafe { NonZeroU16::new_unchecked(100) }, pitch: Pitch::new(), queue: Queue::new(), - now_playing_message_id: None, + track_timestamp: TrackTimestamp::new(), } } @@ -68,6 +100,12 @@ impl PlayerData { &self.queue } + #[inline] + pub fn reset_track_timestamp(&mut self) { + self.track_timestamp.reset(); + } + + #[inline] pub fn queue_mut(&mut self) -> &mut Queue { &mut self.queue } @@ -76,13 +114,47 @@ impl PlayerData { self.volume } + #[inline] pub fn set_volume(&mut self, volume: NonZeroU16) { self.volume = volume; } + #[inline] pub fn pitch_mut(&mut self) -> &mut Pitch { &mut self.pitch } + + pub const fn paused(&self) -> bool { + self.track_timestamp.paused() + } + + #[inline] + pub fn timestamp(&self) -> Duration { + self.track_timestamp.get() + } + + #[inline] + pub fn set_pause(&mut self, state: bool) { + self.track_timestamp.set_pause(state); + } + + #[inline] + pub fn seek_to(&mut self, timestamp: Duration) { + self.track_timestamp.seek_to(timestamp); + } + + #[inline] + pub fn set_speed(&mut self, multiplier: f64) { + self.track_timestamp.set_speed(multiplier); + } + + pub const fn text_channel_id(&self) -> Id { + self.text_channel_id + } + + pub fn set_text_channel_id(&mut self, text_channel_id: Id) { + self.text_channel_id = text_channel_id; + } } pub struct Lavalink { @@ -205,6 +277,20 @@ pub trait DelegateMethods { guild_id: impl Into + Send, timeout: std::time::Duration, ) -> LavalinkResult; + async fn get_connection_info_traced( + &self, + guild_id: impl Into + Send, + ) -> LavalinkResult { + let now = tokio::time::Instant::now(); + let info = self + .get_connection_info( + guild_id, + r#const::connection::GET_LAVALINK_CONNECTION_INFO_TIMEOUT, + ) + .await?; + tracing::trace!("getting lavalink connection info took {:?}", now.elapsed()); + Ok(info) + } async fn create_player_context_with_data( &self, @@ -215,17 +301,10 @@ pub trait DelegateMethods { async fn new_player( &self, guild_id: impl Into + Send + Copy, + channel_id: Id, ) -> LavalinkResult { - let now = tokio::time::Instant::now(); - let info = self - .get_connection_info( - guild_id, - *r#const::connection::get_lavalink_connection_info_timeout(), - ) - .await?; - tracing::trace!("getting lavalink connection info took {:?}", now.elapsed()); - - let data = Arc::new(RwLock::new(PlayerData::new())); + let info = self.get_connection_info_traced(guild_id).await?; + let data = Arc::new(RwLock::new(RawPlayerData::new(channel_id))); let player = self .create_player_context_with_data(guild_id, info, data) .await?; @@ -237,7 +316,7 @@ pub trait DelegateMethods { fn get_player_data( &self, guild_id: impl Into + Send, - ) -> Option { + ) -> Option { self.get_player_context(guild_id) .map(|c| c.data_unwrapped()) } @@ -260,20 +339,33 @@ impl Lavalink { self.connections.get(&guild_id) } - pub fn connection_from(&self, from: &impl GetConnection) -> ConnectionRef { + #[inline] + pub fn try_get_connection( + &self, + guild_id: Id, + ) -> Result { + self.get_connection(guild_id).ok_or(UnrecognisedConnection) + } + + #[inline] + pub fn try_get_connection_mut( + &self, + guild_id: Id, + ) -> Result { + self.get_connection_mut(guild_id) + .ok_or(UnrecognisedConnection) + } + + pub fn connection_from(&self, cx: &impl GetConnection) -> ConnectionRef { // SAFETY: because the caller has an instance of `InVoice`, // this proves that there is a voice connection currently. - unsafe { self.connections.get(&from.guild_id()).unwrap_unchecked() } + unsafe { self.connections.get(&cx.guild_id()).unwrap_unchecked() } } - pub fn connection_mut_from(&self, from: &impl GetConnection) -> ConnectionRefMut { + pub fn connection_mut_from(&self, cx: &impl GetConnection) -> ConnectionRefMut { // SAFETY: because the caller has an instance of `InVoice`, // this proves that there is a voice connection currently. - unsafe { - self.connections - .get_mut(&from.guild_id()) - .unwrap_unchecked() - } + unsafe { self.connections.get_mut(&cx.guild_id()).unwrap_unchecked() } } pub fn get_connection_mut(&self, guild_id: Id) -> Option { @@ -337,17 +429,27 @@ impl DelegateMethods for LavalinkClient { } } -pub trait UnwrappedPlayerData { - fn data_unwrapped(&self) -> PlayerDataRwLockArc; +pub trait UnwrappedData { + type Data; + fn data_unwrapped(&self) -> Self::Data; } -impl UnwrappedPlayerData for PlayerContext { - fn data_unwrapped(&self) -> PlayerDataRwLockArc { +impl UnwrappedData for PlayerContext { + type Data = OwnedPlayerData; + fn data_unwrapped(&self) -> Self::Data { // SAFETY: Player data exists of type `Arc>` unsafe { self.data().unwrap_unchecked() } } } +impl UnwrappedData for LavalinkClient { + type Data = OwnedClientData; + fn data_unwrapped(&self) -> Self::Data { + // SAFETY: Lavalink data exists of type `Arc` + unsafe { self.data().unwrap_unchecked() } + } +} + pub trait UnwrappedPlayerInfoUri { fn into_uri_unwrapped(self) -> String; fn uri_unwrapped(&self) -> &str; @@ -370,3 +472,24 @@ pub trait GetConnection: GuildIdAware {} impl GetConnection for InVoice<'_> {} impl GetConnection for PartialInVoice {} + +pub struct ClientData { + http: Arc, + db: Pool, +} + +impl HttpAware for ClientData { + fn http(&self) -> &Client { + &self.http + } +} + +impl ClientData { + pub const fn new(http: Arc, db: Pool) -> Self { + Self { http, db } + } + + pub const fn db(&self) -> &Pool { + &self.db + } +} diff --git a/lyra/src/lavalink/model/connection.rs b/lyra/src/lavalink/model/connection.rs index f95bc43..2ebdb30 100644 --- a/lyra/src/lavalink/model/connection.rs +++ b/lyra/src/lavalink/model/connection.rs @@ -34,12 +34,10 @@ impl Connection { } pub async fn changed(&self) -> bool { - tokio::time::timeout( - *r#const::connection::connection_changed_timeout(), - self.change.notified(), - ) - .await - .is_ok() + tracing::trace!("waiting for connection change notification"); + let duration = r#const::connection::CHANGED_TIMEOUT; + let future = self.change.notified(); + tokio::time::timeout(duration, future).await.is_ok() } pub const fn poll(&self) -> Option<&Poll> { @@ -95,12 +93,12 @@ pub enum Event { pub type EventRecvResult = Result; -#[allow(clippy::needless_pass_by_ref_mut)] +#[allow(clippy::needless_pass_by_ref_mut)] // false positive pub async fn wait_for_with( rx: &mut broadcast::Receiver, predicate: impl Fn(&Event) -> bool + Send + Sync, ) -> EventRecvResult> { - let event = tokio::time::timeout(*r#const::misc::wait_for_bot_events_timeout(), async { + let event = tokio::time::timeout(r#const::misc::WAIT_FOR_BOT_EVENTS_TIMEOUT, async { loop { let event = rx.recv().await?; if predicate(&event) { diff --git a/lyra/src/lavalink/model/pitch.rs b/lyra/src/lavalink/model/pitch.rs index cb1a38d..223855e 100644 --- a/lyra/src/lavalink/model/pitch.rs +++ b/lyra/src/lavalink/model/pitch.rs @@ -5,7 +5,7 @@ const TWELFTH_ROOT_OF_TWO: f64 = 1.059_463_094_359_295_3; #[derive(Clone)] pub struct Pitch { multiplier: f64, - half_tone_shifts: i32, + half_tone_shifts: i64, } impl Pitch { @@ -30,7 +30,9 @@ impl Pitch { #[inline] pub fn get(&self) -> f64 { - self.multiplier * TWELFTH_ROOT_OF_TWO.powi(self.half_tone_shifts) + #[allow(clippy::cast_possible_truncation)] + let half_ton_shifts_i32 = self.half_tone_shifts as i32; + self.multiplier * TWELFTH_ROOT_OF_TWO.powi(half_ton_shifts_i32) } #[inline] @@ -42,7 +44,7 @@ impl Pitch { } pub fn shift(&mut self, half_tones: NonZeroI64) { - self.half_tone_shifts += half_tones.get() as i32; + self.half_tone_shifts += half_tones.get(); } #[inline] diff --git a/lyra/src/lavalink/model/queue.rs b/lyra/src/lavalink/model/queue.rs index c9e728c..ff1dd35 100644 --- a/lyra/src/lavalink/model/queue.rs +++ b/lyra/src/lavalink/model/queue.rs @@ -1,17 +1,11 @@ -use std::{ - collections::VecDeque, - num::NonZeroUsize, - sync::atomic::{AtomicBool, Ordering}, -}; - -use futures::Future; -use lavalink_rs::{model::track::TrackData, player_context::PlayerContext}; +use std::{collections::VecDeque, num::NonZeroUsize}; + +use lavalink_rs::model::track::TrackData; use rayon::iter::{IntoParallelIterator, ParallelExtend, ParallelIterator}; +use tokio::sync::Notify; use twilight_model::id::{marker::UserMarker, Id}; -use crate::error::component::queue::remove::WithAdvanceLockAndStoppedError; - -use super::queue_indexer::{IndexerType, QueueIndexer}; +use super::queue_indexer::{Indexer, IndexerType}; #[derive(Hash, Copy, Clone)] pub enum RepeatMode { @@ -31,7 +25,7 @@ impl RepeatMode { pub const fn emoji(&self) -> &str { match self { - Self::Off => "**` 🡲 `**", + Self::Off => "➡️", Self::All => "🔁", Self::Track => "🔂", } @@ -66,11 +60,11 @@ impl Item { self.requester } - pub const fn track(&self) -> &TrackData { + pub const fn data(&self) -> &TrackData { &self.track } - pub fn into_track(self) -> TrackData { + pub fn into_data(self) -> TrackData { self.track } } @@ -78,55 +72,45 @@ impl Item { pub struct Queue { inner: VecDeque, index: usize, - indexer: QueueIndexer, + indexer: Indexer, repeat_mode: RepeatMode, - advance_lock: AtomicBool, - current_track_started: u64, + advance_lock: Notify, } impl Queue { pub(super) const fn new() -> Self { Self { inner: VecDeque::new(), - indexer: QueueIndexer::Standard, + indexer: Indexer::Standard, index: 0, repeat_mode: RepeatMode::Off, - advance_lock: AtomicBool::new(false), - current_track_started: 0, + advance_lock: Notify::const_new(), } } - pub fn position(&self) -> NonZeroUsize { - let d = usize::from(self.current().is_some() || self.index == 0); + fn position_from(&self, current: Option<&Item>) -> NonZeroUsize { + let d = usize::from(current.is_some() || self.index == 0); // SAFETY: `self.index + d` is non-zero unsafe { NonZeroUsize::new_unchecked(self.index + d) } } - pub const fn index(&self) -> &usize { - &self.index - } - - pub fn index_mut(&mut self) -> &mut usize { - &mut self.index - } - - pub fn advance_locked(&self) -> bool { - self.advance_lock.load(Ordering::SeqCst) + pub fn position(&self) -> NonZeroUsize { + self.position_from(self.current()) } - pub fn advance_lock(&self) { - self.advance_lock.store(true, Ordering::SeqCst); + pub const fn index(&self) -> usize { + self.index } - pub fn advance_unlock(&self) { - self.advance_lock.store(false, Ordering::Relaxed); + pub fn index_mut(&mut self) -> &mut usize { + &mut self.index } pub fn current_index(&self) -> Option { match self.indexer { - QueueIndexer::Standard => Some(self.index), - QueueIndexer::Fair(ref indexer) => indexer.current(self.index), - QueueIndexer::Shuffled(ref indexer) => indexer.current(self.index), + Indexer::Standard => Some(self.index), + Indexer::Fair(ref indexer) => indexer.current(self.index), + Indexer::Shuffled(ref indexer) => indexer.current(self.index), } } @@ -134,16 +118,17 @@ impl Queue { self.inner.get(self.current_index()?) } - pub fn current_and_index(&self) -> Option<(&Item, usize)> { - self.current_index() - .and_then(|i| Some((self.inner.get(i)?, i))) + pub fn current_and_position(&self) -> (Option<&Item>, NonZeroUsize) { + let current = self.current(); + let position = self.position_from(current); + (current, position) } pub fn enqueue(&mut self, tracks: Vec, requester: Id) { match self.indexer { - QueueIndexer::Fair(ref mut indexer) => indexer.enqueue(tracks.len(), requester), - QueueIndexer::Shuffled(ref mut indexer) => indexer.enqueue(tracks.len(), self.index), - QueueIndexer::Standard => {} + Indexer::Fair(ref mut indexer) => indexer.enqueue(tracks.len(), requester), + Indexer::Shuffled(ref mut indexer) => indexer.enqueue(tracks.len(), self.index), + Indexer::Standard => {} } let queues = tracks.into_par_iter().map(|t| Item::new(t, requester)); self.inner.par_extend(queues); @@ -190,7 +175,7 @@ impl Queue { self.repeat_mode = mode; } - pub fn adjust_repeat_mode(&mut self) { + pub fn downgrade_repeat_mode(&mut self) { if let RepeatMode::All | RepeatMode::Track = self.repeat_mode { self.repeat_mode = if self.len() > 1 { RepeatMode::All @@ -207,16 +192,16 @@ impl Queue { pub fn set_indexer_type(&mut self, kind: IndexerType) { match (self.indexer.kind(), kind) { (IndexerType::Fair | IndexerType::Shuffled, IndexerType::Standard) => { - self.indexer = QueueIndexer::Standard; + self.indexer = Indexer::Standard; } (IndexerType::Standard | IndexerType::Shuffled, IndexerType::Fair) => { - self.indexer = QueueIndexer::Fair(super::queue_indexer::FairIndexer::new( + self.indexer = Indexer::Fair(super::queue_indexer::FairIndexer::new( self.inner.iter(), self.index, )); } (IndexerType::Standard | IndexerType::Fair, IndexerType::Shuffled) => { - self.indexer = QueueIndexer::Shuffled(super::queue_indexer::ShuffledIndexer::new( + self.indexer = Indexer::Shuffled(super::queue_indexer::ShuffledIndexer::new( self.len(), self.index, )); @@ -237,30 +222,37 @@ impl Queue { } } - pub async fn stop_with_advance_lock( - &self, - player: &PlayerContext, - ) -> Result<(), WithAdvanceLockAndStoppedError> { - self.with_advance_lock_and_stopped(player, |_| async { Ok(()) }) - .await + pub fn recede(&mut self) { + match self.repeat_mode { + RepeatMode::Off => { + self.index = self.index.saturating_sub(1); + } + RepeatMode::All => { + let len = self.len(); + self.index = ((self.index + len).saturating_sub(1)) % len; + } + RepeatMode::Track => {} + } } - pub async fn with_advance_lock_and_stopped<'a, 'b, F>( - &self, - player: &'a PlayerContext, - f: impl FnOnce(&'b PlayerContext) -> F + Send, - ) -> Result<(), WithAdvanceLockAndStoppedError> - where - F: Future> + Send, - 'a: 'b, - { - self.advance_lock(); + pub fn acquire_advance_lock(&self) { + tracing::trace!("acquired queue advance lock"); + self.advance_lock.notify_one(); + } - player.stop_now().await?; - f(player).await?; - Ok(()) + pub async fn not_advance_locked(&self) -> bool { + let future = self.advance_lock.notified(); + let duration = crate::core::r#const::misc::QUEUE_ADVANCE_LOCKED_TIMEOUT; + tokio::time::timeout(duration, future).await.is_err() } + pub fn iter_positions_and_items( + &self, + ) -> impl DoubleEndedIterator + Clone { + self.iter() + .enumerate() + .filter_map(|(i, t)| NonZeroUsize::new(i + 1).map(|i| (i, t))) + } #[inline] pub fn is_empty(&self) -> bool { self.inner.is_empty() @@ -294,3 +286,11 @@ impl std::ops::Index for Queue { &self.inner[index] } } + +impl std::ops::Index for Queue { + type Output = Item; + + fn index(&self, index: NonZeroUsize) -> &Self::Output { + &self.inner[index.get() - 1] + } +} diff --git a/lyra/src/lavalink/model/queue_indexer.rs b/lyra/src/lavalink/model/queue_indexer.rs index 58e90a1..1585897 100644 --- a/lyra/src/lavalink/model/queue_indexer.rs +++ b/lyra/src/lavalink/model/queue_indexer.rs @@ -11,13 +11,13 @@ pub enum IndexerType { Shuffled, } -pub(super) enum QueueIndexer { +pub(super) enum Indexer { Standard, Fair(FairIndexer), Shuffled(ShuffledIndexer), } -impl QueueIndexer { +impl Indexer { pub(super) const fn kind(&self) -> IndexerType { match self { Self::Standard => IndexerType::Standard, diff --git a/lyra/src/lavalink/track.rs b/lyra/src/lavalink/track.rs index 6e1a3a6..3317361 100644 --- a/lyra/src/lavalink/track.rs +++ b/lyra/src/lavalink/track.rs @@ -3,27 +3,55 @@ use lavalink_rs::{ hook, model::events::{TrackEnd, TrackException, TrackStart, TrackStuck}, }; +use lyra_ext::num::u64_to_i64_truncating; use crate::{ + core::model::HttpAware, error::lavalink::ProcessResult, - lavalink::{model::CorrectTrackInfo, UnwrappedPlayerData}, + lavalink::{model::CorrectTrackInfo, UnwrappedData}, }; -// FIXME: don't debug `LavalinkClient` until `lavalink_rs` stops stack overflowing - #[tracing::instrument(err, skip_all, name = "track_start")] -async fn impl_start(event: &TrackStart) -> ProcessResult { +async fn impl_start(lavalink: LavalinkClient, _: String, event: &TrackStart) -> ProcessResult { + let guild_id = event.guild_id; tracing::debug!( "guild {} started {:?}", event.guild_id.0, event.track.info.checked_title() ); + let Some(player) = lavalink.get_player_context(guild_id) else { + tracing::warn!(?guild_id, "track started without player"); + + return Ok(()); + }; + player + .data_unwrapped() + .write() + .await + .reset_track_timestamp(); + + let Some(track) = player.data_unwrapped().read().await.queue().current() else { + return Ok(()); + }; + + let rec = sqlx::query!( + "SELECT now_playing FROM guild_configs WHERE id = $1;", + u64_to_i64_truncating(guild_id.0) + ) + .fetch_one(lavalink.data_unwrapped().db()) + .await?; + + if !rec.now_playing { + return Ok(()); + } + Ok(()) } +#[allow(clippy::significant_drop_tightening)] #[tracing::instrument(err, skip_all, name = "track_end")] -async fn impl_end(lavalink: LavalinkClient, event: &TrackEnd) -> ProcessResult { +async fn impl_end(lavalink: LavalinkClient, _: String, event: &TrackEnd) -> ProcessResult { let guild_id = event.guild_id; tracing::debug!( "guild {} ended {:?}", @@ -38,51 +66,99 @@ async fn impl_end(lavalink: LavalinkClient, event: &TrackEnd) -> ProcessResult { }; let data = player.data_unwrapped(); let data_r = data.read().await; - let queue = data_r.queue(); // TODO: handle now playing message deleting - if queue.advance_locked() { - queue.advance_unlock(); - } else { + if data_r.queue().not_advance_locked().await { + tracing::trace!(?guild_id, "track ended normally"); + drop(data_r); + let mut data_w = data.write().await; let queue = data_w.queue_mut(); queue.advance(); if let Some(item) = queue.current() { - player.play_now(item.track()).await?; + player.play_now(item.data()).await?; } + } else { + tracing::trace!(?guild_id, "track ended forcefully"); } Ok(()) } #[tracing::instrument(err, skip_all, name = "track_exception")] -async fn impl_exception() -> ProcessResult { - Ok(()) // TODO: handle track exception +async fn impl_exception( + lavalink: LavalinkClient, + _: String, + event: &TrackException, +) -> ProcessResult { + let guild_id = event.guild_id; + tracing::error!(?event, "track exception"); + + let Some(player) = lavalink.get_player_context(guild_id) else { + return Ok(()); + }; + + let channel_id = { + let data = player.data_unwrapped(); + let data_r = data.read().await; + data_r.text_channel_id() + }; + + lavalink + .data_unwrapped() + .http() + .create_message(channel_id) + .content(&format!( + "💔**`ー`** ~~`{}`~~ `(Error playing this track)`", + event.track.info.title() + )) + .await?; + + Ok(()) } #[tracing::instrument(err, skip_all, name = "track_stuck")] -async fn impl_stuck() -> ProcessResult { - Ok(()) // TODO: handle track stuck +async fn impl_stuck(lavalink: LavalinkClient, _: String, event: &TrackStuck) -> ProcessResult { + let guild_id = event.guild_id; + tracing::warn!(?event, "track stuck"); + + let Some(player) = lavalink.get_player_context(guild_id) else { + return Ok(()); + }; + + let channel_id = player.data_unwrapped().read().await.text_channel_id(); + lavalink + .data_unwrapped() + .http() + .create_message(channel_id) + .content("🌀 Playback interrupted. Please wait or try using the bot again later.") + .await?; + + Ok(()) } #[hook] -pub(super) async fn start(_: LavalinkClient, _session_id: String, event: &TrackStart) { - let _ = impl_start(event).await; +pub(super) async fn start(lavalink: LavalinkClient, session_id: String, event: &TrackStart) { + let _ = impl_start(lavalink, session_id, event).await; } #[hook] -pub(super) async fn end(lavalink: LavalinkClient, _session_id: String, event: &TrackEnd) { - let _ = impl_end(lavalink, event).await; +pub(super) async fn end(lavalink: LavalinkClient, session_id: String, event: &TrackEnd) { + let _ = impl_end(lavalink, session_id, event).await; } #[hook] -pub(super) async fn exception(_: LavalinkClient, _session_id: String, _: &TrackException) { - let _ = impl_exception().await; +pub(super) async fn exception( + lavalink: LavalinkClient, + session_id: String, + event: &TrackException, +) { + let _ = impl_exception(lavalink, session_id, event).await; } #[hook] -pub(super) async fn stuck(_: LavalinkClient, _session_id: String, _: &TrackStuck) { - let _ = impl_stuck().await; +pub(super) async fn stuck(lavalink: LavalinkClient, session_id: String, event: &TrackStuck) { + let _ = impl_stuck(lavalink, session_id, event).await; } diff --git a/lyra/src/main.rs b/lyra/src/main.rs index a1efa26..71d29c3 100644 --- a/lyra/src/main.rs +++ b/lyra/src/main.rs @@ -7,6 +7,11 @@ mod gateway; mod lavalink; mod runner; +pub use { + error::command::Error as CommandError, + lavalink::{ClientAndGuildIdAware as LavalinkAndGuildIdAware, ClientAware as LavalinkAware}, +}; + #[tokio::main] #[tracing::instrument] async fn main() { diff --git a/lyra/src/runner.rs b/lyra/src/runner.rs index 7e6c933..f76fa11 100644 --- a/lyra/src/runner.rs +++ b/lyra/src/runner.rs @@ -15,9 +15,8 @@ use sqlx::{ }; use tokio::task::JoinHandle; use twilight_gateway::{ - error::{ReceiveMessageErrorType, StartRecommendedError}, - CloseFrame, Config as ShardConfig, ConfigBuilder, Event, EventTypeFlags, Intents, - MessageSender, Shard, StreamExt, + error::StartRecommendedError, CloseFrame, Config as ShardConfig, ConfigBuilder, Event, + EventTypeFlags, Intents, MessageSender, Shard, StreamExt, }; use twilight_http::{client::ClientBuilder, Client}; use twilight_model::{ @@ -29,7 +28,11 @@ use twilight_model::{ id::{marker::UserMarker, Id}, }; -use crate::core::r#const::metadata::banner; +use crate::{ + core::r#const::metadata::BANNER, + lavalink::{handlers, ClientData}, + LavalinkAware, +}; use super::{ core::{ @@ -38,7 +41,7 @@ use super::{ }, error::runner::{StartError, WaitForSignalError, WaitUntilShutdownError}, gateway, - lavalink::{self, DelegateMethods, LavalinkAware}, + lavalink::DelegateMethods, }; use super::{gateway::LastCachedStates, lavalink::Lavalink}; @@ -52,11 +55,12 @@ const INTENTS: Intents = Intents::GUILDS.union(Intents::GUILD_VOICE_STATES); static SHUTDOWN: AtomicBool = AtomicBool::new(false); -fn build_http_client() -> Client { +fn build_http_client() -> Arc { ClientBuilder::default() .default_allowed_mentions(AllowedMentions::default()) .token(CONFIG.token.to_owned()) .build() + .into() } fn build_shard_config() -> ShardConfig { @@ -67,7 +71,7 @@ fn build_shard_config() -> ShardConfig { UpdatePresencePayload::new( [Activity::from(MinimalActivity { kind: ActivityType::Listening, - name: "/play".into(), + name: String::from("/play"), url: None, })], false, @@ -93,7 +97,8 @@ pub async fn start() -> Result<(), StartError> { let http = build_http_client(); let user_id = http.current_user().await?.model().await?.id; - let lavalink = build_lavalink_client(user_id).await; + let data = ClientData::new(http.clone(), db.clone()); + let lavalink = build_lavalink_client(user_id, data).await; let shards = build_and_split_shards(&http).await?; let shards_len = shards.len(); @@ -107,7 +112,7 @@ pub async fn start() -> Result<(), StartError> { tasks.push(tokio::spawn(handle_gateway_events(shard, bot.clone()))); } - println!("{}", banner()); + println!("{}", *BANNER); Ok(wait_until_shutdown(senders, tasks).await?) } @@ -122,17 +127,18 @@ async fn build_and_split_shards( } #[tracing::instrument(skip_all, name = "lavalink")] -async fn build_lavalink_client(user_id: Id) -> Lavalink { - let events = lavalink::handlers(); +async fn build_lavalink_client(user_id: Id, data: ClientData) -> Lavalink { + let events = handlers(); let nodes = Vec::from([lavalink_rs::node::NodeBuilder { - hostname: (*CONFIG.lavalink_host).to_string(), - password: (*CONFIG.lavalink_pwd).to_string(), + hostname: String::from(CONFIG.lavalink_host), + password: String::from(CONFIG.lavalink_pwd), user_id: user_id.into(), ..Default::default() }]); - let client = LavalinkClient::new(events, nodes, NodeDistributionStrategy::new()).await; + let strategy = NodeDistributionStrategy::new(); + let client = LavalinkClient::new_with_data(events, nodes, strategy, data.into()).await; client.into() } @@ -142,12 +148,6 @@ async fn handle_gateway_events(mut shard: Shard, bot: Arc) { let event = match item { Ok(Event::GatewayClose(_)) if SHUTDOWN.load(Ordering::Relaxed) => break, Ok(event) => event, - Err(source) - if SHUTDOWN.load(Ordering::Relaxed) - && matches!(source.kind(), ReceiveMessageErrorType::WebSocket) => - { - break - } Err(source) => { tracing::warn!(?source, "error receiving event"); diff --git a/lyra_ext/Cargo.toml b/lyra_ext/Cargo.toml index 9d7d2f7..b339372 100644 --- a/lyra_ext/Cargo.toml +++ b/lyra_ext/Cargo.toml @@ -1,28 +1,29 @@ [package] name = "lyra_ext" -version = "0.7.1" +version = "0.8.0" edition = "2021" +[lints.rust] +unsafe_op_in_unsafe_fn = "forbid" + [lints.clippy] multiple_unsafe_ops_per_block = "forbid" undocumented_unsafe_blocks = "forbid" enum_glob_use = "forbid" unwrap_used = "forbid" +try_err = "forbid" pedantic = { level = "deny", priority = -1 } nursery = { level = "deny", priority = -1 } -module_name_repetitions = "allow" -cast_possible_truncation = "allow" -cast_sign_loss = "allow" - [dependencies] -rstest = "0.21.0" +rstest = "0.22.0" heck = "0.5.0" const-str = "0.5.7" unicode-segmentation = "1" bitflags = "2" regex = "1" rayon = "1" +mock_instant = "0.5.1" [dependencies.kmeans_colors] version = "0.6.0" @@ -30,7 +31,7 @@ features = ["palette_color"] default-features = false [dependencies.image] -version = "0.25.1" +version = "0.25.2" features = ["jpeg", "png", "gif", "tiff"] default-features = false diff --git a/lyra_ext/src/as_grapheme.rs b/lyra_ext/src/as_grapheme.rs index e50cfcc..f9f893c 100644 --- a/lyra_ext/src/as_grapheme.rs +++ b/lyra_ext/src/as_grapheme.rs @@ -10,7 +10,7 @@ pub trait AsGrapheme: UnicodeSegmentation { fn grapheme_truncate(&self, new_len: usize) -> Cow where Self: ToOwned, - ::Owned: for<'a> FromIterator<&'a str>, + Self::Owned: for<'a> FromIterator<&'a str>, { (self.grapheme_len() <= new_len) .then_some(Cow::Borrowed(self)) @@ -21,4 +21,56 @@ pub trait AsGrapheme: UnicodeSegmentation { impl AsGrapheme for str {} #[cfg(test)] -mod test {} +mod test { + use rstest::rstest; + + use crate::as_grapheme::AsGrapheme; + + #[rstest] + #[case("", 0)] + #[case("1", 1)] + #[case("❤️‍🔥", 1)] + #[case("❤️🔥", 2)] + #[case("🇹🇭", 1)] + #[case("🇹+🇭", 3)] + #[case("🏳️‍⚧️ she/her", 9)] + fn grapheme_len(#[case] input: &str, #[case] expected: usize) { + assert_eq!(input.grapheme_len(), expected); + } + + #[rstest] + #[case("", "")] + #[case("?", "")] + #[case("🍄‍🟫", "")] + #[case("🍄🟫", "")] + #[case("🇬🇧", "")] + #[case("🇬+🇧", "")] + #[case("🙂‍↔️ Nope!", "")] + fn grapheme_truncate_0(#[case] input: &str, #[case] expected: &str) { + assert_eq!(input.grapheme_truncate(0), expected); + } + + #[rstest] + #[case("", "")] + #[case("!", "!")] + #[case("🍋‍🟩", "🍋‍🟩")] + #[case("🍋🟩", "🍋")] + #[case("🇺🇸", "🇺🇸")] + #[case("🇺+🇸", "🇺")] + #[case("🙂‍↕️ Yep!", "🙂‍↕️")] + fn grapheme_truncate_1(#[case] input: &str, #[case] expected: &str) { + assert_eq!(input.grapheme_truncate(1), expected); + } + + #[rstest] + #[case("", "")] + #[case("#", "#")] + #[case("🐦‍🔥", "🐦‍🔥")] + #[case("🐦🔥", "🐦🔥")] + #[case("🇯🇵", "🇯🇵")] + #[case("🇯+🇵", "🇯+")] + #[case("🧚🏻‍♀️I'm an angel!", "🧚🏻‍♀️I")] + fn grapheme_truncate_2(#[case] input: &str, #[case] expected: &str) { + assert_eq!(input.grapheme_truncate(2), expected); + } +} diff --git a/lyra_ext/src/image/limit_file_size.rs b/lyra_ext/src/image/limit_file_size.rs index 46f22b9..d0bf179 100644 --- a/lyra_ext/src/image/limit_file_size.rs +++ b/lyra_ext/src/image/limit_file_size.rs @@ -1,37 +1,20 @@ -use image::{imageops::FilterType, DynamicImage, GenericImageView}; - -pub struct LimitImageFileSizeResponse { - image: DynamicImage, - kind: LimitImageFileSizeResponseKind, -} +use std::borrow::Cow; -impl LimitImageFileSizeResponse { - const fn new(image: DynamicImage, kind: LimitImageFileSizeResponseKind) -> Self { - Self { image, kind } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum LimitImageFileSizeResponseKind { - Unchanged, - Resized, -} +use image::{imageops::FilterType, DynamicImage, GenericImageView}; pub trait LimitFileSize { - fn limit_file_size(self, limit: u32) -> LimitImageFileSizeResponse; + fn limit_file_size(&self, limit: u32) -> Cow; } +#[allow(clippy::similar_names)] impl LimitFileSize for DynamicImage { - fn limit_file_size(self, limit: u32) -> LimitImageFileSizeResponse { + fn limit_file_size(&self, limit: u32) -> Cow { let (x, y) = self.dimensions(); - let bytes_per_pixel = u32::from(self.color().bytes_per_pixel()); + let bytes_per_pixel = self.color().bytes_per_pixel(); - let max_image_size = x * y * bytes_per_pixel; + let max_image_size = x * y * u32::from(bytes_per_pixel); if max_image_size <= limit { - return LimitImageFileSizeResponse::new( - self, - LimitImageFileSizeResponseKind::Unchanged, - ); + return Cow::Borrowed(self); } let x_to_y = f64::from(x) / f64::from(y); @@ -39,58 +22,60 @@ impl LimitFileSize for DynamicImage { let new_y = (f64::from(limit) / (f64::from(bytes_per_pixel) * x_to_y)).sqrt(); let new_x = new_y * x_to_y; - let image = self.resize(new_x as u32, new_y as u32, FilterType::Lanczos3); - LimitImageFileSizeResponse::new(image, LimitImageFileSizeResponseKind::Resized) + #[allow(clippy::cast_possible_truncation)] + let (new_x_u32, new_y_u32) = ((new_x as i32).unsigned_abs(), (new_y as i32).unsigned_abs()); + + let image = self.resize(new_x_u32, new_y_u32, FilterType::Lanczos3); + Cow::Owned(image) } } #[cfg(test)] mod test { + use std::borrow::Cow; + use const_str::concat as const_str_concat; use rstest::rstest; - use crate::image::limit_file_size::{LimitFileSize, LimitImageFileSizeResponseKind}; + use crate::image::limit_file_size::LimitFileSize; const TEST_RESOURCES_PATH: &str = "src/image/test"; #[rstest] #[case( const_str_concat!(TEST_RESOURCES_PATH, "/limit_file_size_1.png"), - 2_u32.pow(20), - LimitImageFileSizeResponseKind::Resized - )] - #[case( - const_str_concat!(TEST_RESOURCES_PATH, "/limit_file_size_2.png"), - 2_u32.pow(20), - LimitImageFileSizeResponseKind::Resized + 10 * 2_u32.pow(20), )] #[case( const_str_concat!(TEST_RESOURCES_PATH, "/limit_file_size_1.png"), - 10 * 2_u32.pow(20), - LimitImageFileSizeResponseKind::Unchanged + 50 * 2_u32.pow(20), )] #[case( const_str_concat!(TEST_RESOURCES_PATH, "/limit_file_size_2.png"), - 10 * 2_u32.pow(20), - LimitImageFileSizeResponseKind::Resized + 50 * 2_u32.pow(20), )] + fn limit_file_size_borrowed(#[case] input_path: &str, #[case] input_limit: u32) { + let image = image::open(input_path).unwrap_or_else(|e| panic!("{e:#?}")); + let response = image.limit_file_size(input_limit); + assert!(matches!(response, Cow::Borrowed(_))); + } + + #[rstest] #[case( const_str_concat!(TEST_RESOURCES_PATH, "/limit_file_size_1.png"), - 50 * 2_u32.pow(20), - LimitImageFileSizeResponseKind::Unchanged + 2_u32.pow(20), )] #[case( const_str_concat!(TEST_RESOURCES_PATH, "/limit_file_size_2.png"), - 50 * 2_u32.pow(20), - LimitImageFileSizeResponseKind::Unchanged + 2_u32.pow(20), + )] + #[case( + const_str_concat!(TEST_RESOURCES_PATH, "/limit_file_size_2.png"), + 10 * 2_u32.pow(20), )] - fn limit_file_size( - #[case] input_path: &str, - #[case] input_limit: u32, - #[case] expected_response_kind: LimitImageFileSizeResponseKind, - ) { + fn limit_file_size_owned(#[case] input_path: &str, #[case] input_limit: u32) { let image = image::open(input_path).unwrap_or_else(|e| panic!("{e:#?}")); let response = image.limit_file_size(input_limit); - assert_eq!(response.kind, expected_response_kind); + assert!(matches!(response, Cow::Owned(_))); } } diff --git a/lyra_ext/src/iter/chunked_range.rs b/lyra_ext/src/iter/chunked_range.rs index e0ba1bd..3170112 100644 --- a/lyra_ext/src/iter/chunked_range.rs +++ b/lyra_ext/src/iter/chunked_range.rs @@ -22,224 +22,224 @@ mod test { #[rstest] #[case(0, [], [])] #[case(1, [], [])] - fn chunked_range_0( + fn chunked_range_0( #[case] input_start: usize, - #[case] input_chunk_sizes: [usize; N], - #[case] expected: [Vec; N], + #[case] input_chunk_sizes: [usize; 0], + #[case] expected: [[usize; 0]; 0], ) { assert_eq!( chunked_range(input_start, input_chunk_sizes) - .map(Iterator::collect::>) + .map(Vec::from_iter) .collect::>(), expected ); } #[rstest] - #[case(0, [0], [vec![]])] - #[case(1, [0], [vec![]])] - #[case(0, [1], [vec![0]])] - #[case(1, [1], [vec![1]])] - #[case(0, [2], [vec![0, 1]])] - #[case(1, [2], [vec![1, 2]])] - #[case(0, [3], [vec![0, 1, 2]])] - #[case(1, [3], [vec![1, 2, 3]])] - fn chunked_range_1( + #[case(0, [0], [[ ]])] + #[case(1, [0], [[ ]])] + #[case(0, [1], [[0 ]])] + #[case(1, [1], [[1 ]])] + #[case(0, [2], [[0, 1 ]])] + #[case(1, [2], [[1, 2 ]])] + #[case(0, [3], [[0, 1, 2]])] + #[case(1, [3], [[1, 2, 3]])] + fn chunked_range_1( #[case] input_start: usize, - #[case] input_chunk_sizes: [usize; N], - #[case] expected: [Vec; N], + #[case] input_chunk_sizes: [usize; 1], + #[case] expected: [[usize; M]; 1], ) { assert_eq!( chunked_range(input_start, input_chunk_sizes) - .map(Iterator::collect::>) + .map(Vec::from_iter) .collect::>(), expected ); } #[rstest] - #[case(0, [0, 0], [vec![], vec![]])] - #[case(1, [0, 0], [vec![], vec![]])] - #[case(0, [0, 1], [vec![], vec![0]])] - #[case(1, [0, 1], [vec![], vec![1]])] - #[case(0, [0, 2], [vec![], vec![0, 1]])] - #[case(1, [0, 2], [vec![], vec![1, 2]])] - #[case(0, [0, 3], [vec![], vec![0, 1, 2]])] - #[case(1, [0, 3], [vec![], vec![1, 2, 3]])] - #[case(0, [1, 0], [vec![0], vec![]])] - #[case(1, [1, 0], [vec![1], vec![]])] - #[case(0, [1, 1], [vec![0], vec![1]])] - #[case(1, [1, 1], [vec![1], vec![2]])] - #[case(0, [1, 2], [vec![0], vec![1, 2]])] - #[case(1, [1, 2], [vec![1], vec![2, 3]])] - #[case(0, [1, 3], [vec![0], vec![1, 2, 3]])] - #[case(1, [1, 3], [vec![1], vec![2, 3, 4]])] - #[case(0, [2, 0], [vec![0, 1], vec![]])] - #[case(1, [2, 0], [vec![1, 2], vec![]])] - #[case(0, [2, 1], [vec![0, 1], vec![2]])] - #[case(1, [2, 1], [vec![1, 2], vec![3]])] - #[case(0, [2, 2], [vec![0, 1], vec![2, 3]])] - #[case(1, [2, 2], [vec![1, 2], vec![3, 4]])] - #[case(0, [2, 3], [vec![0, 1], vec![2, 3, 4]])] - #[case(1, [2, 3], [vec![1, 2], vec![3, 4, 5]])] - #[case(0, [3, 0], [vec![0, 1, 2], vec![]])] - #[case(1, [3, 0], [vec![1, 2, 3], vec![]])] - #[case(0, [3, 1], [vec![0, 1, 2], vec![3]])] - #[case(1, [3, 1], [vec![1, 2, 3], vec![4]])] - #[case(0, [3, 2], [vec![0, 1, 2], vec![3, 4]])] - #[case(1, [3, 2], [vec![1, 2, 3], vec![4, 5]])] + #[case(0, [0, 0], [vec![ ], vec![ ]])] + #[case(1, [0, 0], [vec![ ], vec![ ]])] + #[case(0, [0, 1], [vec![ ], vec![0 ]])] + #[case(1, [0, 1], [vec![ ], vec![1 ]])] + #[case(0, [0, 2], [vec![ ], vec![0, 1 ]])] + #[case(1, [0, 2], [vec![ ], vec![1, 2 ]])] + #[case(0, [0, 3], [vec![ ], vec![0, 1, 2]])] + #[case(1, [0, 3], [vec![ ], vec![1, 2, 3]])] + #[case(0, [1, 0], [vec![0 ], vec![ ]])] + #[case(1, [1, 0], [vec![1 ], vec![ ]])] + #[case(0, [1, 1], [vec![0 ], vec![1 ]])] + #[case(1, [1, 1], [vec![1 ], vec![2 ]])] + #[case(0, [1, 2], [vec![0 ], vec![1, 2 ]])] + #[case(1, [1, 2], [vec![1 ], vec![2, 3 ]])] + #[case(0, [1, 3], [vec![0 ], vec![1, 2, 3]])] + #[case(1, [1, 3], [vec![1 ], vec![2, 3, 4]])] + #[case(0, [2, 0], [vec![0, 1 ], vec![ ]])] + #[case(1, [2, 0], [vec![1, 2 ], vec![ ]])] + #[case(0, [2, 1], [vec![0, 1 ], vec![2 ]])] + #[case(1, [2, 1], [vec![1, 2 ], vec![3 ]])] + #[case(0, [2, 2], [vec![0, 1 ], vec![2, 3 ]])] + #[case(1, [2, 2], [vec![1, 2 ], vec![3, 4 ]])] + #[case(0, [2, 3], [vec![0, 1 ], vec![2, 3, 4]])] + #[case(1, [2, 3], [vec![1, 2 ], vec![3, 4, 5]])] + #[case(0, [3, 0], [vec![0, 1, 2], vec![ ]])] + #[case(1, [3, 0], [vec![1, 2, 3], vec![ ]])] + #[case(0, [3, 1], [vec![0, 1, 2], vec![3 ]])] + #[case(1, [3, 1], [vec![1, 2, 3], vec![4 ]])] + #[case(0, [3, 2], [vec![0, 1, 2], vec![3, 4 ]])] + #[case(1, [3, 2], [vec![1, 2, 3], vec![4, 5 ]])] #[case(0, [3, 3], [vec![0, 1, 2], vec![3, 4, 5]])] #[case(1, [3, 3], [vec![1, 2, 3], vec![4, 5, 6]])] - fn chunked_range_2( + fn chunked_range_2( #[case] input_start: usize, - #[case] input_chunk_sizes: [usize; N], - #[case] expected: [Vec; N], + #[case] input_chunk_sizes: [usize; 2], + #[case] expected: [Vec; 2], ) { assert_eq!( chunked_range(input_start, input_chunk_sizes) - .map(Iterator::collect::>) + .map(Vec::from_iter) .collect::>(), expected ); } #[rstest] - #[case(0, [0, 0, 0], [vec![], vec![], vec![]])] - #[case(1, [0, 0, 0], [vec![], vec![], vec![]])] - #[case(0, [0, 1, 0], [vec![], vec![0], vec![]])] - #[case(1, [0, 1, 0], [vec![], vec![1], vec![]])] - #[case(0, [0, 2, 0], [vec![], vec![0, 1], vec![]])] - #[case(1, [0, 2, 0], [vec![], vec![1, 2], vec![]])] - #[case(0, [0, 3, 0], [vec![], vec![0, 1, 2], vec![]])] - #[case(1, [0, 3, 0], [vec![], vec![1, 2, 3], vec![]])] - #[case(0, [1, 0, 0], [vec![0], vec![], vec![]])] - #[case(1, [1, 0, 0], [vec![1], vec![], vec![]])] - #[case(0, [1, 1, 0], [vec![0], vec![1], vec![]])] - #[case(1, [1, 1, 0], [vec![1], vec![2], vec![]])] - #[case(0, [1, 2, 0], [vec![0], vec![1, 2], vec![]])] - #[case(1, [1, 2, 0], [vec![1], vec![2, 3], vec![]])] - #[case(0, [1, 3, 0], [vec![0], vec![1, 2, 3], vec![]])] - #[case(1, [1, 3, 0], [vec![1], vec![2, 3, 4], vec![]])] - #[case(0, [2, 0, 0], [vec![0, 1], vec![], vec![]])] - #[case(1, [2, 0, 0], [vec![1, 2], vec![], vec![]])] - #[case(0, [2, 1, 0], [vec![0, 1], vec![2], vec![]])] - #[case(1, [2, 1, 0], [vec![1, 2], vec![3], vec![]])] - #[case(0, [2, 2, 0], [vec![0, 1], vec![2, 3], vec![]])] - #[case(1, [2, 2, 0], [vec![1, 2], vec![3, 4], vec![]])] - #[case(0, [2, 3, 0], [vec![0, 1], vec![2, 3, 4], vec![]])] - #[case(1, [2, 3, 0], [vec![1, 2], vec![3, 4, 5], vec![]])] - #[case(0, [3, 0, 0], [vec![0, 1, 2], vec![], vec![]])] - #[case(1, [3, 0, 0], [vec![1, 2, 3], vec![], vec![]])] - #[case(0, [3, 1, 0], [vec![0, 1, 2], vec![3], vec![]])] - #[case(1, [3, 1, 0], [vec![1, 2, 3], vec![4], vec![]])] - #[case(0, [3, 2, 0], [vec![0, 1, 2], vec![3, 4], vec![]])] - #[case(1, [3, 2, 0], [vec![1, 2, 3], vec![4, 5], vec![]])] - #[case(0, [3, 3, 0], [vec![0, 1, 2], vec![3, 4, 5], vec![]])] - #[case(1, [3, 3, 0], [vec![1, 2, 3], vec![4, 5, 6], vec![]])] - #[case(0, [0, 0, 1], [vec![], vec![], vec![0]])] - #[case(1, [0, 0, 1], [vec![], vec![], vec![1]])] - #[case(0, [0, 1, 1], [vec![], vec![0], vec![1]])] - #[case(1, [0, 1, 1], [vec![], vec![1], vec![2]])] - #[case(0, [0, 2, 1], [vec![], vec![0, 1], vec![2]])] - #[case(1, [0, 2, 1], [vec![], vec![1, 2], vec![3]])] - #[case(0, [0, 3, 1], [vec![], vec![0, 1, 2], vec![3]])] - #[case(1, [0, 3, 1], [vec![], vec![1, 2, 3], vec![4]])] - #[case(0, [1, 0, 1], [vec![0], vec![], vec![1]])] - #[case(1, [1, 0, 1], [vec![1], vec![], vec![2]])] - #[case(0, [1, 1, 1], [vec![0], vec![1], vec![2]])] - #[case(1, [1, 1, 1], [vec![1], vec![2], vec![3]])] - #[case(0, [1, 2, 1], [vec![0], vec![1, 2], vec![3]])] - #[case(1, [1, 2, 1], [vec![1], vec![2, 3], vec![4]])] - #[case(0, [1, 3, 1], [vec![0], vec![1, 2, 3], vec![4]])] - #[case(1, [1, 3, 1], [vec![1], vec![2, 3, 4], vec![5]])] - #[case(0, [2, 0, 1], [vec![0, 1], vec![], vec![2]])] - #[case(1, [2, 0, 1], [vec![1, 2], vec![], vec![3]])] - #[case(0, [2, 1, 1], [vec![0, 1], vec![2], vec![3]])] - #[case(1, [2, 1, 1], [vec![1, 2], vec![3], vec![4]])] - #[case(0, [2, 2, 1], [vec![0, 1], vec![2, 3], vec![4]])] - #[case(1, [2, 2, 1], [vec![1, 2], vec![3, 4], vec![5]])] - #[case(0, [2, 3, 1], [vec![0, 1], vec![2, 3, 4], vec![5]])] - #[case(1, [2, 3, 1], [vec![1, 2], vec![3, 4, 5], vec![6]])] - #[case(0, [3, 0, 1], [vec![0, 1, 2], vec![], vec![3]])] - #[case(1, [3, 0, 1], [vec![1, 2, 3], vec![], vec![4]])] - #[case(0, [3, 1, 1], [vec![0, 1, 2], vec![3], vec![4]])] - #[case(1, [3, 1, 1], [vec![1, 2, 3], vec![4], vec![5]])] - #[case(0, [3, 2, 1], [vec![0, 1, 2], vec![3, 4], vec![5]])] - #[case(1, [3, 2, 1], [vec![1, 2, 3], vec![4, 5], vec![6]])] - #[case(0, [3, 3, 1], [vec![0, 1, 2], vec![3, 4, 5], vec![6]])] - #[case(1, [3, 3, 1], [vec![1, 2, 3], vec![4, 5, 6], vec![7]])] - #[case(0, [0, 0, 2], [vec![], vec![], vec![0, 1]])] - #[case(1, [0, 0, 2], [vec![], vec![], vec![1, 2]])] - #[case(0, [0, 1, 2], [vec![], vec![0], vec![1, 2]])] - #[case(1, [0, 1, 2], [vec![], vec![1], vec![2, 3]])] - #[case(0, [0, 2, 2], [vec![], vec![0, 1], vec![2, 3]])] - #[case(1, [0, 2, 2], [vec![], vec![1, 2], vec![3, 4]])] - #[case(0, [0, 3, 2], [vec![], vec![0, 1, 2], vec![3, 4]])] - #[case(1, [0, 3, 2], [vec![], vec![1, 2, 3], vec![4, 5]])] - #[case(0, [1, 0, 2], [vec![0], vec![], vec![1, 2]])] - #[case(1, [1, 0, 2], [vec![1], vec![], vec![2, 3]])] - #[case(0, [1, 1, 2], [vec![0], vec![1], vec![2, 3]])] - #[case(1, [1, 1, 2], [vec![1], vec![2], vec![3, 4]])] - #[case(0, [1, 2, 2], [vec![0], vec![1, 2], vec![3, 4]])] - #[case(1, [1, 2, 2], [vec![1], vec![2, 3], vec![4, 5]])] - #[case(0, [1, 3, 2], [vec![0], vec![1, 2, 3], vec![4, 5]])] - #[case(1, [1, 3, 2], [vec![1], vec![2, 3, 4], vec![5, 6]])] - #[case(0, [2, 0, 2], [vec![0, 1], vec![], vec![2, 3]])] - #[case(1, [2, 0, 2], [vec![1, 2], vec![], vec![3, 4]])] - #[case(0, [2, 1, 2], [vec![0, 1], vec![2], vec![3, 4]])] - #[case(1, [2, 1, 2], [vec![1, 2], vec![3], vec![4, 5]])] - #[case(0, [2, 2, 2], [vec![0, 1], vec![2, 3], vec![4, 5]])] - #[case(1, [2, 2, 2], [vec![1, 2], vec![3, 4], vec![5, 6]])] - #[case(0, [2, 3, 2], [vec![0, 1], vec![2, 3, 4], vec![5, 6]])] - #[case(1, [2, 3, 2], [vec![1, 2], vec![3, 4, 5], vec![6, 7]])] - #[case(0, [3, 0, 2], [vec![0, 1, 2], vec![], vec![3, 4]])] - #[case(1, [3, 0, 2], [vec![1, 2, 3], vec![], vec![4, 5]])] - #[case(0, [3, 1, 2], [vec![0, 1, 2], vec![3], vec![4, 5]])] - #[case(1, [3, 1, 2], [vec![1, 2, 3], vec![4], vec![5, 6]])] - #[case(0, [3, 2, 2], [vec![0, 1, 2], vec![3, 4], vec![5, 6]])] - #[case(1, [3, 2, 2], [vec![1, 2, 3], vec![4, 5], vec![6, 7]])] - #[case(0, [3, 3, 2], [vec![0, 1, 2], vec![3, 4, 5], vec![6, 7]])] - #[case(1, [3, 3, 2], [vec![1, 2, 3], vec![4, 5, 6], vec![7, 8]])] - #[case(0, [0, 0, 3], [vec![], vec![], vec![0, 1, 2]])] - #[case(1, [0, 0, 3], [vec![], vec![], vec![1, 2, 3]])] - #[case(0, [0, 1, 3], [vec![], vec![0], vec![1, 2, 3]])] - #[case(1, [0, 1, 3], [vec![], vec![1], vec![2, 3, 4]])] - #[case(0, [0, 2, 3], [vec![], vec![0, 1], vec![2, 3, 4]])] - #[case(1, [0, 2, 3], [vec![], vec![1, 2], vec![3, 4, 5]])] - #[case(0, [0, 3, 3], [vec![], vec![0, 1, 2], vec![3, 4, 5]])] - #[case(1, [0, 3, 3], [vec![], vec![1, 2, 3], vec![4, 5, 6]])] - #[case(0, [1, 0, 3], [vec![0], vec![], vec![1, 2, 3]])] - #[case(1, [1, 0, 3], [vec![1], vec![], vec![2, 3, 4]])] - #[case(0, [1, 1, 3], [vec![0], vec![1], vec![2, 3, 4]])] - #[case(1, [1, 1, 3], [vec![1], vec![2], vec![3, 4, 5]])] - #[case(0, [1, 2, 3], [vec![0], vec![1, 2], vec![3, 4, 5]])] - #[case(1, [1, 2, 3], [vec![1], vec![2, 3], vec![4, 5, 6]])] - #[case(0, [1, 3, 3], [vec![0], vec![1, 2, 3], vec![4, 5, 6]])] - #[case(1, [1, 3, 3], [vec![1], vec![2, 3, 4], vec![5, 6, 7]])] - #[case(0, [2, 0, 3], [vec![0, 1], vec![], vec![2, 3, 4]])] - #[case(1, [2, 0, 3], [vec![1, 2], vec![], vec![3, 4, 5]])] - #[case(0, [2, 1, 3], [vec![0, 1], vec![2], vec![3, 4, 5]])] - #[case(1, [2, 1, 3], [vec![1, 2], vec![3], vec![4, 5, 6]])] - #[case(0, [2, 2, 3], [vec![0, 1], vec![2, 3], vec![4, 5, 6]])] - #[case(1, [2, 2, 3], [vec![1, 2], vec![3, 4], vec![5, 6, 7]])] - #[case(0, [2, 3, 3], [vec![0, 1], vec![2, 3, 4], vec![5, 6, 7]])] - #[case(1, [2, 3, 3], [vec![1, 2], vec![3, 4, 5], vec![6, 7, 8]])] - #[case(0, [3, 0, 3], [vec![0, 1, 2], vec![], vec![3, 4, 5]])] - #[case(1, [3, 0, 3], [vec![1, 2, 3], vec![], vec![4, 5, 6]])] - #[case(0, [3, 1, 3], [vec![0, 1, 2], vec![3], vec![4, 5, 6]])] - #[case(1, [3, 1, 3], [vec![1, 2, 3], vec![4], vec![5, 6, 7]])] - #[case(0, [3, 2, 3], [vec![0, 1, 2], vec![3, 4], vec![5, 6, 7]])] - #[case(1, [3, 2, 3], [vec![1, 2, 3], vec![4, 5], vec![6, 7, 8]])] + #[case(0, [0, 0, 0], [vec![ ], vec![ ], vec![ ]])] + #[case(1, [0, 0, 0], [vec![ ], vec![ ], vec![ ]])] + #[case(0, [0, 1, 0], [vec![ ], vec![0 ], vec![ ]])] + #[case(1, [0, 1, 0], [vec![ ], vec![1 ], vec![ ]])] + #[case(0, [0, 2, 0], [vec![ ], vec![0, 1 ], vec![ ]])] + #[case(1, [0, 2, 0], [vec![ ], vec![1, 2 ], vec![ ]])] + #[case(0, [0, 3, 0], [vec![ ], vec![0, 1, 2], vec![ ]])] + #[case(1, [0, 3, 0], [vec![ ], vec![1, 2, 3], vec![ ]])] + #[case(0, [1, 0, 0], [vec![0 ], vec![ ], vec![ ]])] + #[case(1, [1, 0, 0], [vec![1 ], vec![ ], vec![ ]])] + #[case(0, [1, 1, 0], [vec![0 ], vec![1 ], vec![ ]])] + #[case(1, [1, 1, 0], [vec![1 ], vec![2 ], vec![ ]])] + #[case(0, [1, 2, 0], [vec![0 ], vec![1, 2 ], vec![ ]])] + #[case(1, [1, 2, 0], [vec![1 ], vec![2, 3 ], vec![ ]])] + #[case(0, [1, 3, 0], [vec![0 ], vec![1, 2, 3], vec![ ]])] + #[case(1, [1, 3, 0], [vec![1 ], vec![2, 3, 4], vec![ ]])] + #[case(0, [2, 0, 0], [vec![0, 1 ], vec![ ], vec![ ]])] + #[case(1, [2, 0, 0], [vec![1, 2 ], vec![ ], vec![ ]])] + #[case(0, [2, 1, 0], [vec![0, 1 ], vec![2 ], vec![ ]])] + #[case(1, [2, 1, 0], [vec![1, 2 ], vec![3 ], vec![ ]])] + #[case(0, [2, 2, 0], [vec![0, 1 ], vec![2, 3 ], vec![ ]])] + #[case(1, [2, 2, 0], [vec![1, 2 ], vec![3, 4 ], vec![ ]])] + #[case(0, [2, 3, 0], [vec![0, 1 ], vec![2, 3, 4], vec![ ]])] + #[case(1, [2, 3, 0], [vec![1, 2 ], vec![3, 4, 5], vec![ ]])] + #[case(0, [3, 0, 0], [vec![0, 1, 2], vec![ ], vec![ ]])] + #[case(1, [3, 0, 0], [vec![1, 2, 3], vec![ ], vec![ ]])] + #[case(0, [3, 1, 0], [vec![0, 1, 2], vec![3 ], vec![ ]])] + #[case(1, [3, 1, 0], [vec![1, 2, 3], vec![4 ], vec![ ]])] + #[case(0, [3, 2, 0], [vec![0, 1, 2], vec![3, 4 ], vec![ ]])] + #[case(1, [3, 2, 0], [vec![1, 2, 3], vec![4, 5 ], vec![ ]])] + #[case(0, [3, 3, 0], [vec![0, 1, 2], vec![3, 4, 5], vec![ ]])] + #[case(1, [3, 3, 0], [vec![1, 2, 3], vec![4, 5, 6], vec![ ]])] + #[case(0, [0, 0, 1], [vec![ ], vec![ ], vec![0 ]])] + #[case(1, [0, 0, 1], [vec![ ], vec![ ], vec![1 ]])] + #[case(0, [0, 1, 1], [vec![ ], vec![0 ], vec![1 ]])] + #[case(1, [0, 1, 1], [vec![ ], vec![1 ], vec![2 ]])] + #[case(0, [0, 2, 1], [vec![ ], vec![0, 1 ], vec![2 ]])] + #[case(1, [0, 2, 1], [vec![ ], vec![1, 2 ], vec![3 ]])] + #[case(0, [0, 3, 1], [vec![ ], vec![0, 1, 2], vec![3 ]])] + #[case(1, [0, 3, 1], [vec![ ], vec![1, 2, 3], vec![4 ]])] + #[case(0, [1, 0, 1], [vec![0 ], vec![ ], vec![1 ]])] + #[case(1, [1, 0, 1], [vec![1 ], vec![ ], vec![2 ]])] + #[case(0, [1, 1, 1], [vec![0 ], vec![1 ], vec![2 ]])] + #[case(1, [1, 1, 1], [vec![1 ], vec![2 ], vec![3 ]])] + #[case(0, [1, 2, 1], [vec![0 ], vec![1, 2 ], vec![3 ]])] + #[case(1, [1, 2, 1], [vec![1 ], vec![2, 3 ], vec![4 ]])] + #[case(0, [1, 3, 1], [vec![0 ], vec![1, 2, 3], vec![4 ]])] + #[case(1, [1, 3, 1], [vec![1 ], vec![2, 3, 4], vec![5 ]])] + #[case(0, [2, 0, 1], [vec![0, 1 ], vec![ ], vec![2 ]])] + #[case(1, [2, 0, 1], [vec![1, 2 ], vec![ ], vec![3 ]])] + #[case(0, [2, 1, 1], [vec![0, 1 ], vec![2 ], vec![3 ]])] + #[case(1, [2, 1, 1], [vec![1, 2 ], vec![3 ], vec![4 ]])] + #[case(0, [2, 2, 1], [vec![0, 1 ], vec![2, 3 ], vec![4 ]])] + #[case(1, [2, 2, 1], [vec![1, 2 ], vec![3, 4 ], vec![5 ]])] + #[case(0, [2, 3, 1], [vec![0, 1 ], vec![2, 3, 4], vec![5 ]])] + #[case(1, [2, 3, 1], [vec![1, 2 ], vec![3, 4, 5], vec![6 ]])] + #[case(0, [3, 0, 1], [vec![0, 1, 2], vec![ ], vec![3 ]])] + #[case(1, [3, 0, 1], [vec![1, 2, 3], vec![ ], vec![4 ]])] + #[case(0, [3, 1, 1], [vec![0, 1, 2], vec![3 ], vec![4 ]])] + #[case(1, [3, 1, 1], [vec![1, 2, 3], vec![4 ], vec![5 ]])] + #[case(0, [3, 2, 1], [vec![0, 1, 2], vec![3, 4 ], vec![5 ]])] + #[case(1, [3, 2, 1], [vec![1, 2, 3], vec![4, 5 ], vec![6 ]])] + #[case(0, [3, 3, 1], [vec![0, 1, 2], vec![3, 4, 5], vec![6 ]])] + #[case(1, [3, 3, 1], [vec![1, 2, 3], vec![4, 5, 6], vec![7 ]])] + #[case(0, [0, 0, 2], [vec![ ], vec![ ], vec![0, 1 ]])] + #[case(1, [0, 0, 2], [vec![ ], vec![ ], vec![1, 2 ]])] + #[case(0, [0, 1, 2], [vec![ ], vec![0 ], vec![1, 2 ]])] + #[case(1, [0, 1, 2], [vec![ ], vec![1 ], vec![2, 3 ]])] + #[case(0, [0, 2, 2], [vec![ ], vec![0, 1 ], vec![2, 3 ]])] + #[case(1, [0, 2, 2], [vec![ ], vec![1, 2 ], vec![3, 4 ]])] + #[case(0, [0, 3, 2], [vec![ ], vec![0, 1, 2], vec![3, 4 ]])] + #[case(1, [0, 3, 2], [vec![ ], vec![1, 2, 3], vec![4, 5 ]])] + #[case(0, [1, 0, 2], [vec![0 ], vec![ ], vec![1, 2 ]])] + #[case(1, [1, 0, 2], [vec![1 ], vec![ ], vec![2, 3 ]])] + #[case(0, [1, 1, 2], [vec![0 ], vec![1 ], vec![2, 3 ]])] + #[case(1, [1, 1, 2], [vec![1 ], vec![2 ], vec![3, 4 ]])] + #[case(0, [1, 2, 2], [vec![0 ], vec![1, 2 ], vec![3, 4 ]])] + #[case(1, [1, 2, 2], [vec![1 ], vec![2, 3 ], vec![4, 5 ]])] + #[case(0, [1, 3, 2], [vec![0 ], vec![1, 2, 3], vec![4, 5 ]])] + #[case(1, [1, 3, 2], [vec![1 ], vec![2, 3, 4], vec![5, 6 ]])] + #[case(0, [2, 0, 2], [vec![0, 1 ], vec![ ], vec![2, 3 ]])] + #[case(1, [2, 0, 2], [vec![1, 2 ], vec![ ], vec![3, 4 ]])] + #[case(0, [2, 1, 2], [vec![0, 1 ], vec![2 ], vec![3, 4 ]])] + #[case(1, [2, 1, 2], [vec![1, 2 ], vec![3 ], vec![4, 5 ]])] + #[case(0, [2, 2, 2], [vec![0, 1 ], vec![2, 3 ], vec![4, 5 ]])] + #[case(1, [2, 2, 2], [vec![1, 2 ], vec![3, 4 ], vec![5, 6 ]])] + #[case(0, [2, 3, 2], [vec![0, 1 ], vec![2, 3, 4], vec![5, 6 ]])] + #[case(1, [2, 3, 2], [vec![1, 2 ], vec![3, 4, 5], vec![6, 7 ]])] + #[case(0, [3, 0, 2], [vec![0, 1, 2], vec![ ], vec![3, 4 ]])] + #[case(1, [3, 0, 2], [vec![1, 2, 3], vec![ ], vec![4, 5 ]])] + #[case(0, [3, 1, 2], [vec![0, 1, 2], vec![3 ], vec![4, 5 ]])] + #[case(1, [3, 1, 2], [vec![1, 2, 3], vec![4 ], vec![5, 6 ]])] + #[case(0, [3, 2, 2], [vec![0, 1, 2], vec![3, 4 ], vec![5, 6 ]])] + #[case(1, [3, 2, 2], [vec![1, 2, 3], vec![4, 5 ], vec![6, 7 ]])] + #[case(0, [3, 3, 2], [vec![0, 1, 2], vec![3, 4, 5], vec![6, 7 ]])] + #[case(1, [3, 3, 2], [vec![1, 2, 3], vec![4, 5, 6], vec![7, 8 ]])] + #[case(0, [0, 0, 3], [vec![ ], vec![ ], vec![0, 1, 2]])] + #[case(1, [0, 0, 3], [vec![ ], vec![ ], vec![1, 2, 3]])] + #[case(0, [0, 1, 3], [vec![ ], vec![0 ], vec![1, 2, 3]])] + #[case(1, [0, 1, 3], [vec![ ], vec![1 ], vec![2, 3, 4]])] + #[case(0, [0, 2, 3], [vec![ ], vec![0, 1 ], vec![2, 3, 4]])] + #[case(1, [0, 2, 3], [vec![ ], vec![1, 2 ], vec![3, 4, 5]])] + #[case(0, [0, 3, 3], [vec![ ], vec![0, 1, 2], vec![3, 4, 5]])] + #[case(1, [0, 3, 3], [vec![ ], vec![1, 2, 3], vec![4, 5, 6]])] + #[case(0, [1, 0, 3], [vec![0 ], vec![ ], vec![1, 2, 3]])] + #[case(1, [1, 0, 3], [vec![1 ], vec![ ], vec![2, 3, 4]])] + #[case(0, [1, 1, 3], [vec![0 ], vec![1 ], vec![2, 3, 4]])] + #[case(1, [1, 1, 3], [vec![1 ], vec![2 ], vec![3, 4, 5]])] + #[case(0, [1, 2, 3], [vec![0 ], vec![1, 2 ], vec![3, 4, 5]])] + #[case(1, [1, 2, 3], [vec![1 ], vec![2, 3 ], vec![4, 5, 6]])] + #[case(0, [1, 3, 3], [vec![0 ], vec![1, 2, 3], vec![4, 5, 6]])] + #[case(1, [1, 3, 3], [vec![1 ], vec![2, 3, 4], vec![5, 6, 7]])] + #[case(0, [2, 0, 3], [vec![0, 1 ], vec![ ], vec![2, 3, 4]])] + #[case(1, [2, 0, 3], [vec![1, 2 ], vec![ ], vec![3, 4, 5]])] + #[case(0, [2, 1, 3], [vec![0, 1 ], vec![2 ], vec![3, 4, 5]])] + #[case(1, [2, 1, 3], [vec![1, 2 ], vec![3 ], vec![4, 5, 6]])] + #[case(0, [2, 2, 3], [vec![0, 1 ], vec![2, 3 ], vec![4, 5, 6]])] + #[case(1, [2, 2, 3], [vec![1, 2 ], vec![3, 4 ], vec![5, 6, 7]])] + #[case(0, [2, 3, 3], [vec![0, 1 ], vec![2, 3, 4], vec![5, 6, 7]])] + #[case(1, [2, 3, 3], [vec![1, 2 ], vec![3, 4, 5], vec![6, 7, 8]])] + #[case(0, [3, 0, 3], [vec![0, 1, 2], vec![ ], vec![3, 4, 5]])] + #[case(1, [3, 0, 3], [vec![1, 2, 3], vec![ ], vec![4, 5, 6]])] + #[case(0, [3, 1, 3], [vec![0, 1, 2], vec![3 ], vec![4, 5, 6]])] + #[case(1, [3, 1, 3], [vec![1, 2, 3], vec![4 ], vec![5, 6, 7]])] + #[case(0, [3, 2, 3], [vec![0, 1, 2], vec![3, 4 ], vec![5, 6, 7]])] + #[case(1, [3, 2, 3], [vec![1, 2, 3], vec![4, 5 ], vec![6, 7, 8]])] #[case(0, [3, 3, 3], [vec![0, 1, 2], vec![3, 4, 5], vec![6, 7, 8]])] #[case(1, [3, 3, 3], [vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]])] - fn chunked_range_3( + fn chunked_range_3( #[case] input_start: usize, - #[case] input_chunk_sizes: [usize; N], - #[case] expected: [Vec; N], + #[case] input_chunk_sizes: [usize; 3], + #[case] expected: [Vec; 3], ) { assert_eq!( chunked_range(input_start, input_chunk_sizes) - .map(Iterator::collect::>) + .map(Vec::from_iter) .collect::>(), expected ); diff --git a/lyra_ext/src/iter/multi_interleave.rs b/lyra_ext/src/iter/multi_interleave.rs index 3137bbf..3937722 100644 --- a/lyra_ext/src/iter/multi_interleave.rs +++ b/lyra_ext/src/iter/multi_interleave.rs @@ -1,19 +1,25 @@ -pub fn multi_interleave(iters: impl IntoIterator) -> MultiInterleave +pub fn multi_interleave(iters: A) -> MultiInterleave<::IntoIter> where - I: IntoIterator, - J: Iterator, - Box<[J]>: FromIterator, + A: IntoIterator, + A::Item: IntoIterator, + ::IntoIter: Iterator, { MultiInterleave::new(iters.into_iter().map(IntoIterator::into_iter).collect()) } -pub struct MultiInterleave { +pub struct MultiInterleave +where + I: Iterator, +{ iterators: Box<[I]>, current: usize, } -impl MultiInterleave { - fn new(iterators: Box<[I]>) -> Self { +impl MultiInterleave +where + I: Iterator, +{ + const fn new(iterators: Box<[I]>) -> Self { Self { iterators, current: 0, @@ -21,7 +27,10 @@ impl MultiInterleave { } } -impl Iterator for MultiInterleave { +impl Iterator for MultiInterleave +where + I: Iterator, +{ type Item = I::Item; fn next(&mut self) -> Option { @@ -51,107 +60,110 @@ mod tests { use super::multi_interleave; #[rstest] - #[case([], vec![])] - fn multi_interleave_0(#[case] input: [Vec; N], #[case] expected: Vec) { + #[case([], [])] + fn multi_interleave_0(#[case] input: [[u8; 0]; 0], #[case] expected: [u8; 0]) { assert_eq!(multi_interleave(input).collect::>(), expected); } #[rstest] - #[case([vec![]] , vec![])] - #[case([vec![1]] , vec![1])] - #[case([vec![1, 2]] , vec![1, 2])] - #[case([vec![1, 2, 3]], vec![1, 2, 3])] - fn multi_interleave_1(#[case] input: [Vec; N], #[case] expected: Vec) { + #[case([[ ]], [ ])] + #[case([[1 ]], [1 ])] + #[case([[1, 2]], [1, 2 ])] + #[case([[1, 2, 3]], [1, 2, 3])] + fn multi_interleave_1( + #[case] input: [[u8; N]; 1], + #[case] expected: [u8; M], + ) { assert_eq!(multi_interleave(input).collect::>(), expected); } #[rstest] - #[case([vec![] , vec![]] , vec![])] - #[case([vec![1] , vec![]] , vec![1])] - #[case([vec![1, 2] , vec![]] , vec![1, 2])] - #[case([vec![1, 2, 3], vec![]] , vec![1, 2, 3])] - #[case([vec![] , vec![1]] , vec![1])] - #[case([vec![1] , vec![1]] , vec![1, 1])] - #[case([vec![1, 2] , vec![1]] , vec![1, 1, 2])] - #[case([vec![1, 2, 3], vec![1]] , vec![1, 1, 2, 3])] - #[case([vec![] , vec![1, 2]] , vec![1, 2])] - #[case([vec![1] , vec![1, 2]] , vec![1, 1, 2])] - #[case([vec![1, 2] , vec![1, 2]] , vec![1, 1, 2, 2])] - #[case([vec![1, 2, 3], vec![1, 2]] , vec![1, 1, 2, 2, 3])] - #[case([vec![] , vec![1, 2, 3]], vec![1, 2, 3])] - #[case([vec![1] , vec![1, 2, 3]], vec![1, 1, 2, 3])] - #[case([vec![1, 2] , vec![1, 2, 3]], vec![1, 1, 2, 2, 3])] - #[case([vec![1, 2, 3], vec![1, 2, 3]], vec![1, 1, 2, 2, 3, 3])] - fn multi_interleave_2(#[case] input: [Vec; N], #[case] expected: Vec) { + #[case([vec![ ], vec![ ]], [ ])] + #[case([vec![1 ], vec![ ]], [1 ])] + #[case([vec![1, 2 ], vec![ ]], [1, 2 ])] + #[case([vec![1, 2, 3], vec![ ]], [1, 2, 3 ])] + #[case([vec![ ], vec![1 ]], [1 ])] + #[case([vec![1 ], vec![1 ]], [1, 1 ])] + #[case([vec![1, 2 ], vec![1 ]], [1, 1, 2 ])] + #[case([vec![1, 2, 3], vec![1 ]], [1, 1, 2, 3 ])] + #[case([vec![ ], vec![1, 2 ]], [1, 2 ])] + #[case([vec![1 ], vec![1, 2 ]], [1, 1, 2 ])] + #[case([vec![1, 2 ], vec![1, 2 ]], [1, 1, 2, 2 ])] + #[case([vec![1, 2, 3], vec![1, 2 ]], [1, 1, 2, 2, 3 ])] + #[case([vec![ ], vec![1, 2, 3]], [1, 2, 3 ])] + #[case([vec![1 ], vec![1, 2, 3]], [1, 1, 2, 3 ])] + #[case([vec![1, 2 ], vec![1, 2, 3]], [1, 1, 2, 2, 3 ])] + #[case([vec![1, 2, 3], vec![1, 2, 3]], [1, 1, 2, 2, 3, 3])] + fn multi_interleave_2(#[case] input: [Vec; 2], #[case] expected: [u8; M]) { assert_eq!(multi_interleave(input).collect::>(), expected); } #[rstest] - #[case([vec![] , vec![], vec![]], vec![])] - #[case([vec![1] , vec![], vec![]], vec![1])] - #[case([vec![1, 2] , vec![], vec![]], vec![1, 2])] - #[case([vec![1, 2, 3], vec![], vec![]], vec![1, 2, 3])] - #[case([vec![] , vec![1], vec![]], vec![1])] - #[case([vec![1] , vec![1], vec![]], vec![1, 1])] - #[case([vec![1, 2] , vec![1], vec![]], vec![1, 1, 2])] - #[case([vec![1, 2, 3], vec![1], vec![]], vec![1, 1, 2, 3])] - #[case([vec![] , vec![1, 2], vec![]], vec![1, 2])] - #[case([vec![1] , vec![1, 2], vec![]], vec![1, 1, 2])] - #[case([vec![1, 2] , vec![1, 2], vec![]], vec![1, 1, 2, 2])] - #[case([vec![1, 2, 3], vec![1, 2], vec![]], vec![1, 1, 2, 2, 3])] - #[case([vec![] , vec![1, 2, 3], vec![]], vec![1, 2, 3])] - #[case([vec![1] , vec![1, 2, 3], vec![]], vec![1, 1, 2, 3])] - #[case([vec![1, 2] , vec![1, 2, 3], vec![]], vec![1, 1, 2, 2, 3])] - #[case([vec![1, 2, 3], vec![1, 2, 3], vec![]], vec![1, 1, 2, 2, 3, 3])] - #[case([vec![] , vec![], vec![1]], vec![1])] - #[case([vec![1] , vec![], vec![1]], vec![1, 1])] - #[case([vec![1, 2] , vec![], vec![1]], vec![1, 1, 2])] - #[case([vec![1, 2, 3], vec![], vec![1]], vec![1, 1, 2, 3])] - #[case([vec![] , vec![1], vec![1]], vec![1, 1])] - #[case([vec![1] , vec![1], vec![1]], vec![1, 1, 1])] - #[case([vec![1, 2] , vec![1], vec![1]], vec![1, 1, 1, 2])] - #[case([vec![1, 2, 3], vec![1], vec![1]], vec![1, 1, 1, 2, 3])] - #[case([vec![] , vec![1, 2], vec![1]], vec![1, 1, 2])] - #[case([vec![1] , vec![1, 2], vec![1]], vec![1, 1, 1, 2])] - #[case([vec![1, 2] , vec![1, 2], vec![1]], vec![1, 1, 1, 2, 2])] - #[case([vec![1, 2, 3], vec![1, 2], vec![1]], vec![1, 1, 1, 2, 2, 3])] - #[case([vec![] , vec![1, 2, 3], vec![1]], vec![1, 1, 2, 3])] - #[case([vec![1] , vec![1, 2, 3], vec![1]], vec![1, 1, 1, 2, 3])] - #[case([vec![1, 2] , vec![1, 2, 3], vec![1]], vec![1, 1, 1, 2, 2, 3])] - #[case([vec![1, 2, 3], vec![1, 2, 3], vec![1]], vec![1, 1, 1, 2, 2, 3, 3])] - #[case([vec![] , vec![], vec![1, 2]], vec![1, 2])] - #[case([vec![1] , vec![], vec![1, 2]], vec![1, 1, 2])] - #[case([vec![1, 2] , vec![], vec![1, 2]], vec![1, 1, 2, 2])] - #[case([vec![1, 2, 3], vec![], vec![1, 2]], vec![1, 1, 2, 2, 3])] - #[case([vec![] , vec![1], vec![1, 2]], vec![1, 1, 2])] - #[case([vec![1] , vec![1], vec![1, 2]], vec![1, 1, 1, 2])] - #[case([vec![1, 2] , vec![1], vec![1, 2]], vec![1, 1, 1, 2, 2])] - #[case([vec![1, 2, 3], vec![1], vec![1, 2]], vec![1, 1, 1, 2, 2, 3])] - #[case([vec![] , vec![1, 2], vec![1, 2]], vec![1, 1, 2, 2])] - #[case([vec![1] , vec![1, 2], vec![1, 2]], vec![1, 1, 1, 2, 2])] - #[case([vec![1, 2] , vec![1, 2], vec![1, 2]], vec![1, 1, 1, 2, 2, 2])] - #[case([vec![1, 2, 3], vec![1, 2], vec![1, 2]], vec![1, 1, 1, 2, 2, 2, 3])] - #[case([vec![] , vec![1, 2, 3], vec![1, 2]], vec![1, 1, 2, 2, 3])] - #[case([vec![1] , vec![1, 2, 3], vec![1, 2]], vec![1, 1, 1, 2, 2, 3])] - #[case([vec![1, 2] , vec![1, 2, 3], vec![1, 2]], vec![1, 1, 1, 2, 2, 2, 3])] - #[case([vec![1, 2, 3], vec![1, 2, 3], vec![1, 2]], vec![1, 1, 1, 2, 2, 2, 3, 3])] - #[case([vec![] , vec![], vec![1, 2, 3]], vec![1, 2, 3])] - #[case([vec![1] , vec![], vec![1, 2, 3]], vec![1, 1, 2, 3])] - #[case([vec![1, 2] , vec![], vec![1, 2, 3]], vec![1, 1, 2, 2, 3])] - #[case([vec![1, 2, 3], vec![], vec![1, 2, 3]], vec![1, 1, 2, 2, 3, 3])] - #[case([vec![] , vec![1], vec![1, 2, 3]], vec![1, 1, 2, 3])] - #[case([vec![1] , vec![1], vec![1, 2, 3]], vec![1, 1, 1, 2, 3])] - #[case([vec![1, 2] , vec![1], vec![1, 2, 3]], vec![1, 1, 1, 2, 2, 3])] - #[case([vec![1, 2, 3], vec![1], vec![1, 2, 3]], vec![1, 1, 1, 2, 2, 3, 3])] - #[case([vec![] , vec![1, 2], vec![1, 2, 3]], vec![1, 1, 2, 2, 3])] - #[case([vec![1] , vec![1, 2], vec![1, 2, 3]], vec![1, 1, 1, 2, 2, 3])] - #[case([vec![1, 2] , vec![1, 2], vec![1, 2, 3]], vec![1, 1, 1, 2, 2, 2, 3])] - #[case([vec![1, 2, 3], vec![1, 2], vec![1, 2, 3]], vec![1, 1, 1, 2, 2, 2, 3, 3])] - #[case([vec![] , vec![1, 2, 3], vec![1, 2, 3]], vec![1, 1, 2, 2, 3, 3])] - #[case([vec![1] , vec![1, 2, 3], vec![1, 2, 3]], vec![1, 1, 1, 2, 2, 3, 3])] - #[case([vec![1, 2] , vec![1, 2, 3], vec![1, 2, 3]], vec![1, 1, 1, 2, 2, 2, 3, 3])] - #[case([vec![1, 2, 3], vec![1, 2, 3], vec![1, 2, 3]], vec![1, 1, 1, 2, 2, 2, 3, 3, 3])] - fn multi_interleave_3(#[case] input: [Vec; N], #[case] expected: Vec) { + #[case([vec![ ], vec![ ], vec![ ]], [ ])] + #[case([vec![1 ], vec![ ], vec![ ]], [1 ])] + #[case([vec![1, 2 ], vec![ ], vec![ ]], [1, 2 ])] + #[case([vec![1, 2, 3], vec![ ], vec![ ]], [1, 2, 3 ])] + #[case([vec![ ], vec![1 ], vec![ ]], [1 ])] + #[case([vec![1 ], vec![1 ], vec![ ]], [1, 1 ])] + #[case([vec![1, 2 ], vec![1 ], vec![ ]], [1, 1, 2 ])] + #[case([vec![1, 2, 3], vec![1 ], vec![ ]], [1, 1, 2, 3 ])] + #[case([vec![ ], vec![1, 2 ], vec![ ]], [1, 2 ])] + #[case([vec![1 ], vec![1, 2 ], vec![ ]], [1, 1, 2 ])] + #[case([vec![1, 2 ], vec![1, 2 ], vec![ ]], [1, 1, 2, 2 ])] + #[case([vec![1, 2, 3], vec![1, 2 ], vec![ ]], [1, 1, 2, 2, 3 ])] + #[case([vec![ ], vec![1, 2, 3], vec![ ]], [1, 2, 3 ])] + #[case([vec![1 ], vec![1, 2, 3], vec![ ]], [1, 1, 2, 3 ])] + #[case([vec![1, 2 ], vec![1, 2, 3], vec![ ]], [1, 1, 2, 2, 3 ])] + #[case([vec![1, 2, 3], vec![1, 2, 3], vec![ ]], [1, 1, 2, 2, 3, 3 ])] + #[case([vec![ ], vec![ ], vec![1 ]], [1 ])] + #[case([vec![1 ], vec![ ], vec![1 ]], [1, 1 ])] + #[case([vec![1, 2 ], vec![ ], vec![1 ]], [1, 1, 2 ])] + #[case([vec![1, 2, 3], vec![ ], vec![1 ]], [1, 1, 2, 3 ])] + #[case([vec![ ], vec![1 ], vec![1 ]], [1, 1 ])] + #[case([vec![1 ], vec![1 ], vec![1 ]], [1, 1, 1 ])] + #[case([vec![1, 2 ], vec![1 ], vec![1 ]], [1, 1, 1, 2 ])] + #[case([vec![1, 2, 3], vec![1 ], vec![1 ]], [1, 1, 1, 2, 3 ])] + #[case([vec![ ], vec![1, 2 ], vec![1 ]], [1, 1, 2 ])] + #[case([vec![1 ], vec![1, 2 ], vec![1 ]], [1, 1, 1, 2 ])] + #[case([vec![1, 2 ], vec![1, 2 ], vec![1 ]], [1, 1, 1, 2, 2 ])] + #[case([vec![1, 2, 3], vec![1, 2 ], vec![1 ]], [1, 1, 1, 2, 2, 3 ])] + #[case([vec![ ], vec![1, 2, 3], vec![1 ]], [1, 1, 2, 3 ])] + #[case([vec![1 ], vec![1, 2, 3], vec![1 ]], [1, 1, 1, 2, 3 ])] + #[case([vec![1, 2 ], vec![1, 2, 3], vec![1 ]], [1, 1, 1, 2, 2, 3 ])] + #[case([vec![1, 2, 3], vec![1, 2, 3], vec![1 ]], [1, 1, 1, 2, 2, 3, 3 ])] + #[case([vec![ ], vec![ ], vec![1, 2 ]], [1, 2 ])] + #[case([vec![1 ], vec![ ], vec![1, 2 ]], [1, 1, 2 ])] + #[case([vec![1, 2 ], vec![ ], vec![1, 2 ]], [1, 1, 2, 2 ])] + #[case([vec![1, 2, 3], vec![ ], vec![1, 2 ]], [1, 1, 2, 2, 3 ])] + #[case([vec![ ], vec![1 ], vec![1, 2 ]], [1, 1, 2 ])] + #[case([vec![1 ], vec![1 ], vec![1, 2 ]], [1, 1, 1, 2 ])] + #[case([vec![1, 2 ], vec![1 ], vec![1, 2 ]], [1, 1, 1, 2, 2 ])] + #[case([vec![1, 2, 3], vec![1 ], vec![1, 2 ]], [1, 1, 1, 2, 2, 3 ])] + #[case([vec![ ], vec![1, 2 ], vec![1, 2 ]], [1, 1, 2, 2 ])] + #[case([vec![1 ], vec![1, 2 ], vec![1, 2 ]], [1, 1, 1, 2, 2 ])] + #[case([vec![1, 2 ], vec![1, 2 ], vec![1, 2 ]], [1, 1, 1, 2, 2, 2 ])] + #[case([vec![1, 2, 3], vec![1, 2 ], vec![1, 2 ]], [1, 1, 1, 2, 2, 2, 3 ])] + #[case([vec![ ], vec![1, 2, 3], vec![1, 2 ]], [1, 1, 2, 2, 3 ])] + #[case([vec![1 ], vec![1, 2, 3], vec![1, 2 ]], [1, 1, 1, 2, 2, 3 ])] + #[case([vec![1, 2 ], vec![1, 2, 3], vec![1, 2 ]], [1, 1, 1, 2, 2, 2, 3 ])] + #[case([vec![1, 2, 3], vec![1, 2, 3], vec![1, 2 ]], [1, 1, 1, 2, 2, 2, 3, 3 ])] + #[case([vec![ ], vec![ ], vec![1, 2, 3]], [1, 2, 3 ])] + #[case([vec![1 ], vec![ ], vec![1, 2, 3]], [1, 1, 2, 3 ])] + #[case([vec![1, 2 ], vec![ ], vec![1, 2, 3]], [1, 1, 2, 2, 3 ])] + #[case([vec![1, 2, 3], vec![ ], vec![1, 2, 3]], [1, 1, 2, 2, 3, 3 ])] + #[case([vec![ ], vec![1 ], vec![1, 2, 3]], [1, 1, 2, 3 ])] + #[case([vec![1 ], vec![1 ], vec![1, 2, 3]], [1, 1, 1, 2, 3 ])] + #[case([vec![1, 2 ], vec![1 ], vec![1, 2, 3]], [1, 1, 1, 2, 2, 3 ])] + #[case([vec![1, 2, 3], vec![1 ], vec![1, 2, 3]], [1, 1, 1, 2, 2, 3, 3 ])] + #[case([vec![ ], vec![1, 2 ], vec![1, 2, 3]], [1, 1, 2, 2, 3 ])] + #[case([vec![1 ], vec![1, 2 ], vec![1, 2, 3]], [1, 1, 1, 2, 2, 3 ])] + #[case([vec![1, 2 ], vec![1, 2 ], vec![1, 2, 3]], [1, 1, 1, 2, 2, 2, 3 ])] + #[case([vec![1, 2, 3], vec![1, 2 ], vec![1, 2, 3]], [1, 1, 1, 2, 2, 2, 3, 3 ])] + #[case([vec![ ], vec![1, 2, 3], vec![1, 2, 3]], [1, 1, 2, 2, 3, 3 ])] + #[case([vec![1 ], vec![1, 2, 3], vec![1, 2, 3]], [1, 1, 1, 2, 2, 3, 3 ])] + #[case([vec![1, 2 ], vec![1, 2, 3], vec![1, 2, 3]], [1, 1, 1, 2, 2, 2, 3, 3 ])] + #[case([vec![1, 2, 3], vec![1, 2, 3], vec![1, 2, 3]], [1, 1, 1, 2, 2, 2, 3, 3, 3])] + fn multi_interleave_3(#[case] input: [Vec; 3], #[case] expected: [u8; M]) { assert_eq!(multi_interleave(input).collect::>(), expected); } } diff --git a/lyra_ext/src/lib.rs b/lyra_ext/src/lib.rs index f84827e..b773876 100644 --- a/lyra_ext/src/lib.rs +++ b/lyra_ext/src/lib.rs @@ -3,6 +3,9 @@ pub mod image; pub mod iter; pub mod logical_bind; pub mod nested_transpose; +pub mod num; pub mod pretty; pub mod rgb_hex; pub mod time; + +pub use time::{iso8601::iso8601 as iso8601_time, unix::unix as unix_time}; diff --git a/lyra_ext/src/num.rs b/lyra_ext/src/num.rs new file mode 100644 index 0000000..6de8787 --- /dev/null +++ b/lyra_ext/src/num.rs @@ -0,0 +1,13 @@ +#[inline] +#[must_use] +#[allow(clippy::cast_possible_truncation)] +pub const fn u64_to_i64_truncating(n: u64) -> i64 { + (n as i128) as i64 +} + +#[inline] +#[must_use] +#[allow(clippy::cast_possible_truncation)] +pub const fn usize_to_i64_truncating(n: usize) -> i64 { + (n as i128) as i64 +} diff --git a/lyra_ext/src/pretty/duration_display.rs b/lyra_ext/src/pretty/duration_display.rs index 4c07f81..06cd45f 100644 --- a/lyra_ext/src/pretty/duration_display.rs +++ b/lyra_ext/src/pretty/duration_display.rs @@ -1,56 +1,57 @@ -use std::{fmt::Display, sync::OnceLock, time::Duration}; +use std::{fmt::Display, sync::LazyLock, time::Duration}; use regex::Regex; -fn timestamp() -> &'static Regex { - static TIMESTAMP: OnceLock = OnceLock::new(); - TIMESTAMP.get_or_init(|| { - Regex::new( - r"^(((?[1-9]\d*):(?[0-5]\d))|(?[0-5]?\d)):(?[0-5]\d)(\.(?\d{3}))?$", - ) - .expect("regex is valid") - }) -} +static TIMESTAMP: LazyLock = LazyLock::new(|| { + Regex::new( + r"^(((?[1-9]\d*):(?[0-5]\d))|(?[0-5]?\d)):(?[0-5]\d)(\.(?\d{3}))?$", + ) + .expect("regex is valid") +}); -fn timestamp_2() -> &'static Regex { - static TIMESTAMP_2: OnceLock = OnceLock::new(); - TIMESTAMP_2.get_or_init(|| { - Regex::new( - r"^((?[1-9]\d*)\s?hr?)?\s*((?[1-9]|[1-5]\d)\s?m(in)?)?\s*((?[1-9]|[1-5]\d)\s?s(ec)?)?\s*((?[1-9]\d{0,2})\s?ms(ec)?)?$" - ) - .expect("regex is valid") - }) -} +static TIMESTAMP_2: LazyLock = LazyLock::new(|| { + Regex::new( + r"^((?[1-9]\d*)\s?hr?)?\s*((?[1-9]|[1-5]\d)\s?m(in)?)?\s*((?[1-9]|[1-5]\d)\s?s(ec)?)?\s*((?[1-9]\d{0,2})\s?ms(ec)?)?$" + ).expect("regex is valid") +}); + +pub struct PrettyDurationDisplayer(u128); -pub struct PrettyDurationRefDisplayer<'a>(&'a Duration); +pub trait DurationDisplay { + fn pretty_display(&self) -> PrettyDurationDisplayer; +} -pub trait PrettyDurationDisplay { - fn pretty_display(&self) -> PrettyDurationRefDisplayer; +impl DurationDisplay for Duration { + fn pretty_display(&self) -> PrettyDurationDisplayer { + PrettyDurationDisplayer(self.as_millis()) + } } -impl PrettyDurationDisplay for Duration { - fn pretty_display(&self) -> PrettyDurationRefDisplayer { - PrettyDurationRefDisplayer(self) +impl DurationDisplay for u128 { + fn pretty_display(&self) -> PrettyDurationDisplayer { + PrettyDurationDisplayer(*self) } } -trait FromPrettyStr { - fn from_pretty_str(value: &str) -> Result - where - Self: Sized; +pub struct FromPrettyStrError; + +pub trait FromPrettyStr +where + Self: Sized, +{ + /// # Errors + /// if `value` doesn't match `timestamp` or `timestamp_2` regex + fn from_pretty_str(value: &str) -> Result; } impl FromPrettyStr for Duration { - fn from_pretty_str(value: &str) -> Result - where - Self: Sized, - { - let captures = if let Some(captures) = timestamp().captures(value) { + fn from_pretty_str(value: &str) -> Result { + let captures = if let Some(captures) = TIMESTAMP.captures(value) { captures - } else if let Some(captures) = timestamp_2().captures(value) { + } else if let Some(captures) = TIMESTAMP_2.captures(value) { captures } else { - return Err(value); + return Err(FromPrettyStrError); }; let ms = captures @@ -77,23 +78,20 @@ impl FromPrettyStr for Duration { } } -fn fmt(millis: u128, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let divrem = |x, y| (x / y, x % y); - - let (s, ms) = divrem(millis, 1000); - let (m, s) = divrem(s, 60); - let (h, m) = divrem(m, 60); - - match (h, m, s) { - (0, 0, 0) => write!(f, "0:00.{ms:03}"), - (0, m, s) => write!(f, "{m}:{s:02}"), - (h, m, s) => write!(f, "{h}:{m:02}:{s:02}"), - } -} - -impl Display for PrettyDurationRefDisplayer<'_> { +impl Display for PrettyDurationDisplayer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - fmt(self.0.as_millis(), f) + let f: &mut std::fmt::Formatter<'_> = f; + let divrem = |x, y| (x / y, x % y); + + let (s, ms) = divrem(self.0, 1000); + let (m, s) = divrem(s, 60); + let (h, m) = divrem(m, 60); + + match (h, m, s) { + (0, 0, 0) => write!(f, "0:00.{ms:03}"), + (0, m, s) => write!(f, "{m}:{s:02}"), + (h, m, s) => write!(f, "{h}:{m:02}:{s:02}"), + } } } @@ -103,7 +101,7 @@ mod test { use rstest::rstest; - use super::{FromPrettyStr, PrettyDurationDisplay}; + use super::{DurationDisplay, FromPrettyStr}; #[rstest] #[case(Duration::ZERO, "0:00.000")] diff --git a/lyra_ext/src/pretty/flags_display.rs b/lyra_ext/src/pretty/flags_display.rs index d733aa2..8184835 100644 --- a/lyra_ext/src/pretty/flags_display.rs +++ b/lyra_ext/src/pretty/flags_display.rs @@ -28,13 +28,13 @@ where } flag }) - .collect::>() + .collect::>() .pretty_join_with_and(); f.write_str(&s) } } -pub trait PrettyFlagsDisplay: Flags { +pub trait FlagsDisplay: Flags { fn pretty_display(&self) -> PrettyFlagsDisplayer { PrettyFlagsDisplayer { inner: self, @@ -55,7 +55,7 @@ mod test { use bitflags::bitflags; use rstest::rstest; - use super::PrettyFlagsDisplay; + use super::FlagsDisplay; bitflags! { struct TestFlag: u8 { @@ -71,7 +71,7 @@ mod test { } } - impl PrettyFlagsDisplay for TestFlag {} + impl FlagsDisplay for TestFlag {} #[rstest] #[case(TestFlag::empty(), "")] @@ -103,7 +103,7 @@ mod test { } } - impl PrettyFlagsDisplay for TestFlag2 {} + impl FlagsDisplay for TestFlag2 {} #[rstest] #[case(TestFlag2::empty(), "")] diff --git a/lyra_ext/src/pretty/join.rs b/lyra_ext/src/pretty/join.rs index ccc3f81..ba03440 100644 --- a/lyra_ext/src/pretty/join.rs +++ b/lyra_ext/src/pretty/join.rs @@ -1,6 +1,6 @@ use std::borrow::Borrow; -pub trait PrettyJoin { +pub trait Join { type Joined; fn pretty_join(slice: &Self, sep: J, last_sep: J) -> Self::Joined; @@ -13,33 +13,33 @@ pub trait PrettyJoiner { fn and() -> Self::Joiner; fn or() -> Self::Joiner; - fn pretty_join(&self, sep: J, last_sep: J) -> >::Joined + fn pretty_join(&self, sep: J, last_sep: J) -> >::Joined where - Self: PrettyJoin, + Self: Join, { - PrettyJoin::pretty_join(self, sep, last_sep) + Join::pretty_join(self, sep, last_sep) } - fn pretty_join_with(&self, last_sep: Self::Joiner) -> >::Joined + fn pretty_join_with(&self, last_sep: Self::Joiner) -> >::Joined where - Self: PrettyJoin, + Self: Join, { - PrettyJoin::pretty_join(self, Self::sep(), last_sep) + Join::pretty_join(self, Self::sep(), last_sep) } - fn pretty_join_with_and(&self) -> >::Joined + fn pretty_join_with_and(&self) -> >::Joined where - Self: PrettyJoin, + Self: Join, { - PrettyJoin::pretty_join(self, Self::sep(), Self::and()) + Join::pretty_join(self, Self::sep(), Self::and()) } - fn pretty_join_with_or(&self) -> >::Joined + fn pretty_join_with_or(&self) -> >::Joined where - Self: PrettyJoin, + Self: Join, { - PrettyJoin::pretty_join(self, Self::sep(), Self::or()) + Join::pretty_join(self, Self::sep(), Self::or()) } } -impl> PrettyJoin<&str> for [S] { +impl> Join<&str> for [S] { type Joined = String; fn pretty_join(slice: &Self, sep: &str, last_sep: &str) -> Self::Joined { diff --git a/lyra_ext/src/pretty/truncate.rs b/lyra_ext/src/pretty/truncate.rs index 7889b05..e4ddc5d 100644 --- a/lyra_ext/src/pretty/truncate.rs +++ b/lyra_ext/src/pretty/truncate.rs @@ -6,6 +6,7 @@ pub trait PrettyTruncator: AsGrapheme + ToOwned + 'static where for<'a> Cow<'a, Self>: Add<&'a Self, Output = Cow<'a, Self>>, { + fn empty() -> &'static Self; fn trail() -> &'static Self; fn pretty_truncate(&self, new_len: usize) -> Cow where @@ -15,11 +16,23 @@ where (self.grapheme_len() <= new_len) .then_some(Cow::Borrowed(self)) - .unwrap_or_else(|| self.grapheme_truncate(new_len - trail.grapheme_len()) + trail) + .unwrap_or_else(|| { + let Some(len) = new_len.checked_sub(trail.grapheme_len()) else { + return Cow::Borrowed(Self::empty()); + }; + if len == 0 { + return Cow::Borrowed(trail); + } + self.grapheme_truncate(len) + trail + }) } } impl PrettyTruncator for str { + fn empty() -> &'static Self { + "" + } + fn trail() -> &'static Self { "…" } @@ -33,13 +46,65 @@ mod test { #[rstest] #[case("", "")] - #[case("1", "1")] - #[case("234", "234")] - #[case("5678", "56…")] - #[case("竪琴を弾く", "竪琴…")] - #[case("การเขียนโปรแกรม", "กา…")] - #[case("😶‍🌫️😮‍💨😵‍💫❤️‍🔥❤️‍🩹👁️‍🗨️", "😶‍🌫️😮‍💨…")] - fn string_pretty_truncate(#[case] input: &str, #[case] expected: &str) { + #[case("1", "")] + #[case("23", "")] + #[case("456", "")] + #[case("ตี", "")] + #[case("งุงิ", "")] + #[case("อิอิอิ", "")] + #[case("❤️‍🩹", "")] + #[case("👁️‍🗨️😶‍🌫️", "")] + #[case("🏄‍♂️🐱‍🚀🏳️‍🌈", "")] + #[case("Hello there!", "")] + fn pretty_truncate_0(#[case] input: &str, #[case] expected: &str) { + assert_eq!(input.pretty_truncate(0), expected); + } + + #[rstest] + #[case("", "")] + #[case("a", "a")] + #[case("bc", "…")] + #[case("def", "…")] + #[case("ดี", "ดี")] + #[case("มุมิ", "…")] + #[case("จัตุรัส", "…")] + #[case("🤹🏼‍♀️", "🤹🏼‍♀️")] + #[case("👨🏻‍🚒🏳‍🟧‍⬛‍🟧", "…")] + #[case("🐱‍👓👯🏾‍♂️🐦‍⬛", "…")] + #[case("我想成为哥特女孩。。。", "…")] + fn pretty_truncate_1(#[case] input: &str, #[case] expected: &str) { + assert_eq!(input.pretty_truncate(1), expected); + } + + #[rstest] + #[case("", "")] + #[case("[", "[")] + #[case("{}", "{}")] + #[case("<=>", "<…")] + #[case("🇳🇴", "🇳🇴")] + #[case("🇿🇦🇲🇳", "🇿🇦🇲🇳")] + #[case("🇸🇨🇷🇴🇱🇾", "🇸🇨…")] + #[case("👨‍👶‍👦", "👨‍👶‍👦")] + #[case("🏊🏼‍♂️🤼‍♀️", "🏊🏼‍♂️🤼‍♀️")] + #[case("👩‍👩‍👧‍👦🤸🏼‍♂️😵‍💫", "👩‍👩‍👧‍👦…")] + #[case("剣光よ、世の乱れを斬り尽くせ!", "剣…")] + fn pretty_truncate_2(#[case] input: &str, #[case] expected: &str) { + assert_eq!(input.pretty_truncate(2), expected); + } + + #[rstest] + #[case("", "")] + #[case(";", ";")] + #[case("//", "//")] + #[case("===", "===")] + #[case("🇸🇴", "🇸🇴")] + #[case("🇷🇴🇫🇮", "🇷🇴🇫🇮")] + #[case("🇬🇬🇦🇱🇨🇨", "🇬🇬🇦🇱🇨🇨")] + #[case("💆🏿‍♂️", "💆🏿‍♂️")] + #[case("🤹‍♀️🐱‍🏍", "🤹‍♀️🐱‍🏍")] + #[case("🏴‍☠️👮‍♂️🐻‍❄️", "🏴‍☠️👮‍♂️🐻‍❄️")] + #[case("ลาลาลา ลาลา ลาลาลา ลา ลา ลา~", "ลา…")] + fn pretty_truncate_3(#[case] input: &str, #[case] expected: &str) { assert_eq!(input.pretty_truncate(3), expected); } } diff --git a/lyra_ext/src/rgb_hex.rs b/lyra_ext/src/rgb_hex.rs index 87db1e9..1077386 100644 --- a/lyra_ext/src/rgb_hex.rs +++ b/lyra_ext/src/rgb_hex.rs @@ -17,46 +17,46 @@ mod test { use rstest::rstest; #[rstest] - #[case([0, 0, 0], 0x00_00_00)] - #[case([255, 0, 0], 0xFF_00_00)] - #[case([0, 255, 0], 0x00_FF_00)] - #[case([255, 255, 0], 0xFF_FF_00)] - #[case([0, 0, 255], 0x00_00_FF)] - #[case([255, 0, 255], 0xFF_00_FF)] - #[case([0, 255, 255], 0x00_FF_FF)] + #[case([0 , 0 , 0 ], 0x00_00_00)] + #[case([255, 0 , 0 ], 0xFF_00_00)] + #[case([0 , 255, 0 ], 0x00_FF_00)] + #[case([255, 255, 0 ], 0xFF_FF_00)] + #[case([0 , 0 , 255], 0x00_00_FF)] + #[case([255, 0 , 255], 0xFF_00_FF)] + #[case([0 , 255, 255], 0x00_FF_FF)] #[case([255, 255, 255], 0xFF_FF_FF)] - #[case([123, 45, 67], 0x7B_2D_43)] - #[case([89, 101, 112], 0x59_65_70)] + #[case([123, 45 , 67 ], 0x7B_2D_43)] + #[case([89 , 101, 112], 0x59_65_70)] fn rgb_to_hex(#[case] input: [u8; 3], #[case] expected: u32) { assert_eq!(super::rgb_to_hex(input), expected); } #[rstest] - #[case(0x00_00_00, [0, 0, 0])] - #[case(0xFF_00_00, [255, 0, 0])] - #[case(0x00_FF_00, [0, 255, 0])] - #[case(0xFF_FF_00, [255, 255, 0])] - #[case(0x00_00_FF, [0, 0, 255])] - #[case(0xFF_00_FF, [255, 0, 255])] - #[case(0x00_FF_FF, [0, 255, 255])] + #[case(0x00_00_00, [0 , 0 , 0 ])] + #[case(0xFF_00_00, [255, 0 , 0 ])] + #[case(0x00_FF_00, [0 , 255, 0 ])] + #[case(0xFF_FF_00, [255, 255, 0 ])] + #[case(0x00_00_FF, [0 , 0 , 255])] + #[case(0xFF_00_FF, [255, 0 , 255])] + #[case(0x00_FF_FF, [0 , 255, 255])] #[case(0xFF_FF_FF, [255, 255, 255])] - #[case(0x7B_2D_43, [123, 45, 67])] - #[case(0x59_65_70, [89, 101, 112])] + #[case(0x7B_2D_43, [123, 45 , 67 ])] + #[case(0x59_65_70, [89 , 101, 112])] fn hex_to_rgb(#[case] input: u32, #[case] expected: [u8; 3]) { assert_eq!(super::hex_to_rgb(input), expected); } #[rstest] - #[case([0, 0, 0])] - #[case([255, 0, 0])] - #[case([0, 255, 0])] - #[case([255, 255, 0])] - #[case([0, 0, 255])] - #[case([255, 0, 255])] - #[case([0, 255, 255])] + #[case([0 , 0 , 0 ])] + #[case([255, 0 , 0 ])] + #[case([0 , 255, 0 ])] + #[case([255, 255, 0 ])] + #[case([0 , 0 , 255])] + #[case([255, 0 , 255])] + #[case([0 , 255, 255])] #[case([255, 255, 255])] - #[case([123, 45, 67])] - #[case([89, 101, 112])] + #[case([123, 45 , 67 ])] + #[case([89 , 101, 112])] fn rgb_to_hex_to_rgb(#[case] input: [u8; 3]) { assert_eq!(super::hex_to_rgb(super::rgb_to_hex(input)), input); } diff --git a/lyra_ext/src/time.rs b/lyra_ext/src/time.rs index ec05d24..4e68a94 100644 --- a/lyra_ext/src/time.rs +++ b/lyra_ext/src/time.rs @@ -1,2 +1,3 @@ -pub mod rfc3339; +pub mod iso8601; +pub mod track_timestamp; pub mod unix; diff --git a/lyra_ext/src/time/iso8601.rs b/lyra_ext/src/time/iso8601.rs new file mode 100644 index 0000000..56e8345 --- /dev/null +++ b/lyra_ext/src/time/iso8601.rs @@ -0,0 +1,10 @@ +use time::{format_description::well_known::Iso8601, OffsetDateTime}; + +/// # Panics +/// This function panics when writing ISO 8601 datetime to string fails +#[must_use] +pub fn iso8601() -> String { + OffsetDateTime::now_utc() + .format(&Iso8601::DEFAULT) + .expect("writing iso8601 datetime to string should never fail") +} diff --git a/lyra_ext/src/time/rfc3339.rs b/lyra_ext/src/time/rfc3339.rs deleted file mode 100644 index 8412eb8..0000000 --- a/lyra_ext/src/time/rfc3339.rs +++ /dev/null @@ -1,10 +0,0 @@ -use time::{format_description::well_known::Rfc3339, OffsetDateTime}; - -/// # Panics -/// This function panics when writing RFC 3339 datetime to string fails -#[must_use] -pub fn rfc3339_time() -> String { - OffsetDateTime::now_utc() - .format(&Rfc3339) - .expect("writing rfc3339 datetime to string should never fail") -} diff --git a/lyra_ext/src/time/track_timestamp.rs b/lyra_ext/src/time/track_timestamp.rs new file mode 100644 index 0000000..085cd9d --- /dev/null +++ b/lyra_ext/src/time/track_timestamp.rs @@ -0,0 +1,460 @@ +#[cfg(test)] +use mock_instant::thread_local::Instant; +#[cfg(not(test))] +use std::time::Instant; + +use std::time::Duration; + +#[derive(Debug, Copy, Clone)] +enum Operation { + Pause(bool), + Speed(f64), + Seek(Duration), +} + +pub struct Data { + most_recent_operation: Instant, + most_recent_position: Instant, + paused: bool, + speed: f64, +} + +impl Data { + #[must_use] + const fn new(started: Instant) -> Self { + Self { + most_recent_operation: started, + most_recent_position: started, + paused: false, + speed: 1.0, + } + } + + fn reset(&mut self, started: Instant) { + self.most_recent_operation = started; + self.most_recent_position = started; + self.paused = false; + self.speed = 1.0; + } +} + +pub struct TrackTimestamp { + started: Instant, + data: Data, + last_operation: Instant, +} + +impl TrackTimestamp { + #[must_use] + pub fn new() -> Self { + let started = Instant::now(); + Self { + started, + data: Data::new(started), + last_operation: started, + } + } + + pub fn reset(&mut self) { + let started = Instant::now(); + self.started = started; + self.data.reset(started); + self.last_operation = started; + } + + #[must_use] + pub fn get(&self) -> Duration { + let data = &self.data; + let most_recent_duration = data + .most_recent_position + .saturating_duration_since(self.started); + if data.paused { + return most_recent_duration; + } + let elapsed = Instant::now().saturating_duration_since(data.most_recent_operation); + most_recent_duration + elapsed.mul_f64(data.speed) + } + + #[must_use] + pub const fn paused(&self) -> bool { + self.data.paused + } + + fn apply(&mut self, op: Operation) { + let now = Instant::now(); + let last_operation = &mut self.last_operation; + let since_prev = now - *last_operation; + *last_operation = now; + + let data = &mut self.data; + let most_recent_position = &mut data.most_recent_position; + let paused = &mut data.paused; + let speed = &mut data.speed; + + match op { + Operation::Pause(p) => { + *paused = p; + if p { + *most_recent_position += since_prev.mul_f64(*speed); + } + } + Operation::Speed(m) => { + if !*paused { + *most_recent_position += since_prev.mul_f64(*speed); + } + *speed = m; + } + Operation::Seek(d) => { + *most_recent_position = self.started + d; + } + } + + data.most_recent_operation += since_prev; + } + + pub fn set_pause(&mut self, state: bool) { + if state == self.data.paused { + return; // this is no-op + } + + self.apply(Operation::Pause(state)); + } + + pub fn set_speed(&mut self, multiplier: f64) { + self.apply(Operation::Speed(multiplier)); + } + + pub fn seek_to(&mut self, timestamp: Duration) { + self.apply(Operation::Seek(timestamp)); + } + + #[inline] + pub fn resume(&mut self) { + self.set_pause(false); + } + + #[inline] + pub fn pause(&mut self) { + self.set_pause(true); + } + + #[inline] + pub fn seek_forward(&mut self, duration: Duration) { + self.seek_to(self.get() + duration.mul_f64(self.data.speed)); + } + + #[inline] + pub fn seek_backward(&mut self, duration: Duration) { + self.seek_to(self.get().saturating_sub(duration.mul_f64(self.data.speed))); + } +} + +impl Default for TrackTimestamp { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod test { + use std::time::Duration; + + use mock_instant::thread_local::MockClock; + use rstest::{fixture, rstest}; + + use super::TrackTimestamp; + + const SECS_0: Duration = Duration::ZERO; + const SEC: Duration = Duration::from_secs(1); + + #[fixture] + fn stamp() -> TrackTimestamp { + TrackTimestamp::new() + } + + #[rstest] + fn then_get(stamp: TrackTimestamp) { + MockClock::advance(SEC); + assert_eq!(stamp.get(), SEC); + } + + #[rstest] + fn pause(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.pause(); + assert_eq!(stamp.get(), SEC); + + MockClock::advance(SEC); // +1 [ignored] + assert_eq!(stamp.get(), SEC); + } + + #[rstest] + fn pause_resume(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.pause(); + assert_eq!(stamp.get(), SEC); + + stamp.resume(); + assert_eq!(stamp.get(), SEC); + } + + #[rstest] + fn pause_then_resume(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.pause(); + assert_eq!(stamp.get(), SEC); + + MockClock::advance(SEC); // +1 [ignored] + assert_eq!(stamp.get(), SEC); + + stamp.resume(); + assert_eq!(stamp.get(), SEC); + + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), 2 * SEC); + } + + #[test] + fn seekf() { + MockClock::set_time(SEC); + let mut stamp = TrackTimestamp::new(); + + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.seek_forward(SEC); // +1 + assert_eq!(stamp.get(), 2 * SEC); + } + + #[test] + fn pause_seekf() { + MockClock::set_time(SEC); + let mut stamp = TrackTimestamp::new(); + + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.pause(); + assert_eq!(stamp.get(), SEC); + + stamp.seek_forward(SEC); // +1 + assert_eq!(stamp.get(), 2 * SEC); + } + + #[test] + fn pause_then_seekf() { + MockClock::set_time(SEC); + let mut stamp = TrackTimestamp::new(); + + stamp.pause(); + assert_eq!(stamp.get(), SECS_0); + + MockClock::advance(SEC); // +1 [ignored] + assert_eq!(stamp.get(), SECS_0); + + stamp.seek_forward(SEC); // +1 + assert_eq!(stamp.get(), SEC); + } + + #[rstest] + #[case(SEC, SEC)] + #[case(2 * SEC, SECS_0)] + #[case(3 * SEC, SECS_0)] + fn seekb(mut stamp: TrackTimestamp, #[case] input: Duration, #[case] expected: Duration) { + MockClock::advance(2 * SEC); // +2 + assert_eq!(stamp.get(), 2 * SEC); + + stamp.seek_backward(input); // -input, min 0 + assert_eq!(stamp.get(), expected); + } + + #[rstest] + #[case(SEC, SEC)] + #[case(2 * SEC, SECS_0)] + #[case(3 * SEC + SEC, SECS_0)] + fn pause_seekb(mut stamp: TrackTimestamp, #[case] input: Duration, #[case] expected: Duration) { + MockClock::advance(2 * SEC); // +2 + assert_eq!(stamp.get(), 2 * SEC); + + stamp.pause(); + assert_eq!(stamp.get(), 2 * SEC); + + stamp.seek_backward(input); // -input, min 0 + assert_eq!(stamp.get(), expected); + } + + #[rstest] + #[case(SEC, SEC)] + #[case(2 * SEC, SECS_0)] + #[case(3 * SEC + SEC, SECS_0)] + fn pause_then_seekb( + mut stamp: TrackTimestamp, + #[case] input: Duration, + #[case] expected: Duration, + ) { + MockClock::advance(2 * SEC); // +2 + assert_eq!(stamp.get(), 2 * SEC); + + stamp.pause(); + assert_eq!(stamp.get(), 2 * SEC); + + MockClock::advance(SEC); // +1 [ignored] + assert_eq!(stamp.get(), 2 * SEC); + + stamp.seek_backward(input); // -input, min 0 + assert_eq!(stamp.get(), expected); + } + + #[rstest] + fn speed(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.set_speed(2.); + assert_eq!(stamp.get(), SEC); + + MockClock::advance(SEC); // +(1 x2) = +2 + assert_eq!(stamp.get(), 3 * SEC); + } + + #[rstest] + fn pause_speed(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.pause(); + assert_eq!(stamp.get(), SEC); + + stamp.set_speed(2.); + assert_eq!(stamp.get(), SEC); + } + + #[rstest] + fn pause_then_speed(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.pause(); + assert_eq!(stamp.get(), SEC); + + stamp.set_speed(2.); + assert_eq!(stamp.get(), SEC); + + MockClock::advance(SEC); // ignored + assert_eq!(stamp.get(), SEC); + } + + #[rstest] + fn pause_speed_resume(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.pause(); + assert_eq!(stamp.get(), SEC); + + stamp.set_speed(2.); + assert_eq!(stamp.get(), SEC); + + MockClock::advance(SEC); // +(1 x2) [ignored] + assert_eq!(stamp.get(), SEC); + + stamp.resume(); + assert_eq!(stamp.get(), SEC); + + MockClock::advance(SEC); // +(1 x2) = +2 + assert_eq!(stamp.get(), 3 * SEC); + } + + #[rstest] + fn speed_seekf(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.set_speed(2.); + assert_eq!(stamp.get(), SEC); + + stamp.seek_forward(SEC); // +(1 x2) + assert_eq!(stamp.get(), 3 * SEC); + } + + #[rstest] + fn speed_seekf_pause(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.set_speed(2.); + assert_eq!(stamp.get(), SEC); + + stamp.seek_forward(SEC); // +(1 x2) + assert_eq!(stamp.get(), 3 * SEC); + + stamp.pause(); + assert_eq!(stamp.get(), 3 * SEC); + + MockClock::advance(SEC); + assert_eq!(stamp.get(), 3 * SEC); + } + + #[rstest] + fn speed_seekb(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.set_speed(2.); + assert_eq!(stamp.get(), SEC); + + stamp.seek_backward(SEC); // -(1 x2), min 0 + assert_eq!(stamp.get(), SECS_0); + } + + #[rstest] + fn speed_seekb_pause(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.set_speed(2.); + assert_eq!(stamp.get(), SEC); + + stamp.seek_backward(SEC); // -(1 x2), min 0 + assert_eq!(stamp.get(), SECS_0); + + stamp.pause(); + assert_eq!(stamp.get(), SECS_0); + + MockClock::advance(SEC); // +(1 x2) [ignored] + assert_eq!(stamp.get(), SECS_0); + } + + #[rstest] + fn pause_pause(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.pause(); + assert_eq!(stamp.get(), SEC); + + MockClock::advance(SEC); // +1 [ignored] + assert_eq!(stamp.get(), SEC); + + stamp.pause(); + assert_eq!(stamp.get(), SEC); + + MockClock::advance(SEC); // +1 [ignored] + assert_eq!(stamp.get(), SEC); + } + + #[rstest] + fn resume(mut stamp: TrackTimestamp) { + MockClock::advance(SEC); // +1 + assert_eq!(stamp.get(), SEC); + + stamp.resume(); + assert_eq!(stamp.get(), SEC); + } +} diff --git a/lyra_ext/src/time/unix.rs b/lyra_ext/src/time/unix.rs index 6aa9f9e..132d2a4 100644 --- a/lyra_ext/src/time/unix.rs +++ b/lyra_ext/src/time/unix.rs @@ -1,8 +1,10 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; +/// # Panics +/// if system clock went backwards #[must_use] -pub fn unix_time() -> Duration { +pub fn unix() -> Duration { SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap_or_default() + .unwrap_or_else(|_| panic!("system clock went backwards")) } diff --git a/lyra_proc/Cargo.toml b/lyra_proc/Cargo.toml index fd8a20c..765947f 100644 --- a/lyra_proc/Cargo.toml +++ b/lyra_proc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lyra_proc" -version = "0.7.1" +version = "0.8.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -12,10 +12,11 @@ proc-macro = true unsafe_code = "forbid" [lints.clippy] -enum_glob_use = "deny" -pedantic = "deny" -nursery = "deny" -unwrap_used = "deny" +enum_glob_use = "forbid" +unwrap_used = "forbid" +try_err = "forbid" +pedantic = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } [dependencies] syn = "2" diff --git a/lyra_proc/src/command.rs b/lyra_proc/src/command.rs index 4c58e1a..85f1eb4 100644 --- a/lyra_proc/src/command.rs +++ b/lyra_proc/src/command.rs @@ -18,7 +18,7 @@ fn unwrap(type_path: &TypePath, from: impl Into) -> Option<&Path> { } } -fn process( +fn declare_commands( fields: &FieldsUnnamed, v: &Variant, c: (QuoteTokenStream, QuoteTokenStream), @@ -69,7 +69,7 @@ fn process( } } -pub fn impl_lyra_command_group(input: &DeriveInput) -> TokenStream { +pub fn impl_bot_command_group(input: &DeriveInput) -> TokenStream { let name = &input.ident; let data = &input.data; @@ -77,8 +77,8 @@ pub fn impl_lyra_command_group(input: &DeriveInput) -> TokenStream { Data::Enum(data) => { data.variants .iter() - .fold((quote! {}, quote! {}), |c, v| match v.fields { - Fields::Unnamed(ref fields) => process(fields, v, c, name), + .fold((quote!(), quote!()), |c, v| match v.fields { + Fields::Unnamed(ref fields) => declare_commands(fields, v, c, name), _ => panic!("all fields must be unnamed"), }) } @@ -104,3 +104,54 @@ pub fn impl_lyra_command_group(input: &DeriveInput) -> TokenStream { } .into() } + +fn declare_autocompletes( + fields: &FieldsUnnamed, + v: &Variant, + sub_autocomplete_match: &QuoteTokenStream, +) -> QuoteTokenStream { + let sub_cmd = fields + .unnamed + .first() + .expect("variant must have exactly one unnamed field"); + let v_ident = &v.ident; + match sub_cmd.ty { + Type::Path(_) => { + quote! { + #sub_autocomplete_match + Self::#v_ident(sub_cmd) => sub_cmd.execute(ctx).await, + } + } + _ => panic!("the field must be a path"), + } +} + +pub fn impl_bot_autocomplete_group(input: &DeriveInput) -> TokenStream { + let name = &input.ident; + let data = &input.data; + + let sub_autocomplete_matches = match data { + Data::Enum(data) => data.variants.iter().fold(quote!(), |c, v| match v.fields { + Fields::Unnamed(ref fields) => declare_autocompletes(fields, v, &c), + _ => panic!("all fields must be unnamed"), + }), + _ => panic!("this can only be derived from an enum"), + }; + + let bot_autocomplete_path = + syn::parse_str::("crate::command::model::BotAutocomplete").expect("path is valid"); + let autocomplete_ctx_path = + syn::parse_str::("crate::command::model::AutocompleteCtx").expect("path is valid"); + let result_path = + syn::parse_str::("crate::error::command::AutocompleteResult").expect("path is valid"); + quote! { + impl #bot_autocomplete_path for #name { + async fn execute(self, ctx: #autocomplete_ctx_path) -> #result_path { + match self { + #sub_autocomplete_matches + } + } + } + } + .into() +} diff --git a/lyra_proc/src/config_access.rs b/lyra_proc/src/config_access.rs index 2ddb38e..c129c97 100644 --- a/lyra_proc/src/config_access.rs +++ b/lyra_proc/src/config_access.rs @@ -20,18 +20,12 @@ pub fn impl_view_access_ids(Args(categories): &Args) -> TokenStream { }) .map(|c| format!("{c}_access")); - let access_queries = column_names.clone().map(|t| { - format!( - r"--sql - SELECT id FROM {t} WHERE guild = $1; - ", - ) - }); + let access_queries = column_names + .clone() + .map(|t| format!("SELECT id FROM {t} WHERE guild = $1;",)); let mode_queries = format!( - r"--sql - SELECT {} FROM guild_configs WHERE id = $1 - ", + "SELECT {} FROM guild_configs WHERE id = $1", column_names.clone().join(", ") ); diff --git a/lyra_proc/src/lib.rs b/lyra_proc/src/lib.rs index cf664c5..0ff7e10 100644 --- a/lyra_proc/src/lib.rs +++ b/lyra_proc/src/lib.rs @@ -11,7 +11,14 @@ use proc_macro::TokenStream; pub fn bot_command_group(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); - command::impl_lyra_command_group(&input) + command::impl_bot_command_group(&input) +} + +#[proc_macro_derive(BotAutocompleteGroup)] +pub fn bot_autocomplete_group(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::DeriveInput); + + command::impl_bot_autocomplete_group(&input) } #[proc_macro] diff --git a/scripts/get-lavalink b/scripts/get-lavalink new file mode 100755 index 0000000..4226d02 --- /dev/null +++ b/scripts/get-lavalink @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +LAVALINK_DIR="$PWD/lavalink" + +URL="https://github.com/lavalink-devs/Lavalink/releases/download/4.0.7/Lavalink.jar" +FILE="${LAVALINK_DIR}/Lavalink.jar" +if [ -f $FILE ]; then + echo "File $FILE exists." +else + curl -o $FILE -L $URL +fi +