diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 943198092c4..00000000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -rustflags = ["-C", "target-cpu=haswell"] diff --git a/.github/labeler.yml b/.github/labeler.yml index 9bce978a73a..dbe91565dfd 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,7 +1,5 @@ ci: - .github/**/* -command_attr: - - command_attr/**/* examples: - examples/**/* builder: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65781d79d7e..7726b2d18fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,6 @@ jobs: - no cache - no gateway - unstable Discord API features - - simd-json include: - name: beta @@ -38,21 +37,19 @@ jobs: - name: no default features features: " " - name: no cache - features: builder client framework gateway model http standard_framework utils rustls_backend - - name: simd-json - features: default_no_backend rustls_backend simd_json + features: builder client framework gateway model http utils rustls_backend - name: no gateway features: model http rustls_backend - name: chrono features: chrono - name: unstable API + typesize - features: default unstable_discord_api typesize + features: default unstable typesize dont-test: true - name: builder without model features: builder dont-test: true - name: unstable Discord API (no default features) - features: unstable_discord_api + features: unstable dont-test: true steps: @@ -131,9 +128,6 @@ jobs: - name: Cache uses: Swatinem/rust-cache@v2 - - name: Remove cargo build config - run: rm .cargo/config.toml - - name: Build run: cargo build @@ -210,8 +204,7 @@ jobs: - name: Build docs run: | - cargo doc --no-deps --features collector,voice,unstable_discord_api - cargo doc --no-deps -p command_attr + cargo doc --no-deps --features collector,voice,unstable env: RUSTDOCFLAGS: -D rustdoc::broken_intra_doc_links @@ -241,32 +234,26 @@ jobs: - name: 'Check example 4' run: cargo check -p e04_message_builder - name: 'Check example 5' - run: cargo check -p e05_command_framework + run: cargo check -p e05_sample_bot_structure - name: 'Check example 6' - run: cargo check -p e06_sample_bot_structure + run: cargo check -p e06_env_logging - name: 'Check example 7' - run: cargo check -p e07_env_logging + run: cargo check -p e07_shard_manager - name: 'Check example 8' - run: cargo check -p e08_shard_manager - - name: 'Check example 9' - run: cargo check -p e09_create_message_builder + run: cargo check -p e08_create_message_builder + - name: 'Check example 09' + run: cargo check -p e09_collectors - name: 'Check example 10' - run: cargo check -p e10_collectors + run: cargo check -p e10_gateway_intents - name: 'Check example 11' - run: cargo check -p e11_gateway_intents + run: cargo check -p e11_global_data - name: 'Check example 12' - run: cargo check -p e12_global_data + run: cargo check -p e12_parallel_loops - name: 'Check example 13' - run: cargo check -p e13_parallel_loops + run: cargo check -p e13_sqlite_database - name: 'Check example 14' - run: cargo check -p e14_slash_commands + run: cargo check -p e14_message_components - name: 'Check example 15' - run: cargo check -p e15_simple_dashboard + run: cargo check -p e15_webhook - name: 'Check example 16' - run: cargo check -p e16_sqlite_database - - name: 'Check example 17' - run: cargo check -p e17_message_components - - name: 'Check example 18' - run: cargo check -p e18_webhook - - name: 'Check example 19' - run: cargo check -p e19_interactions_endpoint + run: cargo check -p e16_interactions_endpoint diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6e25f650fc0..1ed9d8ee9fe 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -31,7 +31,6 @@ jobs: RUSTDOCFLAGS: --cfg docsrs -D warnings run: | cargo doc --no-deps --features full - cargo doc --no-deps -p command_attr - name: Prepare docs shell: bash -e -O extglob {0} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d615bc4cc9..c7ca981d5bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,10 +63,9 @@ your code. ## Unsafe -Code that defines or uses `unsafe` functions must be reasoned with comments. -`unsafe` code can pose a potential for undefined behaviour related bugs and other -kinds of bugs to sprout if misused, weakening security. If you commit code containing -`unsafe`, you should confirm that its usage is necessary and correct. +Unsafe code is forbidden, and safe alternatives must be found. This can be mitigated by using +a third party crate to offload the burden of justifying the unsafe code, or finding a safe +alternative. # Comment / Documentation style diff --git a/Cargo.toml b/Cargo.toml index 30d19a3f600..6d365faab89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,39 +24,40 @@ async-trait = "0.1.74" tracing = { version = "0.1.40", features = ["log"] } serde = { version = "1.0.192", features = ["derive"] } url = { version = "2.4.1", features = ["serde"] } -tokio = { version = "1.34.0", features = ["fs", "macros", "rt", "sync", "time", "io-util"] } +tokio = { version = "1.34.0", features = ["macros", "rt", "sync", "time", "io-util"] } futures = { version = "0.3.29", default-features = false, features = ["std"] } dep_time = { version = "0.3.30", package = "time", features = ["formatting", "parsing", "serde-well-known"] } base64 = { version = "0.22.0" } secrecy = { version = "0.8.0", features = ["serde"] } +zeroize = { version = "1.7" } # Not used in serenity, but bumps the minimal version from secrecy arrayvec = { version = "0.7.4", features = ["serde"] } serde_cow = { version = "0.1.0" } +small-fixed-array = { version = "0.4", features = ["serde"] } +bool_to_bitflags = { version = "0.1.2" } +nonmax = { version = "0.5.5", features = ["serde"] } +strum = { version = "0.26", features = ["derive"] } +to-arraystring = "0.1.0" +extract_map = { version = "0.1.0", features = ["serde", "iter_mut"] } # Optional dependencies fxhash = { version = "0.2.1", optional = true } -simd-json = { version = "0.13.4", optional = true } -uwl = { version = "0.6.0", optional = true } -levenshtein = { version = "1.0.5", optional = true } chrono = { version = "0.4.31", default-features = false, features = ["clock", "serde"], optional = true } flate2 = { version = "1.0.28", optional = true } -reqwest = { version = "0.11.22", default-features = false, features = ["multipart", "stream"], optional = true } -static_assertions = { version = "1.1.0", optional = true } +reqwest = { version = "0.12.2", default-features = false, features = ["multipart", "stream", "json"], optional = true } tokio-tungstenite = { version = "0.21.0", optional = true } -typemap_rev = { version = "0.3.0", optional = true } bytes = { version = "1.5.0", optional = true } percent-encoding = { version = "2.3.0", optional = true } mini-moka = { version = "0.10.2", optional = true } mime_guess = { version = "2.0.4", optional = true } dashmap = { version = "5.5.3", features = ["serde"], optional = true } -parking_lot = { version = "0.12.1", optional = true } +parking_lot = { version = "0.12.1"} ed25519-dalek = { version = "2.0.0", optional = true } -typesize = { version = "0.1.2", optional = true, features = ["url", "time", "serde_json", "secrecy", "dashmap", "parking_lot", "details"] } +typesize = { version = "0.1.6", optional = true, features = ["url", "time", "serde_json", "secrecy", "dashmap", "parking_lot", "nonmax", "extract_map_01", "details"] } # serde feature only allows for serialisation, # Serenity workspace crates -command_attr = { version = "0.5.2", path = "./command_attr", optional = true } serenity-voice-model = { version = "0.2.0", path = "./voice-model", optional = true } [dev-dependencies.http_crate] -version = "0.2.11" +version = "1.1.0" package = "http" [features] @@ -74,38 +75,36 @@ default_no_backend = [ "gateway", "model", "http", - "standard_framework", "utils", ] # Enables builder structs to configure Discord HTTP requests. Without this feature, you have to # construct JSON manually at some places. -builder = [] +builder = ["tokio/fs"] # Enables the cache, which stores the data received from Discord gateway to provide access to # complete guild data, channels, users and more without needing HTTP requests. -cache = ["fxhash", "dashmap", "parking_lot"] +cache = ["fxhash", "dashmap"] # Enables collectors, a utility feature that lets you await interaction events in code with # zero setup, without needing to setup an InteractionCreate event listener. collector = ["gateway", "model"] # Wraps the gateway and http functionality into a single interface # TODO: should this require "gateway"? -client = ["http", "typemap_rev"] +client = ["http"] # Enables the Framework trait which is an abstraction for old-style text commands. framework = ["client", "model", "utils"] # Enables gateway support, which allows bots to listen for Discord events. gateway = ["flate2"] # Enables HTTP, which enables bots to execute actions on Discord. -http = ["mime_guess", "percent-encoding"] +http = ["dashmap", "mime_guess", "percent-encoding"] # Enables wrapper methods around HTTP requests on model types. # Requires "builder" to configure the requests and "http" to execute them. # Note: the model type definitions themselves are always active, regardless of this feature. # TODO: remove dependeny on utils feature model = ["builder", "http", "utils"] voice_model = ["serenity-voice-model"] -standard_framework = ["framework", "uwl", "levenshtein", "command_attr", "static_assertions", "parking_lot"] # Enables support for Discord API functionality that's not stable yet, as well as serenity APIs that # are allowed to change even in semver non-breaking updates. -unstable_discord_api = [] +unstable = [] # Enables some utility functions that can be useful for bot creators. utils = [] voice = ["client", "model"] @@ -117,16 +116,17 @@ chrono = ["dep:chrono", "typesize?/chrono"] # This enables all parts of the serenity codebase # (Note: all feature-gated APIs to be documented should have their features listed here!) -full = ["default", "collector", "unstable_discord_api", "voice", "voice_model", "interactions_endpoint"] - -# Enables simd accelerated parsing. -simd_json = ["simd-json", "typesize?/simd_json"] +# +# Unstable functionality should be gated under the `unstable` feature. +full = ["default", "collector", "voice", "voice_model", "interactions_endpoint"] # Enables temporary caching in functions that retrieve data via the HTTP API. temp_cache = ["cache", "mini-moka", "typesize?/mini_moka"] -# Removed feature (https://github.com/serenity-rs/serenity/pull/2246) -absolute_ratelimits = [] +typesize = ["dep:typesize", "small-fixed-array/typesize", "bool_to_bitflags/typesize"] + +# Enables compile-time heavy instrument macros from tracing +tracing_instrument = ["tracing/attributes"] # Backends to pick from: # - Rustls Backends @@ -147,3 +147,4 @@ native_tls_backend = [ [package.metadata.docs.rs] features = ["full"] rustdoc-args = ["--cfg", "docsrs"] + diff --git a/Makefile.toml b/Makefile.toml index 6765d0c3ec5..422774052b8 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -121,171 +121,152 @@ alias = "run_5" [tasks.run_5] command = "cargo" -args = ["make", "run_example_release", "e05_command_framework"] +args = ["make", "run_example_release", "e05_sample_bot_structure"] [tasks.build_5] command = "cargo" -args = ["make", "build_example_release", "e05_command_framework"] +args = ["make", "build_example_release", "e05_sample_bot_structure"] [tasks.dev_run_5] command = "cargo" -args = ["make", "run_example", "e05_command_framework"] +args = ["make", "run_example", "e05_sample_bot_structure"] [tasks.dev_build_5] command = "cargo" -args = ["make", "build_example", "e05_command_framework"] +args = ["make", "build_example", "e05_sample_bot_structure"] [tasks.6] alias = "run_6" [tasks.run_6] command = "cargo" -args = ["make", "run_example_release", "e06_sample_bot_structure"] +args = ["make", "run_example_release", "e06_env_logging"] [tasks.build_6] command = "cargo" -args = ["make", "build_example_release", "e06_sample_bot_structure"] +args = ["make", "build_example_release", "e06_env_logging"] [tasks.dev_run_6] command = "cargo" -args = ["make", "run_example", "e06_sample_bot_structure"] +args = ["make", "run_example", "e06_env_logging"] [tasks.dev_build_6] command = "cargo" -args = ["make", "build_example", "e06_sample_bot_structure"] +args = ["make", "build_example", "e06_env_logging"] [tasks.7] alias = "run_7" [tasks.run_7] command = "cargo" -args = ["make", "run_example_release", "e07_env_logging"] +args = ["make", "run_example_release", "e07_shard_manager"] [tasks.build_7] command = "cargo" -args = ["make", "build_example_release", "e07_env_logging"] +args = ["make", "build_example_release", "e07_shard_manager"] [tasks.dev_run_7] command = "cargo" -args = ["make", "run_example", "e07_env_logging"] +args = ["make", "run_example", "e07_shard_manager"] [tasks.dev_build_7] command = "cargo" -args = ["make", "build_example", "e07_env_logging"] +args = ["make", "build_example", "e07_shard_manager"] [tasks.8] alias = "run_8" [tasks.run_8] command = "cargo" -args = ["make", "run_example_release", "e08_shard_manager"] +args = ["make", "run_example_release", "e08_create_message_builder"] [tasks.build_8] command = "cargo" -args = ["make", "build_example_release", "e08_shard_manager"] +args = ["make", "build_example_release", "e08_create_message_builder"] [tasks.dev_run_8] command = "cargo" -args = ["make", "run_example", "e08_shard_manager"] +args = ["make", "run_example", "e08_create_message_builder"] [tasks.dev_build_8] command = "cargo" -args = ["make", "build_example", "e08_shard_manager"] +args = ["make", "build_example", "e08_create_message_builder"] -[tasks.9] -alias = "run_9" +[tasks.09] +alias = "run_09" -[tasks.run_9] +[tasks.run_09] command = "cargo" -args = ["make", "run_example_release", "e09_create_message_builder"] +args = ["make", "run_example_release", "e09_collectors"] -[tasks.build_9] +[tasks.build_09] command = "cargo" -args = ["make", "build_example_release", "e09_create_message_builder"] +args = ["make", "build_example_release", "e09_collectors"] -[tasks.dev_run_9] +[tasks.dev_run_09] command = "cargo" -args = ["make", "run_example", "e09_create_message_builder"] +args = ["make", "run_example", "e09_collectors"] -[tasks.dev_build_9] +[tasks.dev_build_09] command = "cargo" -args = ["make", "build_example", "e09_create_message_builder"] +args = ["make", "build_example", "e09_collectors"] [tasks.10] alias = "run_10" [tasks.run_10] command = "cargo" -args = ["make", "run_example_release", "e10_collectors"] +args = ["make", "run_example_release", "e10_gateway_intents"] [tasks.build_10] command = "cargo" -args = ["make", "build_example_release", "e10_collectors"] +args = ["make", "build_example_release", "e10_gateway_intents"] [tasks.dev_run_10] command = "cargo" -args = ["make", "run_example", "e10_collectors"] +args = ["make", "run_example", "e10_gateway_intents"] [tasks.dev_build_10] command = "cargo" -args = ["make", "build_example", "e10_collectors"] +args = ["make", "build_example", "e10_gateway_intents"] [tasks.11] alias = "run_11" [tasks.run_11] command = "cargo" -args = ["make", "run_example_release", "e11_gateway_intents"] +args = ["make", "run_example_release", "e11_global_data"] [tasks.build_11] command = "cargo" -args = ["make", "build_example_release", "e11_gateway_intents"] +args = ["make", "build_example_release", "e11_global_data"] [tasks.dev_run_11] command = "cargo" -args = ["make", "run_example", "e11_gateway_intents"] +args = ["make", "run_example", "e11_global_data"] [tasks.dev_build_11] command = "cargo" -args = ["make", "build_example", "e11_gateway_intents"] +args = ["make", "build_example", "e11_global_data"] [tasks.12] alias = "run_12" [tasks.run_12] command = "cargo" -args = ["make", "run_example_release", "e12_global_data"] +args = ["make", "run_example_release", "e12_parallel_loops"] [tasks.build_12] command = "cargo" -args = ["make", "build_example_release", "e12_global_data"] +args = ["make", "build_example_release", "e12_parallel_loops"] [tasks.dev_run_12] command = "cargo" -args = ["make", "run_example", "e12_global_data"] +args = ["make", "run_example", "e12_parallel_loops"] [tasks.dev_build_12] command = "cargo" -args = ["make", "build_example", "e12_global_data"] - -[tasks.13] -alias = "run_13" - -[tasks.run_13] -command = "cargo" -args = ["make", "run_example_release", "e13_parallel_loops"] - -[tasks.build_13] -command = "cargo" -args = ["make", "build_example_release", "e13_parallel_loops"] - -[tasks.dev_run_13] -command = "cargo" -args = ["make", "run_example", "e13_parallel_loops"] - -[tasks.dev_build_13] -command = "cargo" -args = ["make", "build_example", "e13_parallel_loops"] +args = ["make", "build_example", "e12_parallel_loops"] [tasks.14] alias = "run_14" diff --git a/README.md b/README.md index 85ecc849162..70b7e3fe351 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ struct Handler; #[async_trait] impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { + async fn message(&self, ctx: &Context, msg: &Message) { if msg.content == "!ping" { if let Err(why) = msg.channel_id.say(&ctx.http, "Pong!").await { println!("Error sending message: {why:?}"); @@ -71,8 +71,10 @@ async fn main() { | GatewayIntents::MESSAGE_CONTENT; // Create a new instance of the Client, logging in as a bot. - let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + let mut client = Client::builder(&token, intents) + .event_handler(Handler) + .await + .expect("Error creating client"); // Start listening for events by starting a single shard if let Err(why) = client.start().await { @@ -125,7 +127,7 @@ version = "0.12" ``` The default features are: `builder`, `cache`, `chrono`, `client`, `framework`, `gateway`, -`http`, `model`, `standard_framework`, `utils`, and `rustls_backend`. +`http`, `model`, `utils`, and `rustls_backend`. There are these alternative default features, they require to set `default-features = false`: @@ -151,15 +153,13 @@ the Discord gateway over a WebSocket client. enough level that optional parameters can be provided at will via a JsonMap. - **model**: Method implementations for models, acting as helper methods over the HTTP functions. -- **standard_framework**: A standard, default implementation of the Framework. **NOTE**: Deprecated as of v0.12.1. Using the [poise](https://github.com/serenity-rs/poise) framework is recommended instead. - **utils**: Utility functions for common use cases by users. - **voice**: Enables registering a voice plugin to the client, which will handle actual voice connections from Discord. [lavalink-rs][project:lavalink-rs] or [Songbird][project:songbird] are recommended voice plugins. - **default_native_tls**: Default features but using `native_tls_backend` instead of `rustls_backend`. - **tokio_task_builder**: Enables tokio's `tracing` feature and uses `tokio::task::Builder` to spawn tasks with names if `RUSTFLAGS="--cfg tokio_unstable"` is set. -- **unstable_discord_api**: Enables features of the Discord API that do not have a stable interface. The features might not have official documentation or are subject to change. -- **simd_json**: Enables SIMD accelerated JSON parsing and rendering for API calls, if supported on the target CPU architecture. +- **unstable**: Enables features of the Serenity and Discord API that do not have a stable interface. The features might not have official documentation and are subject to change without a breaking version bump. - **temp_cache**: Enables temporary caching in functions that retrieve data via the HTTP API. - **chrono**: Uses the `chrono` crate to represent timestamps. If disabled, the `time` crate is used instead. - **interactions_endpoint**: Enables tools related to Discord's Interactions Endpoint URL feature @@ -190,7 +190,6 @@ features = [ "gateway", "http", "model", - "standard_framework", "utils", "rustls_backend", ] diff --git a/benches/bench_args.rs b/benches/bench_args.rs deleted file mode 100644 index ff503d12b6e..00000000000 --- a/benches/bench_args.rs +++ /dev/null @@ -1,82 +0,0 @@ -#![feature(test)] - -#[cfg(test)] -mod benches { - extern crate test; - - use serenity::framework::standard::{Args, Delimiter}; - - use self::test::Bencher; - - #[bench] - fn single_with_one_delimiter(b: &mut Bencher) { - b.iter(|| { - let mut args = Args::new("1,2", &[Delimiter::Single(',')]); - args.single::().unwrap(); - }) - } - - #[bench] - fn single_with_one_delimiter_and_long_string(b: &mut Bencher) { - b.iter(|| { - let mut args = - Args::new("1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25", &[ - Delimiter::Single(','), - ]); - args.single::().unwrap(); - }) - } - - #[bench] - fn single_with_three_delimiters(b: &mut Bencher) { - b.iter(|| { - let mut args = Args::new("1,2 @3@4 5,", &[ - Delimiter::Single(','), - Delimiter::Single(' '), - Delimiter::Single('@'), - ]); - args.single::().unwrap(); - }) - } - - #[bench] - fn single_with_three_delimiters_and_long_string(b: &mut Bencher) { - b.iter(|| { - let mut args = - Args::new("1,2 @3@4 5,1,2 @3@4 5,1,2 @3@4 5,1,2 @3@4 5,1,2 @3@4 5,1,2 @3@4 5,", &[ - Delimiter::Single(','), - Delimiter::Single(' '), - Delimiter::Single('@'), - ]); - args.single::().unwrap(); - }) - } - - #[bench] - fn single_quoted_with_one_delimiter(b: &mut Bencher) { - b.iter(|| { - let mut args = Args::new(r#""1","2""#, &[Delimiter::Single(',')]); - args.single_quoted::().unwrap(); - }) - } - - #[bench] - fn iter_with_one_delimiter(b: &mut Bencher) { - b.iter(|| { - let mut args = Args::new("1,2,3,4,5,6,7,8,9,10", &[Delimiter::Single(',')]); - args.iter::().collect::, _>>().unwrap(); - }) - } - - #[bench] - fn iter_with_three_delimiters(b: &mut Bencher) { - b.iter(|| { - let mut args = Args::new("1-2<3,4,5,6,7<8,9,10", &[ - Delimiter::Single(','), - Delimiter::Single('-'), - Delimiter::Single('<'), - ]); - args.iter::().collect::, _>>().unwrap(); - }) - } -} diff --git a/command_attr/Cargo.toml b/command_attr/Cargo.toml deleted file mode 100644 index 343ae42c653..00000000000 --- a/command_attr/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "command_attr" -version = "0.5.2" -authors = ["Alex M. M. "] -edition = "2021" -description = "Procedural macros for command creation for the Serenity library." -license = "ISC" - -[lib] -proc-macro = true - -[dependencies] -quote = "^1.0" -syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] } -proc-macro2 = "^1.0.60" diff --git a/command_attr/src/attributes.rs b/command_attr/src/attributes.rs deleted file mode 100644 index 9b292be5df1..00000000000 --- a/command_attr/src/attributes.rs +++ /dev/null @@ -1,320 +0,0 @@ -use std::fmt::{self, Write}; - -use proc_macro2::Span; -use syn::parse::{Error, Result}; -use syn::spanned::Spanned; -use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path}; - -use crate::structures::{Checks, Colour, HelpBehaviour, OnlyIn, Permissions}; -use crate::util::{AsOption, LitExt}; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ValueKind { - // #[] - Name, - - // #[ = ] - Equals, - - // #[([, , , ...])] - List, - - // #[()] - SingleList, -} - -impl fmt::Display for ValueKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Name => f.pad("`#[]`"), - Self::Equals => f.pad("`#[ = ]`"), - Self::List => f.pad("`#[([, , , ...])]`"), - Self::SingleList => f.pad("`#[()]`"), - } - } -} - -fn to_ident(p: &Path) -> Result { - if p.segments.is_empty() { - return Err(Error::new(p.span(), "cannot convert an empty path to an identifier")); - } - - if p.segments.len() > 1 { - return Err(Error::new(p.span(), "the path must not have more than one segment")); - } - - if !p.segments[0].arguments.is_empty() { - return Err(Error::new(p.span(), "the singular path segment must not have any arguments")); - } - - Ok(p.segments[0].ident.clone()) -} - -#[derive(Debug)] -pub struct Values { - pub name: Ident, - pub literals: Vec, - pub kind: ValueKind, - pub span: Span, -} - -impl Values { - #[inline] - pub fn new(name: Ident, kind: ValueKind, literals: Vec, span: Span) -> Self { - Values { - name, - literals, - kind, - span, - } - } -} - -pub fn parse_values(attr: &Attribute) -> Result { - let meta = attr.parse_meta()?; - - match meta { - Meta::Path(path) => { - let name = to_ident(&path)?; - - Ok(Values::new(name, ValueKind::Name, Vec::new(), attr.span())) - }, - Meta::List(meta) => { - let name = to_ident(&meta.path)?; - let nested = meta.nested; - - if nested.is_empty() { - return Err(Error::new(attr.span(), "list cannot be empty")); - } - - let mut lits = Vec::with_capacity(nested.len()); - - for meta in nested { - match meta { - NestedMeta::Lit(l) => lits.push(l), - NestedMeta::Meta(m) => match m { - Meta::Path(path) => { - let i = to_ident(&path)?; - lits.push(Lit::Str(LitStr::new(&i.to_string(), i.span()))); - } - Meta::List(_) | Meta::NameValue(_) => { - return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level")) - } - }, - } - } - - let kind = if lits.len() == 1 { ValueKind::SingleList } else { ValueKind::List }; - - Ok(Values::new(name, kind, lits, attr.span())) - }, - Meta::NameValue(meta) => { - let name = to_ident(&meta.path)?; - let lit = meta.lit; - - Ok(Values::new(name, ValueKind::Equals, vec![lit], attr.span())) - }, - } -} - -#[derive(Debug, Clone)] -struct DisplaySlice<'a, T>(&'a [T]); - -impl<'a, T: fmt::Display> fmt::Display for DisplaySlice<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut iter = self.0.iter().enumerate(); - - match iter.next() { - None => f.write_str("nothing")?, - Some((idx, elem)) => { - write!(f, "{idx}: {elem}")?; - - for (idx, elem) in iter { - f.write_char('\n')?; - write!(f, "{idx}: {elem}")?; - } - }, - } - - Ok(()) - } -} - -#[inline] -fn is_form_acceptable(expect: &[ValueKind], kind: ValueKind) -> bool { - if expect.contains(&ValueKind::List) && kind == ValueKind::SingleList { - true - } else { - expect.contains(&kind) - } -} - -#[inline] -fn validate(values: &Values, forms: &[ValueKind]) -> Result<()> { - if !is_form_acceptable(forms, values.kind) { - return Err(Error::new( - values.span, - // Using the `_args` version here to avoid an allocation. - format_args!("the attribute must be in of these forms:\n{}", DisplaySlice(forms)), - )); - } - - Ok(()) -} - -#[inline] -pub fn parse(values: Values) -> Result { - T::parse(values) -} - -pub trait AttributeOption: Sized { - fn parse(values: Values) -> Result; -} - -impl AttributeOption for Vec { - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::List])?; - - Ok(values.literals.into_iter().map(|lit| lit.to_str()).collect()) - } -} - -impl AttributeOption for String { - #[inline] - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?; - - Ok(values.literals[0].to_str()) - } -} - -impl AttributeOption for bool { - #[inline] - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::Name, ValueKind::SingleList])?; - - Ok(values.literals.first().map_or(true, LitExt::to_bool)) - } -} - -impl AttributeOption for Ident { - #[inline] - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::SingleList])?; - - Ok(values.literals[0].to_ident()) - } -} - -impl AttributeOption for Vec { - #[inline] - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::List])?; - - Ok(values.literals.iter().map(LitExt::to_ident).collect()) - } -} - -impl AttributeOption for Option { - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList])?; - - Ok(values.literals.first().map(LitExt::to_str)) - } -} - -impl AttributeOption for OnlyIn { - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::SingleList])?; - - let lit = &values.literals[0]; - - OnlyIn::from_str(&lit.to_str()[..], lit.span()) - } -} - -impl AttributeOption for Colour { - fn parse(values: Values) -> Result { - let span = values.span; - let value = String::parse(values)?; - - Colour::from_str(&value) - .ok_or_else(|| Error::new(span, format_args!("invalid colour: \"{value}\""))) - } -} - -impl AttributeOption for HelpBehaviour { - fn parse(values: Values) -> Result { - let span = values.span; - let value = String::parse(values)?; - - HelpBehaviour::from_str(&value) - .ok_or_else(|| Error::new(span, format_args!("invalid help behaviour: \"{value}\""))) - } -} - -impl AttributeOption for Checks { - #[inline] - fn parse(values: Values) -> Result { - as AttributeOption>::parse(values).map(Checks) - } -} - -impl AttributeOption for Permissions { - fn parse(values: Values) -> Result { - let perms = as AttributeOption>::parse(values)?; - - let mut permissions = Permissions::default(); - for permission in perms { - let p = match Permissions::from_str(&permission.to_string()) { - Some(p) => p, - None => return Err(Error::new(permission.span(), "invalid permission")), - }; - - permissions.0 |= p.0; - } - - Ok(permissions) - } -} - -impl AttributeOption for AsOption { - #[inline] - fn parse(values: Values) -> Result { - Ok(AsOption(Some(T::parse(values)?))) - } -} - -macro_rules! attr_option_num { - ($($n:ty),*) => { - $( - impl AttributeOption for $n { - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::SingleList])?; - - Ok(match &values.literals[0] { - Lit::Int(l) => l.base10_parse::<$n>()?, - l => { - let s = l.to_str(); - // Use `as_str` to guide the compiler to use `&str`'s parse method. - // We don't want to use our `parse` method here (`impl AttributeOption for String`). - match s.as_str().parse::<$n>() { - Ok(n) => n, - Err(_) => return Err(Error::new(l.span(), "invalid integer")), - } - } - }) - } - } - - impl AttributeOption for Option<$n> { - #[inline] - fn parse(values: Values) -> Result { - <$n as AttributeOption>::parse(values).map(Some) - } - } - )* - } -} - -attr_option_num!(u16, u32, usize); diff --git a/command_attr/src/consts.rs b/command_attr/src/consts.rs deleted file mode 100644 index f579b5d5b59..00000000000 --- a/command_attr/src/consts.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod suffixes { - pub const COMMAND: &str = "COMMAND"; - pub const COMMAND_OPTIONS: &str = "COMMAND_OPTIONS"; - pub const HELP_OPTIONS: &str = "OPTIONS"; - pub const GROUP: &str = "GROUP"; - pub const GROUP_OPTIONS: &str = "GROUP_OPTIONS"; - pub const CHECK: &str = "CHECK"; -} - -pub use self::suffixes::*; diff --git a/command_attr/src/lib.rs b/command_attr/src/lib.rs deleted file mode 100644 index c9828edf99b..00000000000 --- a/command_attr/src/lib.rs +++ /dev/null @@ -1,958 +0,0 @@ -#![deny(rust_2018_idioms)] - -use proc_macro::TokenStream; -use proc_macro2::Span; -use quote::quote; -use syn::parse::{Error, Parse, ParseStream, Result}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{parse_macro_input, parse_quote, Ident, Lit, Token}; - -pub(crate) mod attributes; -pub(crate) mod consts; -pub(crate) mod structures; - -#[macro_use] -pub(crate) mod util; - -use attributes::*; -use consts::*; -use structures::*; -use util::*; - -macro_rules! match_options { - ($v:expr, $values:ident, $options:ident, $span:expr => [$($name:ident);*]) => { - match $v { - $( - stringify!($name) => $options.$name = propagate_err!($crate::attributes::parse($values)), - )* - _ => { - return Error::new($span, format_args!("invalid attribute: {:?}", $v)) - .to_compile_error() - .into(); - }, - } - }; -} - -#[rustfmt::skip] -/// The heart of the attribute-based framework. -/// -/// This is a function attribute macro. Using this on other Rust constructs won't work. -/// -/// ## Options -/// -/// To alter how the framework will interpret the command, you can provide options as attributes -/// following this `#[command]` macro. -/// -/// Each option has its own kind of data to stock and manipulate with. They're given to the option -/// either with the `#[option(...)]` or `#[option = ...]` syntaxes. If an option doesn't require -/// for any data to be supplied, then it's simply an empty `#[option]`. -/// -/// If the input to the option is malformed, the macro will give you can error, describing the -/// correct method for passing data, and what it should be. -/// -/// The list of available options, is, as follows: -/// -/// | Syntax | Description | Argument explanation | -/// | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -/// | `#[checks(identifiers)]` | Preconditions that must met before the command's execution. | `identifiers` is a comma separated list of identifiers referencing functions marked by the `#[check]` macro | -/// | `#[aliases(names)]` | Alternative names to refer to this command. | `names` is a comma separated list of desired aliases. | -/// | `#[description(desc)]`
`#[description = desc]` | The command's description or summary. | `desc` is a string describing the command. | -/// | `#[usage(use)]`
`#[usage = use]` | The command's intended usage. | `use` is a string stating the schema for the command's usage. | -/// | `#[example(ex)]`
`#[example = ex]` | An example of the command's usage. May be called multiple times to add many examples at once. | `ex` is a string | -/// | `#[delimiters(delims)]` | Argument delimiters specific to this command. Overrides the global list of delimiters in the framework. | `delims` is a comma separated list of strings | -/// | `#[min_args(min)]`
`#[max_args(max)]`
`#[num_args(min_and_max)]` | The expected length of arguments that the command must receive in order to function correctly. | `min`, `max` and `min_and_max` are 16-bit, unsigned integers. | -/// | `#[required_permissions(perms)]` | Set of permissions the user must possess.
In order for this attribute to work, "Presence Intent" and "Server Member Intent" options in bot application must be enabled and all intent flags must be enabled during client creation. | `perms` is a comma separated list of permission names.
These can be found at [Discord's official documentation](https://discord.com/developers/docs/topics/permissions). | -/// | `#[allowed_roles(roles)]` | Set of roles the user must possess. | `roles` is a comma separated list of role names. | -/// | `#[help_available]`
`#[help_available(b)]` | If the command should be displayed in the help message. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | -/// | `#[only_in(ctx)]` | Which environment the command can be executed in. | `ctx` is a string with the accepted values `guild`/`guilds` and `dm`/`dms` (Direct Message). | -/// | `#[bucket(name)]`
`#[bucket = name]` | What bucket will impact this command. | `name` is a string containing the bucket's name.
Refer to [the bucket example in the standard framework](https://docs.rs/serenity/*/serenity/framework/standard/struct.StandardFramework.html#method.bucket) for its usage. | -/// | `#[owners_only]`
`#[owners_only(b)]` | If this command is exclusive to owners. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | -/// | `#[owner_privilege]`
`#[owner_privilege(b)]` | If owners can bypass certain options. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | -/// | `#[sub_commands(commands)]` | The sub or children commands of this command. They are executed in the form: `this-command sub-command`. | `commands` is a comma separated list of identifiers referencing functions marked by the `#[command]` macro. | -/// -/// Documentation comments (`///`) applied onto the function are interpreted as sugar for the -/// `#[description]` option. When more than one application of the option is performed, the text is -/// delimited by newlines. This mimics the behaviour of regular doc-comments, which are sugar for -/// the `#[doc = "..."]` attribute. If you wish to join lines together, however, you have to end -/// the previous lines with `\$`. -/// -/// # Notes -/// -/// The name of the command is parsed from the applied function, or may be specified inside the -/// `#[command]` attribute, a lá `#[command("foobar")]`. -/// -/// This macro attribute generates static instances of `Command` and `CommandOptions`, conserving -/// the provided options. -/// -/// The names of the instances are all uppercased names of the command name. For example, with a -/// name of "foo": -/// ```rust,ignore -/// pub static FOO_COMMAND_OPTIONS: CommandOptions = CommandOptions { ... }; -/// pub static FOO_COMMAND: Command = Command { options: FOO_COMMAND_OPTIONS, ... }; -/// ``` -#[proc_macro_attribute] -pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { - let mut fun = parse_macro_input!(input as CommandFun); - - let _name = if attr.is_empty() { - fun.name.to_string_non_raw() - } else { - parse_macro_input!(attr as Lit).to_str() - }; - - let mut options = Options::new(); - - for attribute in &fun.attributes { - if is_rustfmt_or_clippy_attr(&attribute.path) { - continue; - } - - let span = attribute.span(); - let values = propagate_err!(parse_values(attribute)); - - let name = values.name.to_string(); - let name = &name[..]; - - match name { - "num_args" => { - let args = propagate_err!(u16::parse(values)); - - options.min_args = AsOption(Some(args)); - options.max_args = AsOption(Some(args)); - }, - "example" => { - options.examples.push(propagate_err!(attributes::parse(values))); - }, - "description" => { - let line: String = propagate_err!(attributes::parse(values)); - util::append_line(&mut options.description, line); - }, - _ => { - match_options!(name, values, options, span => [ - checks; - bucket; - aliases; - delimiters; - usage; - min_args; - max_args; - required_permissions; - allowed_roles; - help_available; - only_in; - owners_only; - owner_privilege; - sub_commands - ]); - }, - } - } - - let Options { - checks, - bucket, - aliases, - description, - delimiters, - usage, - examples, - min_args, - max_args, - allowed_roles, - required_permissions, - help_available, - only_in, - owners_only, - owner_privilege, - sub_commands, - } = options; - - propagate_err!(create_declaration_validations(&mut fun, DeclarFor::Command)); - - let res = parse_quote!(serenity::framework::standard::CommandResult); - create_return_type_validation(&mut fun, &res); - - let visibility = fun.visibility; - let name = fun.name.clone(); - let options = name.with_suffix(COMMAND_OPTIONS); - let sub_commands = sub_commands.into_iter().map(|i| i.with_suffix(COMMAND)).collect::>(); - let body = fun.body; - let ret = fun.ret; - - let n = name.with_suffix(COMMAND); - - let cooked = fun.cooked.clone(); - - let options_path = quote!(serenity::framework::standard::CommandOptions); - let command_path = quote!(serenity::framework::standard::Command); - - populate_fut_lifetimes_on_refs(&mut fun.args); - let args = fun.args; - - (quote! { - #(#cooked)* - #[allow(missing_docs)] - pub static #options: #options_path = #options_path { - checks: #checks, - bucket: #bucket, - names: &[#_name, #(#aliases),*], - desc: #description, - delimiters: &[#(#delimiters),*], - usage: #usage, - examples: &[#(#examples),*], - min_args: #min_args, - max_args: #max_args, - allowed_roles: &[#(#allowed_roles),*], - required_permissions: #required_permissions, - help_available: #help_available, - only_in: #only_in, - owners_only: #owners_only, - owner_privilege: #owner_privilege, - sub_commands: &[#(&#sub_commands),*], - }; - - #(#cooked)* - #[allow(missing_docs)] - pub static #n: #command_path = #command_path { - fun: #name, - options: &#options, - }; - - #(#cooked)* - #[allow(missing_docs)] - #visibility fn #name<'fut> (#(#args),*) -> std::pin::Pin + Send + 'fut>> { - Box::pin(async move { - let _output: #ret = { #(#body)* }; - #[allow(unreachable_code)] - _output - }) - } - }) - .into() -} - -#[rustfmt::skip] -/// A brother macro to [`command`], but for the help command. An interface for simple browsing of -/// all the available commands the bot provides, and reading through specific information regarding -/// a command. -/// -/// As such, the options here will pertain in the help command's **layout** than its functionality. -/// -/// ## Options -/// -/// | Syntax | Description | Argument explanation | -/// | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -/// | `#[suggestion_text(s)]`
`#[suggestion_text = s]` | When suggesting a command's name | `s` is a string | -/// | `#[no_help_available_text(s)]`
`#[no_help_available_text = s]` | When help is unavailable for a command. | `s` is a string | -/// | `#[usage_label(s)]`
`#[usage_label = s]` | How should the command be used. | `s` is a string | -/// | `#[usage_sample_label(s)]`
`#[usage_sample_label = s]` | Actual sample label. | `s` is a string | -/// | `#[ungrouped_label(s)]`
`#[ungrouped_label = s]` | Ungrouped commands label. | `s` is a string | -/// | `#[grouped_label(s)]`
`#[grouped_label = s]` | Grouped commands label. | `s` is a string | -/// | `#[sub_commands_label(s)]`
`#[sub_commands_label = s]` | Sub commands label. | `s` is a string | -/// | `#[description_label(s)]`
`#[description_label = s]` | Label at the start of the description. | `s` is a string | -/// | `#[aliases_label(s)]`
`#[aliases_label= s]` | Label for a command's aliases. | `s` is a string | -/// | `#[guild_only_text(s)]`
`#[guild_only_text = s]` | When a command is specific to guilds only. | `s` is a string | -/// | `#[checks_label(s)]`
`#[checks_label = s]` | The header text when showing checks in the help command. | `s` is a string | -/// | `#[dm_only_text(s)]`
`#[dm_only_text = s]` | When a command is specific to dms only. | `s` is a string | -/// | `#[dm_and_guild_text(s)]`
`#[dm_and_guild_text = s]` | When a command is usable in both guilds and dms. | `s` is a string | -/// | `#[available_text(s)]`
`#[available_text = s]` | When a command is available. | `s` is a string | -/// | `#[command_not_found_text(s)]`
`#[command_not_found_text = s]` | When a command wasn't found. | `s` is a string | -/// | `#[individual_command_tip(s)]`
`#[individual_command_tip = s]` | How the user should access a command's details. | `s` is a string | -/// | `#[strikethrough_commands_tip_in_dm(s)]`
`#[strikethrough_commands_tip_in_dm = s]` | Reasoning behind strikethrough-commands.
*Only used in dms.* | `s` is a string. If not provided, default text will be used instead. | -/// | `#[strikethrough_commands_tip_in_guild(s)]`
`#[strikethrough_commands_tip_in_guild = s]` | Reasoning behind strikethrough-commands.
*Only used in guilds.* | `s` is a string. If not provided, default text will be used instead. | -/// | `#[group_prefix(s)]`
`#[group_prefix = s]` | For introducing a group's prefix | `s` is a string | -/// | `#[lacking_role(s)]`
`#[lacking_role = s]` | If a user lacks required roles, this will treat how commands will be displayed. | `s` is a string. Accepts `strike` (strikethroughs), `hide` (will not be listed) or `nothing`(leave be). | -/// | `#[lacking_ownership(s)]`
`#[lacking_ownership = s]` | If a user lacks ownership, this will treat how these commands will be displayed. | `s` is a string. Accepts `strike` (strikethroughs), `hide` (will not be listed) or `nothing`(leave be). | -/// | `#[lacking_permissions(s)]`
`#[lacking_permissions = s]` | If a user lacks permissions, this will treat how commands will be displayed. | `s` is a string. Accepts `strike` (strikethroughs), `hide` (will not be listed) or `nothing`(leave be). | -/// | `#[lacking_conditions(s)]`
`#[lacking_conditions = s]` | If conditions (of a check) may be lacking by the user, this will treat how these commands will be displayed. | `s` is a string. Accepts `strike` (strikethroughs), `hide` (will not be listed) or `nothing`(leave be). | -/// | `#[wrong_channel(s)]`
`#[wrong_channel = s]` | If a user is using the help-command in a channel where a command is not available, this behaviour will be executed. | `s` is a string. Accepts `strike` (strikethroughs), `hide` (will not be listed) or `nothing`(leave be). | -/// | `#[embed_error_colour(n)]` | Colour that the help-embed will use upon an error. | `n` is a name to one of the provided constants of the `Colour` struct or an RGB value `#RRGGBB`. | -/// | `#[embed_success_colour(n)]` | Colour that the help-embed will use normally. | `n` is a name to one of the provided constants of the `Colour` struct or an RGB value `#RRGGBB`. | -/// | `#[max_levenshtein_distance(n)]` | How much should the help command search for a similar name.
Indicator for a nested guild. The prefix will be repeated based on what kind of level the item sits. A sub-group would be level two, a sub-sub-group would be level three. | `n` is a 64-bit, unsigned integer. | -/// | `#[indention_prefix(s)]`
`#[indention_prefix = s]` | The prefix used to express how deeply nested a command or group is. | `s` is a string | -/// -/// [`command`]: macro@command -#[proc_macro_attribute] -pub fn help(attr: TokenStream, input: TokenStream) -> TokenStream { - let mut fun = parse_macro_input!(input as CommandFun); - - let names = if attr.is_empty() { - vec!["help".to_string()] - } else { - struct Names(Vec); - - impl Parse for Names { - fn parse(input: ParseStream<'_>) -> Result { - let n: Punctuated = input.parse_terminated(Lit::parse)?; - Ok(Names(n.into_iter().map(|l| l.to_str()).collect())) - } - } - let Names(names) = parse_macro_input!(attr as Names); - - names - }; - - // Revert the change for the names of documentation attributes done when parsing the function - // input with `CommandFun`. - util::rename_attributes(&mut fun.attributes, "description", "doc"); - - // Additionally, place the documentation attributes to the `cooked` list to prevent the macro - // from rejecting them as invalid attributes. - { - let mut i = 0; - while i < fun.attributes.len() { - if fun.attributes[i].path.is_ident("doc") { - fun.cooked.push(fun.attributes.remove(i)); - continue; - } - - i += 1; - } - } - - let mut options = HelpOptions::default(); - - for attribute in &fun.attributes { - if is_rustfmt_or_clippy_attr(&attribute.path) { - continue; - } - - let span = attribute.span(); - let values = propagate_err!(parse_values(attribute)); - - let name = values.name.to_string(); - let name = &name[..]; - - match_options!(name, values, options, span => [ - suggestion_text; - no_help_available_text; - usage_label; - usage_sample_label; - ungrouped_label; - grouped_label; - aliases_label; - description_label; - guild_only_text; - checks_label; - dm_only_text; - dm_and_guild_text; - available_text; - command_not_found_text; - individual_command_tip; - group_prefix; - lacking_role; - lacking_permissions; - lacking_ownership; - lacking_conditions; - wrong_channel; - embed_error_colour; - embed_success_colour; - strikethrough_commands_tip_in_dm; - strikethrough_commands_tip_in_guild; - sub_commands_label; - max_levenshtein_distance; - indention_prefix - ]); - } - - fn produce_strike_text(options: &HelpOptions, dm_or_guild: &str) -> Option { - use std::fmt::Write; - - let mut strike_text = - String::from("~~`Strikethrough commands`~~ are unavailable because they"); - let mut is_any_option_strike = false; - - let mut concat_with_comma = if let HelpBehaviour::Strike = options.lacking_permissions { - is_any_option_strike = true; - strike_text.push_str(" require permissions"); - - true - } else { - false - }; - - if let HelpBehaviour::Strike = options.lacking_role { - is_any_option_strike = true; - - if concat_with_comma { - strike_text.push_str(", require a specific role"); - } else { - strike_text.push_str(" require a specific role"); - concat_with_comma = true; - } - } - - if let HelpBehaviour::Strike = options.lacking_conditions { - is_any_option_strike = true; - - if concat_with_comma { - strike_text.push_str(", require certain conditions"); - } else { - strike_text.push_str(" require certain conditions"); - concat_with_comma = true; - } - } - - if let HelpBehaviour::Strike = options.wrong_channel { - is_any_option_strike = true; - - if concat_with_comma { - let _ = write!(strike_text, ", or are limited to {dm_or_guild}"); - } else { - let _ = write!(strike_text, " are limited to {dm_or_guild}"); - } - } - - strike_text.push('.'); - is_any_option_strike.then_some(strike_text) - } - - if options.strikethrough_commands_tip_in_dm.is_none() { - options.strikethrough_commands_tip_in_dm = produce_strike_text(&options, "server messages"); - } - - if options.strikethrough_commands_tip_in_guild.is_none() { - options.strikethrough_commands_tip_in_guild = - produce_strike_text(&options, "direct messages"); - } - - let HelpOptions { - suggestion_text, - no_help_available_text, - usage_label, - usage_sample_label, - ungrouped_label, - grouped_label, - aliases_label, - description_label, - guild_only_text, - checks_label, - sub_commands_label, - dm_only_text, - dm_and_guild_text, - available_text, - command_not_found_text, - individual_command_tip, - group_prefix, - strikethrough_commands_tip_in_dm, - strikethrough_commands_tip_in_guild, - lacking_role, - lacking_permissions, - lacking_ownership, - lacking_conditions, - wrong_channel, - embed_error_colour, - embed_success_colour, - max_levenshtein_distance, - indention_prefix, - } = options; - - let strikethrough_commands_tip_in_dm = AsOption(strikethrough_commands_tip_in_dm); - let strikethrough_commands_tip_in_guild = AsOption(strikethrough_commands_tip_in_guild); - - propagate_err!(create_declaration_validations(&mut fun, DeclarFor::Help)); - - let res = parse_quote!(serenity::framework::standard::CommandResult); - create_return_type_validation(&mut fun, &res); - - let options = fun.name.with_suffix(HELP_OPTIONS); - - let n = fun.name.to_uppercase(); - let nn = fun.name.clone(); - - let cooked = fun.cooked.clone(); - - let options_path = quote!(serenity::framework::standard::HelpOptions); - let command_path = quote!(serenity::framework::standard::HelpCommand); - - let body = fun.body; - let ret = fun.ret; - populate_fut_lifetimes_on_refs(&mut fun.args); - let args = fun.args; - - (quote! { - #(#cooked)* - #[allow(missing_docs)] - pub static #options: #options_path = #options_path { - names: &[#(#names),*], - suggestion_text: #suggestion_text, - no_help_available_text: #no_help_available_text, - usage_label: #usage_label, - usage_sample_label: #usage_sample_label, - ungrouped_label: #ungrouped_label, - grouped_label: #grouped_label, - aliases_label: #aliases_label, - description_label: #description_label, - guild_only_text: #guild_only_text, - checks_label: #checks_label, - sub_commands_label: #sub_commands_label, - dm_only_text: #dm_only_text, - dm_and_guild_text: #dm_and_guild_text, - available_text: #available_text, - command_not_found_text: #command_not_found_text, - individual_command_tip: #individual_command_tip, - group_prefix: #group_prefix, - strikethrough_commands_tip_in_dm: #strikethrough_commands_tip_in_dm, - strikethrough_commands_tip_in_guild: #strikethrough_commands_tip_in_guild, - lacking_role: #lacking_role, - lacking_permissions: #lacking_permissions, - lacking_ownership: #lacking_ownership, - lacking_conditions: #lacking_conditions, - wrong_channel: #wrong_channel, - embed_error_colour: #embed_error_colour, - embed_success_colour: #embed_success_colour, - max_levenshtein_distance: #max_levenshtein_distance, - indention_prefix: #indention_prefix, - }; - - #(#cooked)* - #[allow(missing_docs)] - pub static #n: #command_path = #command_path { - fun: #nn, - options: &#options, - }; - - #(#cooked)* - #[allow(missing_docs)] - pub fn #nn<'fut>(#(#args),*) -> std::pin::Pin + Send + 'fut>> { - Box::pin(async move { - let _output: #ret = { #(#body)* }; - #[allow(unreachable_code)] - _output - }) - } - }) - .into() -} - -#[rustfmt::skip] -/// Create a grouping of commands. -/// -/// It is a prerequisite for all commands to be assigned under a common group, before they may be -/// executed by a user. -/// -/// A group might have one or more *prefixes* set. This will necessitate for one of the prefixes to -/// appear before the group's command. For example, for a general prefix `!`, a group prefix `foo` -/// and a command `bar`, the invocation would be `!foo bar`. -/// -/// It might have some options apply to *all* of its commands. E.g. guild or dm only. -/// -/// It may even couple other groups as well. -/// -/// This group macro purports all of the said purposes above, applied onto a `struct`: -/// -/// ```rust,ignore -/// use command_attr::{command, group}; -/// -/// # type CommandResult = (); -/// -/// #[command] -/// fn bar() -> CommandResult { -/// println!("baz"); -/// -/// Ok(()) -/// } -/// -/// #[command] -/// fn answer_to_life() -> CommandResult { -/// println!("42"); -/// -/// Ok(()) -/// } -/// -/// #[group] -/// // All sub-groups must own at least one prefix. -/// #[prefix = "baz"] -/// #[commands(answer_to_life)] -/// struct Baz; -/// -/// #[group] -/// #[commands(bar)] -/// // Case does not matter; the names will be all uppercased. -/// #[sub_groups(baz)] -/// struct Foo; -/// ``` -/// -/// ## Options -/// -/// These appear after `#[group]` as a series of attributes: -/// -/// | Syntax | Description | Argument explanation | -/// | ----------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -/// | `#[commands(commands)]` | Set of commands belonging to this group. | `commands` is a comma separated list of identifiers referencing functions marked by the `#[command]` macro | -/// | `#[sub_groups(subs)]` | Set of sub groups belonging to this group. | `subs` is a comma separated list of identifiers referencing structs marked by the `#[group]` macro | -/// | `#[prefixes(prefs)]` | Text that must appear before an invocation of a command of this group may occur. | `prefs` is a comma separated list of strings | -/// | `#[prefix(pref)]` | Assign just a single prefix. | `pref` is a string | -/// | `#[allowed_roles(roles)]` | Set of roles the user must possess | `roles` is a comma separated list of strings containing role names | -/// | `#[only_in(ctx)]` | Which environment the command can be executed in. | `ctx` is a string with the accepted values `guild`/`guilds` and `dm`/ `dms` (Direct Message). | -/// | `#[owners_only]`
`#[owners_only(b)]` | If this command is exclusive to owners. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | -/// | `#[owner_privilege]`
`#[owner_privilege(b)]` | If owners can bypass certain options. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | -/// | `#[help_available]`
`#[help_available(b)]` | If the group should be displayed in the help message. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | -/// | `#[checks(identifiers)]` | Preconditions that must met before the command's execution. | `identifiers` is a comma separated list of identifiers referencing functions marked by the `#[check]` macro | -/// | `#[required_permissions(perms)]` | Set of permissions the user must possess.
In order for this attribute to work, "Presence Intent" and "Server Member Intent" options in bot application must be enabled and all intent flags must be enabled during client creation. | `perms` is a comma separated list of permission names.
These can be found at [Discord's official documentation](https://discord.com/developers/docs/topics/permissions). | -/// | `#[default_command(cmd)]` | A command to execute if none of the group's prefixes are given. | `cmd` is an identifier referencing a function marked by the `#[command]` macro | -/// | `#[description(desc)]`
`#[description = desc]` | The group's description or summary. | `desc` is a string describing the group. | -/// | `#[summary(desc)]`
`#[summary = desc]` | A summary group description displayed when shown multiple groups. | `desc` is a string summaryly describing the group. | -/// -/// Documentation comments (`///`) applied onto the struct are interpreted as sugar for the -/// `#[description]` option. When more than one application of the option is performed, the text is -/// delimited by newlines. This mimics the behaviour of regular doc-comments, which are sugar for -/// the `#[doc = "..."]` attribute. If you wish to join lines together, however, you have to end -/// the previous lines with `\$`. -/// -/// Similarly to [`command`], this macro generates static instances of the group and its options. -/// The identifiers of these instances are based off the name of the struct to differentiate this -/// group from others. This name is given as the default value of the group's `name` field, used in -/// the help command for display and browsing of the group. It may also be passed as an argument to -/// the macro. For example: `#[group("Banana Phone")]`. -/// -/// [`command`]: macro@command - -#[proc_macro_attribute] -pub fn group(attr: TokenStream, input: TokenStream) -> TokenStream { - let group = parse_macro_input!(input as GroupStruct); - - let name = if attr.is_empty() { - group.name.to_string_non_raw() - } else { - parse_macro_input!(attr as Lit).to_str() - }; - - let mut options = GroupOptions::new(); - - for attribute in &group.attributes { - if is_rustfmt_or_clippy_attr(&attribute.path) { - continue; - } - - let span = attribute.span(); - let values = propagate_err!(parse_values(attribute)); - - let name = values.name.to_string(); - let name = &name[..]; - - match name { - "prefix" => { - options.prefixes = vec![propagate_err!(attributes::parse(values))]; - }, - "description" => { - let line: String = propagate_err!(attributes::parse(values)); - util::append_line(&mut options.description, line); - }, - "summary" => { - let arg: String = propagate_err!(attributes::parse(values)); - - if let Some(desc) = &mut options.summary.0 { - use std::fmt::Write; - - let _ = write!(desc, "\n{}", arg.trim_matches(' ')); - } else { - options.summary = AsOption(Some(arg)); - } - }, - _ => match_options!(name, values, options, span => [ - prefixes; - only_in; - owners_only; - owner_privilege; - help_available; - allowed_roles; - required_permissions; - checks; - default_command; - commands; - sub_groups - ]), - } - } - - let GroupOptions { - prefixes, - only_in, - owners_only, - owner_privilege, - help_available, - allowed_roles, - required_permissions, - checks, - default_command, - description, - summary, - commands, - sub_groups, - } = options; - - let cooked = group.cooked.clone(); - - let n = group.name.with_suffix(GROUP); - - let default_command = default_command.map(|ident| { - let i = ident.with_suffix(COMMAND); - - quote!(&#i) - }); - - let commands = commands.into_iter().map(|c| c.with_suffix(COMMAND)).collect::>(); - - let sub_groups = sub_groups.into_iter().map(|c| c.with_suffix(GROUP)).collect::>(); - - let options = group.name.with_suffix(GROUP_OPTIONS); - let options_path = quote!(serenity::framework::standard::GroupOptions); - let group_path = quote!(serenity::framework::standard::CommandGroup); - - (quote! { - #(#cooked)* - #[allow(missing_docs)] - pub static #options: #options_path = #options_path { - prefixes: &[#(#prefixes),*], - only_in: #only_in, - owners_only: #owners_only, - owner_privilege: #owner_privilege, - help_available: #help_available, - allowed_roles: &[#(#allowed_roles),*], - required_permissions: #required_permissions, - checks: #checks, - default_command: #default_command, - description: #description, - summary: #summary, - commands: &[#(&#commands),*], - sub_groups: &[#(&#sub_groups),*], - }; - - #(#cooked)* - #[allow(missing_docs)] - pub static #n: #group_path = #group_path { - name: #name, - options: &#options, - }; - - #group - }) - .into() -} - -#[rustfmt::skip] -/// A macro for marking a function as a condition checker to groups and commands. -/// -/// ## Options -/// -/// | Syntax | Description | Argument explanation | -/// | --------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | -/// | `#[name(s)]`
`#[name = s]` | How the check should be listed in help. | `s` is a string. If this option isn't provided, the value is assumed to be `""`. | -/// | `#[display_in_help]`
`#[display_in_help(b)]` | If the check should be listed in help. Has no effect on `check_in_help`. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | -/// | `#[check_in_help]`
`#[check_in_help(b)]` | If the check should be evaluated in help. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | -#[proc_macro_attribute] -pub fn check(_attr: TokenStream, input: TokenStream) -> TokenStream { - let mut fun = parse_macro_input!(input as CommandFun); - - let mut name = "".to_string(); - let mut display_in_help = true; - let mut check_in_help = true; - - for attribute in &fun.attributes { - if is_rustfmt_or_clippy_attr(&attribute.path) { - continue; - } - - let span = attribute.span(); - let values = propagate_err!(parse_values(attribute)); - - let n = values.name.to_string(); - let n = &n[..]; - - match n { - "name" => name = propagate_err!(attributes::parse(values)), - "display_in_help" => display_in_help = propagate_err!(attributes::parse(values)), - "check_in_help" => check_in_help = propagate_err!(attributes::parse(values)), - _ => { - return Error::new(span, format_args!("invalid attribute: {n:?}")) - .to_compile_error() - .into(); - }, - } - } - - propagate_err!(create_declaration_validations(&mut fun, DeclarFor::Check)); - - let res = parse_quote!(std::result::Result<(), serenity::framework::standard::Reason>); - create_return_type_validation(&mut fun, &res); - - let n = fun.name.clone(); - let n2 = name.clone(); - let visibility = fun.visibility; - let name = if name == "" { fun.name.clone() } else { Ident::new(&name, Span::call_site()) }; - let name = name.with_suffix(CHECK); - - let check = quote!(serenity::framework::standard::Check); - let cooked = fun.cooked; - let body = fun.body; - let ret = fun.ret; - populate_fut_lifetimes_on_refs(&mut fun.args); - let args = fun.args; - - (quote! { - #[allow(missing_docs)] - pub static #name: #check = #check { - name: #n2, - function: #n, - display_in_help: #display_in_help, - check_in_help: #check_in_help - }; - - #(#cooked)* - #[allow(missing_docs)] - #visibility fn #n<'fut>(#(#args),*) -> std::pin::Pin + Send + 'fut>> { - Box::pin(async move { - let _output: #ret = { #(#body)* }; - #[allow(unreachable_code)] - _output - }) - } - }) - .into() -} - -/// A macro that transforms `async` functions (and closures) into plain functions, whose return -/// type is a boxed [`Future`]. -/// -/// # Transformation -/// -/// The macro transforms an `async` function, which may look like this: -/// -/// ```rust,no_run -/// async fn foo(n: i32) -> i32 { -/// n + 4 -/// } -/// ``` -/// -/// into this (some details omitted): -/// -/// ```rust,no_run -/// use std::future::Future; -/// use std::pin::Pin; -/// -/// fn foo(n: i32) -> Pin>> { -/// Box::pin(async move { n + 4 }) -/// } -/// ``` -/// -/// This transformation also applies to closures, which are converted more simply. For instance, -/// this closure: -/// -/// ```rust,no_run -/// # #![feature(async_closure)] -/// # -/// async move |x: i32| x * 2 + 4 -/// # ; -/// ``` -/// -/// is changed to: -/// -/// ```rust,no_run -/// |x: i32| Box::pin(async move { x * 2 + 4 }) -/// # ; -/// ``` -/// -/// ## How references are handled -/// -/// When a function contains references, their lifetimes are constrained to the returned -/// [`Future`]. If the above `foo` function had `&i32` as a parameter, the transformation would be -/// instead this: -/// -/// ```rust,no_run -/// use std::future::Future; -/// use std::pin::Pin; -/// -/// fn foo<'fut>(n: &'fut i32) -> Pin + 'fut>> { -/// Box::pin(async move { *n + 4 }) -/// } -/// ``` -/// -/// Explicitly specifying lifetimes (in the parameters or in the return type) or complex usage of -/// lifetimes (e.g. `'a: 'b`) is not supported. -/// -/// # Necessity for the macro -/// -/// The macro performs the transformation to permit the framework to store and invoke the functions. -/// -/// Functions marked with the `async` keyword will wrap their return type with the [`Future`] -/// trait, which a state-machine generated by the compiler for the function will implement. This -/// complicates matters for the framework, as [`Future`] is a trait. Depending on a type that -/// implements a trait is done with two methods in Rust: -/// -/// 1. static dispatch - generics -/// 2. dynamic dispatch - trait objects -/// -/// First method is infeasible for the framework. Typically, the framework will contain a plethora -/// of different commands that will be stored in a single list. And due to the nature of generics, -/// generic types can only resolve to a single concrete type. If commands had a generic type for -/// their function's return type, the framework would be unable to store commands, as only a single -/// [`Future`] type from one of the commands would get resolved, preventing other commands from -/// being stored. -/// -/// Second method involves heap allocations, but is the only working solution. If a trait is -/// object-safe (which [`Future`] is), the compiler can generate a table of function pointers -/// (a vtable) that correspond to certain implementations of the trait. This allows to decide which -/// implementation to use at runtime. Thus, we can use the interface for the [`Future`] trait, and -/// avoid depending on the underlying value (such as its size). To opt-in to dynamic dispatch, -/// trait objects must be used with a pointer, like references (`&` and `&mut`) or `Box`. The -/// latter is what's used by the macro, as the ownership of the value (the state-machine) must be -/// given to the caller, the framework in this case. -/// -/// The macro exists to retain the normal syntax of `async` functions (and closures), while -/// granting the user the ability to pass those functions to the framework, like command functions -/// and hooks (`before`, `after`, `on_dispatch_error`, etc.). -/// -/// # Notes -/// -/// If applying the macro on an `async` closure, you will need to enable the `async_closure` -/// feature. Inputs to procedural macro attributes must be valid Rust code, and `async` closures -/// are not stable yet. -/// -/// [`Future`]: std::future::Future -#[proc_macro_attribute] -pub fn hook(_attr: TokenStream, input: TokenStream) -> TokenStream { - let hook = parse_macro_input!(input as Hook); - - match hook { - Hook::Function(mut fun) => { - let attributes = fun.attributes; - let visibility = fun.visibility; - let fun_name = fun.name; - let body = fun.body; - let ret = fun.ret; - - populate_fut_lifetimes_on_refs(&mut fun.args); - let args = fun.args; - - (quote! { - #(#attributes)* - #[allow(missing_docs)] - #visibility fn #fun_name<'fut>(#(#args),*) -> std::pin::Pin + Send + 'fut>> { - Box::pin(async move { - let _output: #ret = { #(#body)* }; - #[allow(unreachable_code)] - _output - }) - } - }) - .into() - }, - Hook::Closure(closure) => { - let attributes = closure.attributes; - let args = closure.args; - let ret = closure.ret; - let body = closure.body; - - (quote! { - #(#attributes)* - |#args| #ret { - Box::pin(async move { #body }) - } - }) - .into() - }, - } -} diff --git a/command_attr/src/structures.rs b/command_attr/src/structures.rs deleted file mode 100644 index bab59084704..00000000000 --- a/command_attr/src/structures.rs +++ /dev/null @@ -1,645 +0,0 @@ -use std::str::FromStr; - -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{quote, ToTokens}; -use syn::parse::{Error, Parse, ParseStream, Result}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{ - braced, - Attribute, - Block, - Expr, - ExprClosure, - FnArg, - Ident, - Pat, - Path, - ReturnType, - Stmt, - Token, - Type, - Visibility, -}; - -use crate::consts::CHECK; -use crate::util::{self, Argument, AsOption, IdentExt2, Parenthesised}; - -#[derive(Debug, Default, Eq, PartialEq)] -pub enum OnlyIn { - Dm, - Guild, - #[default] - None, -} - -impl OnlyIn { - #[inline] - pub fn from_str(s: &str, span: Span) -> Result { - match s { - "guilds" | "guild" => Ok(OnlyIn::Guild), - "dms" | "dm" => Ok(OnlyIn::Dm), - _ => Err(Error::new(span, "invalid restriction type")), - } - } -} - -impl ToTokens for OnlyIn { - fn to_tokens(&self, stream: &mut TokenStream2) { - let only_in_path = quote!(serenity::framework::standard::OnlyIn); - match self { - Self::Dm => stream.extend(quote!(#only_in_path::Dm)), - Self::Guild => stream.extend(quote!(#only_in_path::Guild)), - Self::None => stream.extend(quote!(#only_in_path::None)), - } - } -} - -fn parse_argument(arg: FnArg) -> Result { - match arg { - FnArg::Typed(typed) => { - let pat = typed.pat; - let kind = typed.ty; - - match *pat { - Pat::Ident(id) => { - let name = id.ident; - let mutable = id.mutability; - - Ok(Argument { - mutable, - name, - kind: *kind, - }) - }, - Pat::Wild(wild) => { - let token = wild.underscore_token; - - let name = Ident::new("_", token.spans[0]); - - Ok(Argument { - mutable: None, - name, - kind: *kind, - }) - }, - _ => Err(Error::new(pat.span(), format_args!("unsupported pattern: {pat:?}"))), - } - }, - FnArg::Receiver(_) => { - Err(Error::new(arg.span(), format_args!("`self` arguments are prohibited: {arg:?}"))) - }, - } -} - -/// Test if the attribute is cooked. -fn is_cooked(attr: &Attribute) -> bool { - const COOKED_ATTRIBUTE_NAMES: &[&str] = - &["cfg", "cfg_attr", "derive", "inline", "allow", "warn", "deny", "forbid"]; - - COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n)) -} - -pub fn is_rustfmt_or_clippy_attr(path: &Path) -> bool { - path.segments.first().map_or(false, |s| s.ident == "rustfmt" || s.ident == "clippy") -} - -/// Removes cooked attributes from a vector of attributes. Uncooked attributes are left in the -/// vector. -/// -/// # Return -/// -/// Returns a vector of cooked attributes that have been removed from the input vector. -fn remove_cooked(attrs: &mut Vec) -> Vec { - let mut cooked = Vec::new(); - - // FIXME: Replace with `Vec::drain_filter` once it is stable. - let mut i = 0; - while i < attrs.len() { - if !is_cooked(&attrs[i]) && !is_rustfmt_or_clippy_attr(&attrs[i].path) { - i += 1; - continue; - } - - cooked.push(attrs.remove(i)); - } - - cooked -} - -#[derive(Debug)] -pub struct CommandFun { - /// `#[...]`-style attributes. - pub attributes: Vec, - /// Populated cooked attributes. These are attributes outside of the realm of this crate's - /// procedural macros and will appear in generated output. - pub cooked: Vec, - pub visibility: Visibility, - pub name: Ident, - pub args: Vec, - pub ret: Type, - pub body: Vec, -} - -impl Parse for CommandFun { - fn parse(input: ParseStream<'_>) -> Result { - let mut attributes = input.call(Attribute::parse_outer)?; - - // Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`. - util::rename_attributes(&mut attributes, "doc", "description"); - - let cooked = remove_cooked(&mut attributes); - - let visibility = input.parse::()?; - - input.parse::()?; - - input.parse::()?; - let name = input.parse()?; - - // (...) - let Parenthesised(args) = input.parse::>()?; - - let ret = match input.parse::()? { - ReturnType::Type(_, t) => (*t).clone(), - ReturnType::Default => { - return Err(input - .error("expected a result type of either `CommandResult` or `CheckResult`")) - }, - }; - - // { ... } - let bcont; - braced!(bcont in input); - let body = bcont.call(Block::parse_within)?; - - let args = args.into_iter().map(parse_argument).collect::>>()?; - - Ok(Self { - attributes, - cooked, - visibility, - name, - args, - ret, - body, - }) - } -} - -impl ToTokens for CommandFun { - fn to_tokens(&self, stream: &mut TokenStream2) { - let Self { - attributes: _, - cooked, - visibility, - name, - args, - ret, - body, - } = self; - - stream.extend(quote! { - #(#cooked)* - #visibility async fn #name (#(#args),*) -> #ret { - #(#body)* - } - }); - } -} - -#[derive(Debug)] -pub struct FunctionHook { - pub attributes: Vec, - pub visibility: Visibility, - pub name: Ident, - pub args: Vec, - pub ret: Type, - pub body: Vec, -} - -#[derive(Debug)] -pub struct ClosureHook { - pub attributes: Vec, - pub args: Punctuated, - pub ret: ReturnType, - pub body: Box, -} - -#[derive(Debug)] -pub enum Hook { - Function(Box), - Closure(ClosureHook), -} - -impl Parse for Hook { - fn parse(input: ParseStream<'_>) -> Result { - let attributes = input.call(Attribute::parse_outer)?; - - if is_function(input) { - parse_function_hook(input, attributes).map(|h| Self::Function(Box::new(h))) - } else { - parse_closure_hook(input, attributes).map(Self::Closure) - } - } -} - -fn is_function(input: ParseStream<'_>) -> bool { - input.peek(Token![pub]) || (input.peek(Token![async]) && input.peek2(Token![fn])) -} - -fn parse_function_hook(input: ParseStream<'_>, attributes: Vec) -> Result { - let visibility = input.parse::()?; - - input.parse::()?; - input.parse::()?; - - let name = input.parse()?; - - // (...) - let Parenthesised(args) = input.parse::>()?; - - let ret = match input.parse::()? { - ReturnType::Type(_, t) => (*t).clone(), - ReturnType::Default => { - Type::Verbatim(TokenStream2::from_str("()").expect("Invalid str to create `()`-type")) - }, - }; - - // { ... } - let bcont; - braced!(bcont in input); - let body = bcont.call(Block::parse_within)?; - - let args = args.into_iter().map(parse_argument).collect::>>()?; - - Ok(FunctionHook { - attributes, - visibility, - name, - args, - ret, - body, - }) -} - -fn parse_closure_hook(input: ParseStream<'_>, attributes: Vec) -> Result { - input.parse::()?; - let closure = input.parse::()?; - - Ok(ClosureHook { - attributes, - args: closure.inputs, - ret: closure.output, - body: closure.body, - }) -} - -#[derive(Debug, Default)] -pub struct Permissions(pub u64); - -impl Permissions { - pub fn from_str(s: &str) -> Option { - Some(Permissions(match s.to_uppercase().as_str() { - "PRESET_GENERAL" => 0b0000_0110_0011_0111_1101_1100_0100_0001, - "PRESET_TEXT" => 0b0000_0000_0000_0111_1111_1100_0100_0000, - "PRESET_VOICE" => 0b0000_0011_1111_0000_0000_0000_0000_0000, - "CREATE_INVITE" | "CREATE_INSTANT_INVITE" => 1 << 0, - "KICK_MEMBERS" => 1 << 1, - "BAN_MEMBERS" => 1 << 2, - "ADMINISTRATOR" => 1 << 3, - "MANAGE_CHANNELS" => 1 << 4, - "MANAGE_GUILD" => 1 << 5, - "ADD_REACTIONS" => 1 << 6, - "VIEW_AUDIT_LOG" => 1 << 7, - "PRIORITY_SPEAKER" => 1 << 8, - "STREAM" => 1 << 9, - "VIEW_CHANNEL" => 1 << 10, - "SEND_MESSAGES" => 1 << 11, - "SEND_TTS_MESSAGES" => 1 << 12, - "MANAGE_MESSAGES" => 1 << 13, - "EMBED_LINKS" => 1 << 14, - "ATTACH_FILES" => 1 << 15, - "READ_MESSAGE_HISTORY" => 1 << 16, - "MENTION_EVERYONE" => 1 << 17, - "USE_EXTERNAL_EMOJIS" => 1 << 18, - "VIEW_GUILD_INSIGHTS" => 1 << 19, - "CONNECT" => 1 << 20, - "SPEAK" => 1 << 21, - "MUTE_MEMBERS" => 1 << 22, - "DEAFEN_MEMBERS" => 1 << 23, - "MOVE_MEMBERS" => 1 << 24, - "USE_VAD" => 1 << 25, - "CHANGE_NICKNAME" => 1 << 26, - "MANAGE_NICKNAMES" => 1 << 27, - "MANAGE_ROLES" => 1 << 28, - "MANAGE_WEBHOOKS" => 1 << 29, - "MANAGE_EMOJIS_AND_STICKERS" | "MANAGE_GUILD_EXPRESSIONS" => 1 << 30, - "USE_SLASH_COMMANDS" | "USE_APPLICATION_COMMANDS" => 1 << 31, - "REQUEST_TO_SPEAK" => 1 << 32, - "MANAGE_EVENTS" => 1 << 33, - "MANAGE_THREADS" => 1 << 34, - "CREATE_PUBLIC_THREADS" => 1 << 35, - "CREATE_PRIVATE_THREADS" => 1 << 36, - "USE_EXTERNAL_STICKERS" => 1 << 37, - "SEND_MESSAGES_IN_THREADS" => 1 << 38, - "USE_EMBEDDED_ACTIVITIES" => 1 << 39, - "MODERATE_MEMBERS" => 1 << 40, - "VIEW_CREATOR_MONETIZATION_ANALYTICS" => 1 << 41, - "USE_SOUNDBOARD" => 1 << 42, - "CREATE_GUILD_EXPRESSIONS" => 1 << 43, - "CREATE_EVENTS" => 1 << 44, - "USE_EXTERNAL_SOUNDS" => 1 << 45, - "SEND_VOICE_MESSAGES" => 1 << 46, - "SET_VOICE_CHANNEL_STATUS" => 1 << 48, - _ => return None, - })) - } -} - -impl ToTokens for Permissions { - fn to_tokens(&self, stream: &mut TokenStream2) { - let bits = self.0; - - let path = quote!(serenity::model::permissions::Permissions::from_bits_truncate); - - stream.extend(quote! { - #path(#bits) - }); - } -} - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct Colour(pub u32); - -impl Colour { - pub fn from_str(s: &str) -> Option { - let hex = match s.to_uppercase().as_str() { - "BLITZ_BLUE" => 0x6FC6E2, - "BLUE" => 0x3498DB, - "BLURPLE" => 0x7289DA, - "DARK_BLUE" => 0x206694, - "DARK_GOLD" => 0xC27C0E, - "DARK_GREEN" => 0x1F8B4C, - "DARK_GREY" => 0x607D8B, - "DARK_MAGENTA" => 0xAD14757, - "DARK_ORANGE" => 0xA84300, - "DARK_PURPLE" => 0x71368A, - "DARK_RED" => 0x992D22, - "DARK_TEAL" => 0x11806A, - "DARKER_GREY" => 0x546E7A, - "FABLED_PINK" => 0xFAB81ED, - "FADED_PURPLE" => 0x8882C4, - "FOOYOO" => 0x11CA80, - "GOLD" => 0xF1C40F, - "KERBAL" => 0xBADA55, - "LIGHT_GREY" => 0x979C9F, - "LIGHTER_GREY" => 0x95A5A6, - "MAGENTA" => 0xE91E63, - "MEIBE_PINK" => 0xE68397, - "ORANGE" => 0xE67E22, - "PURPLE" => 0x9B59B6, - "RED" => 0xE74C3C, - "ROHRKATZE_BLUE" => 0x7596FF, - "ROSEWATER" => 0xF6DBD8, - "TEAL" => 0x1ABC9C, - _ => { - let s = s.strip_prefix('#')?; - - if s.len() != 6 { - return None; - } - - u32::from_str_radix(s, 16).ok()? - }, - }; - - Some(Colour(hex)) - } -} - -impl ToTokens for Colour { - fn to_tokens(&self, stream: &mut TokenStream2) { - let value = self.0; - let path = quote!(serenity::model::Colour); - - stream.extend(quote! { - #path(#value) - }); - } -} - -#[derive(Debug, Default)] -pub struct Checks(pub Vec); - -impl ToTokens for Checks { - fn to_tokens(&self, stream: &mut TokenStream2) { - let v = self.0.iter().map(|i| i.with_suffix(CHECK)); - - stream.extend(quote!(&[#(&#v),*])); - } -} - -#[derive(Debug, Default)] -pub struct Options { - pub checks: Checks, - pub bucket: AsOption, - pub aliases: Vec, - pub description: AsOption, - pub delimiters: Vec, - pub usage: AsOption, - pub examples: Vec, - pub min_args: AsOption, - pub max_args: AsOption, - pub allowed_roles: Vec, - pub required_permissions: Permissions, - pub help_available: bool, - pub only_in: OnlyIn, - pub owners_only: bool, - pub owner_privilege: bool, - pub sub_commands: Vec, -} - -impl Options { - #[inline] - pub fn new() -> Self { - Self { - help_available: true, - ..Default::default() - } - } -} - -#[derive(Debug, Eq, PartialEq)] -pub enum HelpBehaviour { - Strike, - Hide, - Nothing, -} - -impl HelpBehaviour { - pub fn from_str(s: &str) -> Option { - Some(match s.to_lowercase().as_str() { - "strike" => HelpBehaviour::Strike, - "hide" => HelpBehaviour::Hide, - "nothing" => HelpBehaviour::Nothing, - _ => return None, - }) - } -} - -impl ToTokens for HelpBehaviour { - fn to_tokens(&self, stream: &mut TokenStream2) { - let help_behaviour_path = quote!(serenity::framework::standard::HelpBehaviour); - match self { - Self::Strike => stream.extend(quote!(#help_behaviour_path::Strike)), - Self::Hide => stream.extend(quote!(#help_behaviour_path::Hide)), - Self::Nothing => stream.extend(quote!(#help_behaviour_path::Nothing)), - } - } -} - -#[derive(Debug, Eq, PartialEq)] -pub struct HelpOptions { - pub suggestion_text: String, - pub no_help_available_text: String, - pub usage_label: String, - pub usage_sample_label: String, - pub ungrouped_label: String, - pub description_label: String, - pub grouped_label: String, - pub aliases_label: String, - pub sub_commands_label: String, - pub guild_only_text: String, - pub checks_label: String, - pub dm_only_text: String, - pub dm_and_guild_text: String, - pub available_text: String, - pub command_not_found_text: String, - pub individual_command_tip: String, - pub strikethrough_commands_tip_in_dm: Option, - pub strikethrough_commands_tip_in_guild: Option, - pub group_prefix: String, - pub lacking_role: HelpBehaviour, - pub lacking_permissions: HelpBehaviour, - pub lacking_ownership: HelpBehaviour, - pub lacking_conditions: HelpBehaviour, - pub wrong_channel: HelpBehaviour, - pub embed_error_colour: Colour, - pub embed_success_colour: Colour, - pub max_levenshtein_distance: usize, - pub indention_prefix: String, -} - -impl Default for HelpOptions { - fn default() -> HelpOptions { - HelpOptions { - suggestion_text: "Did you mean `{}`?".to_string(), - no_help_available_text: "**Error**: No help available.".to_string(), - usage_label: "Usage".to_string(), - usage_sample_label: "Sample usage".to_string(), - ungrouped_label: "Ungrouped".to_string(), - grouped_label: "Group".to_string(), - aliases_label: "Aliases".to_string(), - description_label: "Description".to_string(), - guild_only_text: "Only in servers".to_string(), - checks_label: "Checks".to_string(), - sub_commands_label: "Sub Commands".to_string(), - dm_only_text: "Only in DM".to_string(), - dm_and_guild_text: "In DM and servers".to_string(), - available_text: "Available".to_string(), - command_not_found_text: "**Error**: Command `{}` not found.".to_string(), - individual_command_tip: "To get help with an individual command, pass its \ - name as an argument to this command." - .to_string(), - group_prefix: "Prefix".to_string(), - strikethrough_commands_tip_in_dm: None, - strikethrough_commands_tip_in_guild: None, - lacking_role: HelpBehaviour::Strike, - lacking_permissions: HelpBehaviour::Strike, - lacking_ownership: HelpBehaviour::Hide, - lacking_conditions: HelpBehaviour::Strike, - wrong_channel: HelpBehaviour::Strike, - embed_error_colour: Colour::from_str("DARK_RED").unwrap(), - embed_success_colour: Colour::from_str("ROSEWATER").unwrap(), - max_levenshtein_distance: 0, - indention_prefix: "-".to_string(), - } - } -} - -#[derive(Debug)] -pub struct GroupStruct { - pub visibility: Visibility, - pub cooked: Vec, - pub attributes: Vec, - pub name: Ident, -} - -impl Parse for GroupStruct { - fn parse(input: ParseStream<'_>) -> Result { - let mut attributes = input.call(Attribute::parse_outer)?; - - util::rename_attributes(&mut attributes, "doc", "description"); - - let cooked = remove_cooked(&mut attributes); - - let visibility = input.parse()?; - - input.parse::()?; - - let name = input.parse()?; - - input.parse::()?; - - Ok(Self { - visibility, - cooked, - attributes, - name, - }) - } -} - -impl ToTokens for GroupStruct { - fn to_tokens(&self, stream: &mut TokenStream2) { - let Self { - visibility, - cooked, - attributes: _, - name, - } = self; - - stream.extend(quote! { - #(#cooked)* - #visibility struct #name; - }); - } -} - -#[derive(Debug, Default)] -pub struct GroupOptions { - pub prefixes: Vec, - pub only_in: OnlyIn, - pub owners_only: bool, - pub owner_privilege: bool, - pub help_available: bool, - pub allowed_roles: Vec, - pub required_permissions: Permissions, - pub checks: Checks, - pub default_command: AsOption, - pub description: AsOption, - pub summary: AsOption, - pub commands: Vec, - pub sub_groups: Vec, -} - -impl GroupOptions { - #[inline] - pub fn new() -> Self { - Self { - help_available: true, - ..Default::default() - } - } -} diff --git a/command_attr/src/util.rs b/command_attr/src/util.rs deleted file mode 100644 index cfaeb81728b..00000000000 --- a/command_attr/src/util.rs +++ /dev/null @@ -1,252 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote, ToTokens}; -use syn::parse::{Error, Parse, ParseStream, Result as SynResult}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::token::{Comma, Mut}; -use syn::{parenthesized, parse_quote, Attribute, Ident, Lifetime, Lit, Path, PathSegment, Type}; - -use crate::structures::CommandFun; - -pub trait LitExt { - fn to_str(&self) -> String; - fn to_bool(&self) -> bool; - fn to_ident(&self) -> Ident; -} - -impl LitExt for Lit { - fn to_str(&self) -> String { - match self { - Self::Str(s) => s.value(), - Self::ByteStr(s) => unsafe { String::from_utf8_unchecked(s.value()) }, - Self::Char(c) => c.value().to_string(), - Self::Byte(b) => (b.value() as char).to_string(), - _ => panic!("values must be a (byte)string or a char"), - } - } - - fn to_bool(&self) -> bool { - if let Lit::Bool(b) = self { - b.value - } else { - self.to_str().parse().unwrap_or_else(|_| panic!("expected bool from {:?}", self)) - } - } - - #[inline] - fn to_ident(&self) -> Ident { - Ident::new(&self.to_str(), self.span()) - } -} - -pub trait IdentExt2: Sized { - fn to_string_non_raw(&self) -> String; - fn to_uppercase(&self) -> Self; - fn with_suffix(&self, suf: &str) -> Ident; -} - -impl IdentExt2 for Ident { - #[inline] - fn to_string_non_raw(&self) -> String { - let ident_string = self.to_string(); - ident_string.trim_start_matches("r#").into() - } - - #[inline] - fn to_uppercase(&self) -> Self { - // This should be valid because keywords are lowercase. - format_ident!("{}", self.to_string_non_raw().to_uppercase()) - } - - #[inline] - fn with_suffix(&self, suffix: &str) -> Ident { - format_ident!("{}_{}", self.to_uppercase(), suffix) - } -} - -#[inline] -pub fn into_stream(e: &Error) -> TokenStream { - e.to_compile_error().into() -} - -macro_rules! propagate_err { - ($res:expr) => {{ - match $res { - Ok(v) => v, - Err(e) => return $crate::util::into_stream(&e), - } - }}; -} - -#[derive(Debug)] -pub struct Parenthesised(pub Punctuated); - -impl Parse for Parenthesised { - fn parse(input: ParseStream<'_>) -> SynResult { - let content; - parenthesized!(content in input); - - Ok(Parenthesised(content.parse_terminated(T::parse)?)) - } -} - -#[derive(Debug)] -pub struct AsOption(pub Option); - -impl AsOption { - #[inline] - pub fn map(self, f: impl FnOnce(T) -> U) -> AsOption { - AsOption(self.0.map(f)) - } -} - -impl ToTokens for AsOption { - fn to_tokens(&self, stream: &mut TokenStream2) { - match &self.0 { - Some(o) => stream.extend(quote!(Some(#o))), - None => stream.extend(quote!(None)), - } - } -} - -impl Default for AsOption { - #[inline] - fn default() -> Self { - AsOption(None) - } -} - -#[derive(Debug)] -pub struct Argument { - pub mutable: Option, - pub name: Ident, - pub kind: Type, -} - -impl ToTokens for Argument { - fn to_tokens(&self, stream: &mut TokenStream2) { - let Argument { - mutable, - name, - kind, - } = self; - - stream.extend(quote! { - #mutable #name: #kind - }); - } -} - -#[inline] -pub fn generate_type_validation(have: &Type, expect: &Type) -> syn::Stmt { - parse_quote! { - serenity::static_assertions::assert_type_eq_all!(#have, #expect); - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum DeclarFor { - Command, - Help, - Check, -} - -pub fn create_declaration_validations(fun: &mut CommandFun, dec_for: DeclarFor) -> SynResult<()> { - let len = match dec_for { - DeclarFor::Command => 3, - DeclarFor::Help => 6, - DeclarFor::Check => 4, - }; - - if fun.args.len() > len { - return Err(Error::new( - fun.args.last().unwrap().span(), - format_args!("function's arity exceeds more than {len} arguments"), - )); - } - - let context: Type = parse_quote!(&serenity::client::Context); - let message: Type = parse_quote!(&serenity::model::channel::Message); - let args: Type = parse_quote!(serenity::framework::standard::Args); - let args2: Type = parse_quote!(&mut serenity::framework::standard::Args); - let options: Type = parse_quote!(&serenity::framework::standard::CommandOptions); - let hoptions: Type = parse_quote!(&'static serenity::framework::standard::HelpOptions); - let groups: Type = parse_quote!(&[&'static serenity::framework::standard::CommandGroup]); - let owners: Type = parse_quote!(std::collections::HashSet); - - let mut index = 0; - - let mut spoof_or_check = |kind: Type, name: &str| { - match fun.args.get(index) { - Some(x) => fun.body.insert(0, generate_type_validation(&x.kind, &kind)), - None => fun.args.push(Argument { - mutable: None, - name: Ident::new(name, Span::call_site()), - kind, - }), - } - - index += 1; - }; - - spoof_or_check(context, "_ctx"); - spoof_or_check(message, "_msg"); - - if dec_for == DeclarFor::Check { - spoof_or_check(args2, "_args"); - spoof_or_check(options, "_options"); - - return Ok(()); - } - - spoof_or_check(args, "_args"); - - if dec_for == DeclarFor::Help { - spoof_or_check(hoptions, "_hoptions"); - spoof_or_check(groups, "_groups"); - spoof_or_check(owners, "_owners"); - } - - Ok(()) -} - -#[inline] -pub fn create_return_type_validation(r#fn: &mut CommandFun, expect: &Type) { - let stmt = generate_type_validation(&r#fn.ret, expect); - r#fn.body.insert(0, stmt); -} - -#[inline] -pub fn populate_fut_lifetimes_on_refs(args: &mut Vec) { - for arg in args { - if let Type::Reference(reference) = &mut arg.kind { - reference.lifetime = Some(Lifetime::new("'fut", Span::call_site())); - } - } -} - -/// Renames all attributes that have a specific `name` to the `target`. -pub fn rename_attributes(attributes: &mut Vec, name: &str, target: &str) { - for attr in attributes { - if attr.path.is_ident(name) { - attr.path = Path::from(PathSegment::from(Ident::new(target, Span::call_site()))); - } - } -} - -pub fn append_line(desc: &mut AsOption, mut line: String) { - if line.starts_with(' ') { - line.remove(0); - } - - let desc = desc.0.get_or_insert_with(String::default); - - if let Some(i) = line.rfind("\\$") { - desc.push_str(line[..i].trim_end()); - desc.push(' '); - } else { - desc.push_str(&line); - desc.push('\n'); - } -} diff --git a/examples/README.md b/examples/README.md index 6327f6db136..168ce52e08e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -43,10 +43,9 @@ To run an example, you have various options: 13 => Parallel Loops: How to run tasks in a loop with context access. Additionally, show how to send a message to a specific channel. 14 => Slash Commands: How to use the low level slash command API. - 15 => Simple Dashboard: A simple dashboard to control and monitor the bot with `rillrate`. - 16 => SQLite Database: How to run an embedded SQLite database alongside the bot using SQLx - 17 => Message Components: How to structure and use buttons and select menus - 18 => Webhook: How to construct and call a webhook + 15 => SQLite Database: How to run an embedded SQLite database alongside the bot using SQLx + 16 => Message Components: How to structure and use buttons and select menus + 17 => Webhook: How to construct and call a webhook ``` 2. Manually running: diff --git a/examples/e01_basic_ping_bot/src/main.rs b/examples/e01_basic_ping_bot/src/main.rs index 166ccae3574..86a41bcff5f 100644 --- a/examples/e01_basic_ping_bot/src/main.rs +++ b/examples/e01_basic_ping_bot/src/main.rs @@ -11,8 +11,8 @@ struct Handler; impl EventHandler for Handler { // Set a handler for the `message` event. This is called whenever a new message is received. // - // Event handlers are dispatched through a threadpool, and so multiple events can be - // dispatched simultaneously. + // Event handlers are dispatched through a threadpool, and so multiple events can be dispatched + // simultaneously. async fn message(&self, ctx: Context, msg: Message) { if msg.content == "!ping" { // Sending a message can fail, due to a network error, an authentication error, or lack diff --git a/examples/e03_struct_utilities/src/main.rs b/examples/e03_struct_utilities/src/main.rs index f313cc3d96f..e304d3dcd2e 100644 --- a/examples/e03_struct_utilities/src/main.rs +++ b/examples/e03_struct_utilities/src/main.rs @@ -19,7 +19,7 @@ impl EventHandler for Handler { // In this case, you can direct message a User directly by simply calling a method on // its instance, with the content of the message. let builder = CreateMessage::new().content("Hello!"); - let dm = msg.author.dm(&context, builder).await; + let dm = msg.author.dm(&context.http, builder).await; if let Err(why) = dm { println!("Error when direct messaging user: {why:?}"); diff --git a/examples/e04_message_builder/src/main.rs b/examples/e04_message_builder/src/main.rs index e5fd8ce3b0c..1c3fd296d21 100644 --- a/examples/e04_message_builder/src/main.rs +++ b/examples/e04_message_builder/src/main.rs @@ -26,7 +26,7 @@ impl EventHandler for Handler { // emojis, and more. let response = MessageBuilder::new() .push("User ") - .push_bold_safe(&msg.author.name) + .push_bold_safe(msg.author.name.as_str()) .push(" used the 'ping' command in the ") .mention(&channel) .push(" channel") diff --git a/examples/e05_command_framework/Cargo.toml b/examples/e05_command_framework/Cargo.toml deleted file mode 100644 index bb829733ff7..00000000000 --- a/examples/e05_command_framework/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "e05_command_framework" -version = "0.1.0" -authors = ["my name "] -edition = "2018" - -[dependencies.serenity] -features = ["framework", "standard_framework", "rustls_backend"] -path = "../../" - -[dependencies.tokio] -version = "1.0" -features = ["macros", "rt-multi-thread"] diff --git a/examples/e05_command_framework/src/main.rs b/examples/e05_command_framework/src/main.rs deleted file mode 100644 index d4bab0fd054..00000000000 --- a/examples/e05_command_framework/src/main.rs +++ /dev/null @@ -1,592 +0,0 @@ -//! Requires the 'framework' feature flag be enabled in your project's `Cargo.toml`. -//! -//! This can be enabled by specifying the feature in the dependency section: -//! -//! ```toml -//! [dependencies.serenity] -//! git = "https://github.com/serenity-rs/serenity.git" -//! features = ["framework", "standard_framework"] -//! ``` -#![allow(deprecated)] // We recommend migrating to poise, instead of using the standard command framework. -use std::collections::{HashMap, HashSet}; -use std::env; -use std::fmt::Write; -use std::sync::Arc; - -use serenity::async_trait; -use serenity::builder::EditChannel; -use serenity::framework::standard::buckets::{LimitedFor, RevertBucket}; -use serenity::framework::standard::macros::{check, command, group, help, hook}; -use serenity::framework::standard::{ - help_commands, - Args, - BucketBuilder, - CommandGroup, - CommandOptions, - CommandResult, - Configuration, - DispatchError, - HelpOptions, - Reason, - StandardFramework, -}; -use serenity::gateway::ShardManager; -use serenity::http::Http; -use serenity::model::channel::Message; -use serenity::model::gateway::Ready; -use serenity::model::id::UserId; -use serenity::model::permissions::Permissions; -use serenity::prelude::*; -use serenity::utils::{content_safe, ContentSafeOptions}; - -// A container type is created for inserting into the Client's `data`, which allows for data to be -// accessible across all events and framework commands, or anywhere else that has a copy of the -// `data` Arc. -struct ShardManagerContainer; - -impl TypeMapKey for ShardManagerContainer { - type Value = Arc; -} - -struct CommandCounter; - -impl TypeMapKey for CommandCounter { - type Value = HashMap; -} - -struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn ready(&self, _: Context, ready: Ready) { - println!("{} is connected!", ready.user.name); - } -} - -#[group] -#[commands(about, am_i_admin, say, commands, ping, latency, some_long_command, upper_command)] -struct General; - -#[group] -// Sets multiple prefixes for a group. -// This requires us to call commands in this group via `~emoji` (or `~em`) instead of just `~`. -#[prefixes("emoji", "em")] -// Set a description to appear if a user wants to display a single group e.g. via help using the -// group-name or one of its prefixes. -#[description = "A group with commands providing an emoji as response."] -// Summary only appears when listing multiple groups. -#[summary = "Do emoji fun!"] -// Sets a command that will be executed if only a group-prefix was passed. -#[default_command(bird)] -#[commands(cat, dog)] -struct Emoji; - -#[group] -// Sets a single prefix for this group. -// So one has to call commands in this group via `~math` instead of just `~`. -#[prefix = "math"] -#[commands(multiply)] -struct Math; - -#[group] -#[owners_only] -// Limit all commands to be guild-restricted. -#[only_in(guilds)] -// Summary only appears when listing multiple groups. -#[summary = "Commands for server owners"] -#[commands(slow_mode)] -struct Owner; - -// The framework provides two built-in help commands for you to use. But you can also make your own -// customized help command that forwards to the behaviour of either of them. -#[help] -// This replaces the information that a user can pass a command-name as argument to gain specific -// information about it. -#[individual_command_tip = "Hello! こんにちは!Hola! Bonjour! 您好! 안녕하세요~\n\n\ -If you want more information about a specific command, just pass the command as argument."] -// Some arguments require a `{}` in order to replace it with contextual information. -// In this case our `{}` refers to a command's name. -#[command_not_found_text = "Could not find: `{}`."] -// Define the maximum Levenshtein-distance between a searched command-name and commands. If the -// distance is lower than or equal the set distance, it will be displayed as a suggestion. -// Setting the distance to 0 will disable suggestions. -#[max_levenshtein_distance(3)] -// When you use sub-groups, Serenity will use the `indention_prefix` to indicate how deeply an item -// is indented. The default value is "-", it will be changed to "+". -#[indention_prefix = "+"] -// On another note, you can set up the help-menu-filter-behaviour. -// Here are all possible settings shown on all possible options. -// First case is if a user lacks permissions for a command, we can hide the command. -#[lacking_permissions = "Hide"] -// If the user is nothing but lacking a certain role, we just display it. -#[lacking_role = "Nothing"] -// The last `enum`-variant is `Strike`, which ~~strikes~~ a command. -#[wrong_channel = "Strike"] -// Serenity will automatically analyse and generate a hint/tip explaining the possible cases of -// ~~strikethrough-commands~~, but only if `strikethrough_commands_tip_in_{dm, guild}` aren't -// specified. If you pass in a value, it will be displayed instead. -async fn my_help( - context: &Context, - msg: &Message, - args: Args, - help_options: &'static HelpOptions, - groups: &[&'static CommandGroup], - owners: HashSet, -) -> CommandResult { - let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await; - Ok(()) -} - -#[hook] -async fn before(ctx: &Context, msg: &Message, command_name: &str) -> bool { - println!("Got command '{}' by user '{}'", command_name, msg.author.name); - - // Increment the number of times this command has been run once. If the command's name does not - // exist in the counter, add a default value of 0. - let mut data = ctx.data.write().await; - let counter = data.get_mut::().expect("Expected CommandCounter in TypeMap."); - let entry = counter.entry(command_name.to_string()).or_insert(0); - *entry += 1; - - true // if `before` returns false, command processing doesn't happen. -} - -#[hook] -async fn after(_ctx: &Context, _msg: &Message, command_name: &str, command_result: CommandResult) { - match command_result { - Ok(()) => println!("Processed command '{command_name}'"), - Err(why) => println!("Command '{command_name}' returned error {why:?}"), - } -} - -#[hook] -async fn unknown_command(_ctx: &Context, _msg: &Message, unknown_command_name: &str) { - println!("Could not find command named '{unknown_command_name}'"); -} - -#[hook] -async fn normal_message(_ctx: &Context, msg: &Message) { - println!("Message is not a command '{}'", msg.content); -} - -#[hook] -async fn delay_action(ctx: &Context, msg: &Message) { - // You may want to handle a Discord rate limit if this fails. - let _ = msg.react(ctx, '⏱').await; -} - -#[hook] -async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError, _command_name: &str) { - if let DispatchError::Ratelimited(info) = error { - // We notify them only once. - if info.is_first_try { - let _ = msg - .channel_id - .say(&ctx.http, &format!("Try this again in {} seconds.", info.as_secs())) - .await; - } - } -} - -// You can construct a hook without the use of a macro, too. -// This requires some boilerplate though and the following additional import. -use serenity::futures::future::BoxFuture; -use serenity::FutureExt; -fn _dispatch_error_no_macro<'fut>( - ctx: &'fut mut Context, - msg: &'fut Message, - error: DispatchError, - _command_name: &str, -) -> BoxFuture<'fut, ()> { - async move { - if let DispatchError::Ratelimited(info) = error { - if info.is_first_try { - let _ = msg - .channel_id - .say(&ctx.http, &format!("Try this again in {} seconds.", info.as_secs())) - .await; - } - }; - } - .boxed() -} - -#[tokio::main] -async fn main() { - // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - - let http = Http::new(&token); - - // We will fetch your bot's owners and id - let (owners, bot_id) = match http.get_current_application_info().await { - Ok(info) => { - let mut owners = HashSet::new(); - if let Some(team) = info.team { - owners.insert(team.owner_user_id); - } else if let Some(owner) = &info.owner { - owners.insert(owner.id); - } - match http.get_current_user().await { - Ok(bot_id) => (owners, bot_id.id), - Err(why) => panic!("Could not access the bot id: {:?}", why), - } - }, - Err(why) => panic!("Could not access application info: {:?}", why), - }; - - let framework = StandardFramework::new() - // Set a function to be called prior to each command execution. This provides the context - // of the command, the message that was received, and the full name of the command that - // will be called. - // - // Avoid using this to determine whether a specific command should be executed. Instead, - // prefer using the `#[check]` macro which gives you this functionality. - // - // **Note**: Async closures are unstable, you may use them in your application if you are - // fine using nightly Rust. If not, we need to provide the function identifiers to the - // hook-functions (before, after, normal, ...). - .before(before) - // Similar to `before`, except will be called directly _after_ command execution. - .after(after) - // Set a function that's called whenever an attempted command-call's command could not be - // found. - .unrecognised_command(unknown_command) - // Set a function that's called whenever a message is not a command. - .normal_message(normal_message) - // Set a function that's called whenever a command's execution didn't complete for one - // reason or another. For example, when a user has exceeded a rate-limit or a command can - // only be performed by the bot owner. - .on_dispatch_error(dispatch_error) - // Can't be used more than once per 5 seconds: - .bucket("emoji", BucketBuilder::default().delay(5)).await - // Can't be used more than 2 times per 30 seconds, with a 5 second delay applying per - // channel. Optionally `await_ratelimits` will delay until the command can be executed - // instead of cancelling the command invocation. - .bucket("complicated", - BucketBuilder::default().limit(2).time_span(30).delay(5) - // The target each bucket will apply to. - .limit_for(LimitedFor::Channel) - // The maximum amount of command invocations that can be delayed per target. - // Setting this to 0 (default) will never await/delay commands and cancel the invocation. - .await_ratelimits(1) - // A function to call when a rate limit leads to a delay. - .delay_action(delay_action) - ).await - // The `#[group]` macro generates `static` instances of the options set for the group. - // They're made in the pattern: `#name_GROUP` for the group instance and `#name_GROUP_OPTIONS`. - // #name is turned all uppercase - .help(&MY_HELP) - .group(&GENERAL_GROUP) - .group(&EMOJI_GROUP) - .group(&MATH_GROUP) - .group(&OWNER_GROUP); - - framework.configure( - Configuration::new().with_whitespace(true) - .on_mention(Some(bot_id)) - .prefix("~") - // In this case, if "," would be first, a message would never be delimited at ", ", - // forcing you to trim your arguments if you want to avoid whitespaces at the start of - // each. - .delimiters(vec![", ", ","]) - // Sets the bot's owners. These will be used for commands that are owners only. - .owners(owners), - ); - - // For this example to run properly, the "Presence Intent" and "Server Members Intent" options - // need to be enabled. - // These are needed so the `required_permissions` macro works on the commands that need to use - // it. - // You will need to enable these 2 options on the bot application, and possibly wait up to 5 - // minutes. - let intents = GatewayIntents::all(); - let mut client = Client::builder(&token, intents) - .event_handler(Handler) - .framework(framework) - .type_map_insert::(HashMap::default()) - .await - .expect("Err creating client"); - - { - let mut data = client.data.write().await; - data.insert::(Arc::clone(&client.shard_manager)); - } - - if let Err(why) = client.start().await { - println!("Client error: {why:?}"); - } -} - -// Commands can be created via the attribute `#[command]` macro. -#[command] -// Options are passed via subsequent attributes. -// Make this command use the "complicated" bucket. -#[bucket = "complicated"] -async fn commands(ctx: &Context, msg: &Message) -> CommandResult { - let mut contents = "Commands used:\n".to_string(); - - let data = ctx.data.read().await; - let counter = data.get::().expect("Expected CommandCounter in TypeMap."); - - for (name, amount) in counter { - writeln!(contents, "- {name}: {amount}")?; - } - - msg.channel_id.say(&ctx.http, &contents).await?; - - Ok(()) -} - -// Repeats what the user passed as argument but ensures that user and role mentions are replaced -// with a safe textual alternative. -// In this example channel mentions are excluded via the `ContentSafeOptions`. -#[command] -async fn say(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - match args.single_quoted::() { - Ok(x) => { - let settings = if let Some(guild_id) = msg.guild_id { - // By default roles, users, and channel mentions are cleaned. - ContentSafeOptions::default() - // We do not want to clean channal mentions as they do not ping users. - .clean_channel(false) - // If it's a guild channel, we want mentioned users to be displayed as their - // display name. - .display_as_member_from(guild_id) - } else { - ContentSafeOptions::default().clean_channel(false).clean_role(false) - }; - - let content = content_safe(&ctx.cache, x, &settings, &msg.mentions); - - msg.channel_id.say(&ctx.http, &content).await?; - - return Ok(()); - }, - Err(_) => { - msg.reply(ctx, "An argument is required to run this command.").await?; - return Ok(()); - }, - }; -} - -// A function which acts as a "check", to determine whether to call a command. -// -// In this case, this command checks to ensure you are the owner of the message in order for the -// command to be executed. If the check fails, the command is not called. -#[check] -#[name = "Owner"] -#[rustfmt::skip] -async fn owner_check( - _: &Context, - msg: &Message, - _: &mut Args, - _: &CommandOptions, -) -> Result<(), Reason> { - // Replace 7 with your ID to make this check pass. - // - // 1. If you want to pass a reason alongside failure you can do: - // `Reason::User("Lacked admin permission.".to_string())`, - // - // 2. If you want to mark it as something you want to log only: - // `Reason::Log("User lacked admin permission.".to_string())`, - // - // 3. If the check's failure origin is unknown you can mark it as such: - // `Reason::Unknown` - // - // 4. If you want log for your system and for the user, use: - // `Reason::UserAndLog { user, log }` - if msg.author.id != 7 { - return Err(Reason::User("Lacked owner permission".to_string())); - } - - Ok(()) -} - -#[command] -async fn some_long_command(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - msg.channel_id.say(&ctx.http, &format!("Arguments: {:?}", args.rest())).await?; - - Ok(()) -} - -#[command] -// Limits the usage of this command to roles named: -#[allowed_roles("mods", "ultimate neko")] -async fn about_role(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - let role_name = args.rest(); - let to_send = match msg.guild(&ctx.cache).as_deref().and_then(|g| g.role_by_name(role_name)) { - Some(role_id) => format!("Role-ID: {role_id}"), - None => format!("Could not find role name: {role_name:?}"), - }; - - if let Err(why) = msg.channel_id.say(&ctx.http, to_send).await { - println!("Error sending message: {why:?}"); - } - - Ok(()) -} - -#[command] -// Lets us also call `~math *` instead of just `~math multiply`. -#[aliases("*")] -async fn multiply(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let first = args.single::()?; - let second = args.single::()?; - - let res = first * second; - - msg.channel_id.say(&ctx.http, &res.to_string()).await?; - - Ok(()) -} - -#[command] -async fn about(ctx: &Context, msg: &Message) -> CommandResult { - msg.channel_id.say(&ctx.http, "This is a small test-bot! : )").await?; - - Ok(()) -} - -#[command] -async fn latency(ctx: &Context, msg: &Message) -> CommandResult { - // The shard manager is an interface for mutating, stopping, restarting, and retrieving - // information about shards. - let data = ctx.data.read().await; - - let shard_manager = match data.get::() { - Some(v) => v, - None => { - msg.reply(ctx, "There was a problem getting the shard manager").await?; - - return Ok(()); - }, - }; - - let runners = shard_manager.runners.lock().await; - - // Shards are backed by a "shard runner" responsible for processing events over the shard, so - // we'll get the information about the shard runner for the shard this command was sent over. - let runner = match runners.get(&ctx.shard_id) { - Some(runner) => runner, - None => { - msg.reply(ctx, "No shard found").await?; - - return Ok(()); - }, - }; - - msg.reply(ctx, &format!("The shard latency is {:?}", runner.latency)).await?; - - Ok(()) -} - -#[command] -// Limit command usage to guilds. -#[only_in(guilds)] -#[checks(Owner)] -async fn ping(ctx: &Context, msg: &Message) -> CommandResult { - msg.channel_id.say(&ctx.http, "Pong! : )").await?; - - Ok(()) -} - -#[command] -// Adds multiple aliases -#[aliases("kitty", "neko")] -// Make this command use the "emoji" bucket. -#[bucket = "emoji"] -// Allow only administrators to call this: -#[required_permissions("ADMINISTRATOR")] -async fn cat(ctx: &Context, msg: &Message) -> CommandResult { - msg.channel_id.say(&ctx.http, ":cat:").await?; - - // We can return one ticket to the bucket undoing the ratelimit. - Err(RevertBucket.into()) -} - -#[command] -#[description = "Sends an emoji with a dog."] -#[bucket = "emoji"] -async fn dog(ctx: &Context, msg: &Message) -> CommandResult { - msg.channel_id.say(&ctx.http, ":dog:").await?; - - Ok(()) -} - -#[command] -async fn bird(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - let say_content = if args.is_empty() { - ":bird: can find animals for you.".to_string() - } else { - format!(":bird: could not find animal named: `{}`.", args.rest()) - }; - - msg.channel_id.say(&ctx.http, say_content).await?; - - Ok(()) -} - -// We could also use #[required_permissions(ADMINISTRATOR)] but that would not let us reply when it -// fails. -#[command] -async fn am_i_admin(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let is_admin = if let (Some(member), Some(guild)) = (&msg.member, msg.guild(&ctx.cache)) { - member.roles.iter().any(|role| { - guild.roles.get(role).is_some_and(|r| r.has_permission(Permissions::ADMINISTRATOR)) - }) - } else { - false - }; - - if is_admin { - msg.channel_id.say(&ctx.http, "Yes, you are.").await?; - } else { - msg.channel_id.say(&ctx.http, "No, you are not.").await?; - } - - Ok(()) -} - -#[command] -async fn slow_mode(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let say_content = if let Ok(slow_mode_rate_seconds) = args.single::() { - let builder = EditChannel::new().rate_limit_per_user(slow_mode_rate_seconds); - if let Err(why) = msg.channel_id.edit(&ctx.http, builder).await { - println!("Error setting channel's slow mode rate: {why:?}"); - - format!("Failed to set slow mode to `{slow_mode_rate_seconds}` seconds.") - } else { - format!("Successfully set slow mode rate to `{slow_mode_rate_seconds}` seconds.") - } - } else if let Some(channel) = msg.channel_id.to_channel_cached(&ctx.cache) { - let slow_mode_rate = channel.rate_limit_per_user.unwrap_or(0); - format!("Current slow mode rate is `{slow_mode_rate}` seconds.") - } else { - "Failed to find channel in cache.".to_string() - }; - - msg.channel_id.say(&ctx.http, say_content).await?; - - Ok(()) -} - -// A command can have sub-commands, just like in command lines tools. Imagine `cargo help` and -// `cargo help run`. -#[command("upper")] -#[sub_commands(sub)] -async fn upper_command(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - msg.reply(&ctx.http, "This is the main function!").await?; - - Ok(()) -} - -// This will only be called if preceded by the `upper`-command. -#[command] -#[aliases("sub-command", "secret")] -#[description("This is `upper`'s sub-command.")] -async fn sub(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - msg.reply(&ctx.http, "This is a sub function!").await?; - - Ok(()) -} diff --git a/examples/e14_slash_commands/Cargo.toml b/examples/e05_sample_bot_structure/Cargo.toml similarity index 84% rename from examples/e14_slash_commands/Cargo.toml rename to examples/e05_sample_bot_structure/Cargo.toml index 4e73adeb7f2..97bd625dc00 100644 --- a/examples/e14_slash_commands/Cargo.toml +++ b/examples/e05_sample_bot_structure/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "e14_slash_commands" +name = "e05_sample_bot_structure" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition = "2021" [dependencies] serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "collector"] } diff --git a/examples/e05_command_framework/Makefile.toml b/examples/e05_sample_bot_structure/Makefile.toml similarity index 100% rename from examples/e05_command_framework/Makefile.toml rename to examples/e05_sample_bot_structure/Makefile.toml diff --git a/examples/e14_slash_commands/README.md b/examples/e05_sample_bot_structure/README.md similarity index 100% rename from examples/e14_slash_commands/README.md rename to examples/e05_sample_bot_structure/README.md diff --git a/examples/e14_slash_commands/src/commands/attachmentinput.rs b/examples/e05_sample_bot_structure/src/commands/attachmentinput.rs similarity index 94% rename from examples/e14_slash_commands/src/commands/attachmentinput.rs rename to examples/e05_sample_bot_structure/src/commands/attachmentinput.rs index 21924fe2027..1dce65a1c14 100644 --- a/examples/e14_slash_commands/src/commands/attachmentinput.rs +++ b/examples/e05_sample_bot_structure/src/commands/attachmentinput.rs @@ -12,7 +12,7 @@ pub fn run(options: &[ResolvedOption]) -> String { } } -pub fn register() -> CreateCommand { +pub fn register() -> CreateCommand<'static> { CreateCommand::new("attachmentinput") .description("Test command for attachment input") .add_option( diff --git a/examples/e14_slash_commands/src/commands/id.rs b/examples/e05_sample_bot_structure/src/commands/id.rs similarity index 93% rename from examples/e14_slash_commands/src/commands/id.rs rename to examples/e05_sample_bot_structure/src/commands/id.rs index 74e29911191..47da776fb08 100644 --- a/examples/e14_slash_commands/src/commands/id.rs +++ b/examples/e05_sample_bot_structure/src/commands/id.rs @@ -12,7 +12,7 @@ pub fn run(options: &[ResolvedOption]) -> String { } } -pub fn register() -> CreateCommand { +pub fn register() -> CreateCommand<'static> { CreateCommand::new("id").description("Get a user id").add_option( CreateCommandOption::new(CommandOptionType::User, "id", "The user to lookup") .required(true), diff --git a/examples/e14_slash_commands/src/commands/mod.rs b/examples/e05_sample_bot_structure/src/commands/mod.rs similarity index 100% rename from examples/e14_slash_commands/src/commands/mod.rs rename to examples/e05_sample_bot_structure/src/commands/mod.rs diff --git a/examples/e14_slash_commands/src/commands/modal.rs b/examples/e05_sample_bot_structure/src/commands/modal.rs similarity index 93% rename from examples/e14_slash_commands/src/commands/modal.rs rename to examples/e05_sample_bot_structure/src/commands/modal.rs index 1f3e7f918a1..4e9746827e1 100644 --- a/examples/e14_slash_commands/src/commands/modal.rs +++ b/examples/e05_sample_bot_structure/src/commands/modal.rs @@ -17,7 +17,7 @@ pub async fn run(ctx: &Context, interaction: &CommandInteraction) -> Result<(), response .interaction .create_response( - ctx, + &ctx.http, CreateInteractionResponse::Message(CreateInteractionResponseMessage::new().content( format!("**Name**: {first_name} {last_name}\n\nHobbies and interests: {hobbies}"), )), @@ -26,6 +26,6 @@ pub async fn run(ctx: &Context, interaction: &CommandInteraction) -> Result<(), Ok(()) } -pub fn register() -> CreateCommand { +pub fn register() -> CreateCommand<'static> { CreateCommand::new("modal").description("Asks some details about you") } diff --git a/examples/e14_slash_commands/src/commands/numberinput.rs b/examples/e05_sample_bot_structure/src/commands/numberinput.rs similarity index 94% rename from examples/e14_slash_commands/src/commands/numberinput.rs rename to examples/e05_sample_bot_structure/src/commands/numberinput.rs index f7643bd3cca..18b77edf27a 100644 --- a/examples/e14_slash_commands/src/commands/numberinput.rs +++ b/examples/e05_sample_bot_structure/src/commands/numberinput.rs @@ -1,7 +1,7 @@ use serenity::builder::{CreateCommand, CreateCommandOption}; use serenity::model::application::CommandOptionType; -pub fn register() -> CreateCommand { +pub fn register() -> CreateCommand<'static> { CreateCommand::new("numberinput") .description("Test command for number input") .add_option( diff --git a/examples/e14_slash_commands/src/commands/ping.rs b/examples/e05_sample_bot_structure/src/commands/ping.rs similarity index 83% rename from examples/e14_slash_commands/src/commands/ping.rs rename to examples/e05_sample_bot_structure/src/commands/ping.rs index cd92b879919..6970a84e4fc 100644 --- a/examples/e14_slash_commands/src/commands/ping.rs +++ b/examples/e05_sample_bot_structure/src/commands/ping.rs @@ -5,6 +5,6 @@ pub fn run(_options: &[ResolvedOption]) -> String { "Hey, I'm alive!".to_string() } -pub fn register() -> CreateCommand { +pub fn register() -> CreateCommand<'static> { CreateCommand::new("ping").description("A ping command") } diff --git a/examples/e14_slash_commands/src/commands/welcome.rs b/examples/e05_sample_bot_structure/src/commands/welcome.rs similarity index 69% rename from examples/e14_slash_commands/src/commands/welcome.rs rename to examples/e05_sample_bot_structure/src/commands/welcome.rs index e11a98d6c7b..08d3bd86f61 100644 --- a/examples/e14_slash_commands/src/commands/welcome.rs +++ b/examples/e05_sample_bot_structure/src/commands/welcome.rs @@ -1,7 +1,16 @@ +use std::borrow::Cow; +use std::collections::HashMap; + use serenity::builder::{CreateCommand, CreateCommandOption}; use serenity::model::application::CommandOptionType; -pub fn register() -> CreateCommand { +fn new_map<'a>(key: &'a str, value: &'a str) -> HashMap, Cow<'a, str>> { + let mut map = HashMap::with_capacity(1); + map.insert(Cow::Borrowed(key), Cow::Borrowed(value)); + map +} + +pub fn register() -> CreateCommand<'static> { CreateCommand::new("welcome") .description("Welcome a user") .name_localized("de", "begrüßen") @@ -20,27 +29,28 @@ pub fn register() -> CreateCommand { .add_string_choice_localized( "Welcome to our cool server! Ask me if you need help", "pizza", - [( + new_map( "de", "Willkommen auf unserem coolen Server! Frag mich, falls du Hilfe brauchst", - )], + ), + ) + .add_string_choice_localized( + "Hey, do you want a coffee?", + "coffee", + new_map("de", "Hey, willst du einen Kaffee?"), ) - .add_string_choice_localized("Hey, do you want a coffee?", "coffee", [( - "de", - "Hey, willst du einen Kaffee?", - )]) .add_string_choice_localized( "Welcome to the club, you're now a good person. Well, I hope.", "club", - [( + new_map( "de", "Willkommen im Club, du bist jetzt ein guter Mensch. Naja, hoffentlich.", - )], + ), ) .add_string_choice_localized( "I hope that you brought a controller to play together!", "game", - [("de", "Ich hoffe du hast einen Controller zum Spielen mitgebracht!")], + new_map("de", "Ich hoffe du hast einen Controller zum Spielen mitgebracht!"), ), ) } diff --git a/examples/e14_slash_commands/src/commands/wonderful_command.rs b/examples/e05_sample_bot_structure/src/commands/wonderful_command.rs similarity index 72% rename from examples/e14_slash_commands/src/commands/wonderful_command.rs rename to examples/e05_sample_bot_structure/src/commands/wonderful_command.rs index 95e4f1761d8..d1f991a6427 100644 --- a/examples/e14_slash_commands/src/commands/wonderful_command.rs +++ b/examples/e05_sample_bot_structure/src/commands/wonderful_command.rs @@ -1,5 +1,5 @@ use serenity::builder::CreateCommand; -pub fn register() -> CreateCommand { +pub fn register() -> CreateCommand<'static> { CreateCommand::new("wonderful_command").description("An amazing command") } diff --git a/examples/e14_slash_commands/src/main.rs b/examples/e05_sample_bot_structure/src/main.rs similarity index 96% rename from examples/e14_slash_commands/src/main.rs rename to examples/e05_sample_bot_structure/src/main.rs index 70e5cea2a29..76026564090 100644 --- a/examples/e14_slash_commands/src/main.rs +++ b/examples/e05_sample_bot_structure/src/main.rs @@ -49,7 +49,7 @@ impl EventHandler for Handler { ); let commands = guild_id - .set_commands(&ctx.http, vec![ + .set_commands(&ctx.http, &[ commands::ping::register(), commands::id::register(), commands::welcome::register(), @@ -75,7 +75,7 @@ async fn main() { let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); // Build our client. - let mut client = Client::builder(token, GatewayIntents::empty()) + let mut client = Client::builder(&token, GatewayIntents::empty()) .event_handler(Handler) .await .expect("Error creating client"); diff --git a/examples/e07_env_logging/Cargo.toml b/examples/e06_env_logging/Cargo.toml similarity index 92% rename from examples/e07_env_logging/Cargo.toml rename to examples/e06_env_logging/Cargo.toml index c81e6b6d031..48f48964b69 100644 --- a/examples/e07_env_logging/Cargo.toml +++ b/examples/e06_env_logging/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e07_env_logging" +name = "e06_env_logging" version = "0.1.0" authors = ["my name "] edition = "2018" diff --git a/examples/e07_env_logging/Makefile.toml b/examples/e06_env_logging/Makefile.toml similarity index 100% rename from examples/e07_env_logging/Makefile.toml rename to examples/e06_env_logging/Makefile.toml diff --git a/examples/e07_env_logging/src/main.rs b/examples/e06_env_logging/src/main.rs similarity index 54% rename from examples/e07_env_logging/src/main.rs rename to examples/e06_env_logging/src/main.rs index f49348526c8..0e1a674a29b 100644 --- a/examples/e07_env_logging/src/main.rs +++ b/examples/e06_env_logging/src/main.rs @@ -1,11 +1,6 @@ -#![allow(deprecated)] // We recommend migrating to poise, instead of using the standard command framework. - use std::env; use serenity::async_trait; -use serenity::framework::standard::macros::{command, group, hook}; -use serenity::framework::standard::{CommandResult, Configuration, StandardFramework}; -use serenity::model::channel::Message; use serenity::model::event::ResumedEvent; use serenity::model::gateway::Ready; use serenity::prelude::*; @@ -34,23 +29,6 @@ impl EventHandler for Handler { } } -#[hook] -// instrument will show additional information on all the logs that happen inside the function. -// -// This additional information includes the function name, along with all it's arguments formatted -// with the Debug impl. This additional information will also only be shown if the LOG level is set -// to `debug` -#[instrument] -async fn before(_: &Context, msg: &Message, command_name: &str) -> bool { - info!("Got command '{}' by user '{}'", command_name, msg.author.name); - - true -} - -#[group] -#[commands(ping)] -struct General; - #[tokio::main] #[instrument] async fn main() { @@ -66,30 +44,14 @@ async fn main() { // Configure the client with your Discord bot token in the environment. let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - let framework = StandardFramework::new().before(before).group(&GENERAL_GROUP); - framework.configure(Configuration::new().prefix("~")); - let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(&token, intents) - .event_handler(Handler) - .framework(framework) - .await - .expect("Err creating client"); + + let mut client = + Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); if let Err(why) = client.start().await { error!("Client error: {:?}", why); } } - -// Currently, the instrument macro doesn't work with commands. -// If you wish to instrument commands, use it on the before function. -#[command] -async fn ping(ctx: &Context, msg: &Message) -> CommandResult { - if let Err(why) = msg.channel_id.say(&ctx.http, "Pong! : )").await { - error!("Error sending message: {:?}", why); - } - - Ok(()) -} diff --git a/examples/e06_sample_bot_structure/.env.example b/examples/e06_sample_bot_structure/.env.example deleted file mode 100644 index 95715bb5809..00000000000 --- a/examples/e06_sample_bot_structure/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -# This declares an environment variable named "DISCORD_TOKEN" with the given -# value. When calling `dotenv::dotenv()`, it will read the `.env` file and parse -# these key-value pairs and insert them into the environment. -# -# Environment variables are separated by newlines and must not have space -# around the equals sign (`=`). -DISCORD_TOKEN=put your token here -# Declares the level of logging to use. Read the documentation for the `log` -# and `env_logger` crates for more information. -RUST_LOG=debug diff --git a/examples/e06_sample_bot_structure/Cargo.toml b/examples/e06_sample_bot_structure/Cargo.toml deleted file mode 100644 index 8be4f908a40..00000000000 --- a/examples/e06_sample_bot_structure/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "e06_sample_bot_structure" -version = "0.1.0" -authors = ["my name "] -edition = "2018" - -[dependencies] -dotenv = "0.15" -tracing = "0.1.23" -tracing-subscriber = "0.3" - -[dependencies.tokio] -version = "1.0" -features = ["macros", "signal", "rt-multi-thread"] - -[dependencies.serenity] -features = ["cache", "framework", "standard_framework", "rustls_backend"] -path = "../../" diff --git a/examples/e06_sample_bot_structure/src/commands/math.rs b/examples/e06_sample_bot_structure/src/commands/math.rs deleted file mode 100644 index 6376dfe45dc..00000000000 --- a/examples/e06_sample_bot_structure/src/commands/math.rs +++ /dev/null @@ -1,16 +0,0 @@ -use serenity::framework::standard::macros::command; -use serenity::framework::standard::{Args, CommandResult}; -use serenity::model::prelude::*; -use serenity::prelude::*; - -#[command] -pub async fn multiply(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let one = args.single::()?; - let two = args.single::()?; - - let product = one * two; - - msg.channel_id.say(&ctx.http, product.to_string()).await?; - - Ok(()) -} diff --git a/examples/e06_sample_bot_structure/src/commands/meta.rs b/examples/e06_sample_bot_structure/src/commands/meta.rs deleted file mode 100644 index 5ee6a57379b..00000000000 --- a/examples/e06_sample_bot_structure/src/commands/meta.rs +++ /dev/null @@ -1,11 +0,0 @@ -use serenity::framework::standard::macros::command; -use serenity::framework::standard::CommandResult; -use serenity::model::prelude::*; -use serenity::prelude::*; - -#[command] -async fn ping(ctx: &Context, msg: &Message) -> CommandResult { - msg.channel_id.say(&ctx.http, "Pong!").await?; - - Ok(()) -} diff --git a/examples/e06_sample_bot_structure/src/commands/mod.rs b/examples/e06_sample_bot_structure/src/commands/mod.rs deleted file mode 100644 index 9c5dfaaa520..00000000000 --- a/examples/e06_sample_bot_structure/src/commands/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod math; -pub mod meta; -pub mod owner; diff --git a/examples/e06_sample_bot_structure/src/commands/owner.rs b/examples/e06_sample_bot_structure/src/commands/owner.rs deleted file mode 100644 index 973679889ab..00000000000 --- a/examples/e06_sample_bot_structure/src/commands/owner.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serenity::framework::standard::macros::command; -use serenity::framework::standard::CommandResult; -use serenity::model::prelude::*; -use serenity::prelude::*; - -use crate::ShardManagerContainer; - -#[command] -#[owners_only] -async fn quit(ctx: &Context, msg: &Message) -> CommandResult { - let data = ctx.data.read().await; - - if let Some(manager) = data.get::() { - msg.reply(ctx, "Shutting down!").await?; - manager.shutdown_all().await; - } else { - msg.reply(ctx, "There was a problem getting the shard manager").await?; - - return Ok(()); - } - - Ok(()) -} diff --git a/examples/e06_sample_bot_structure/src/main.rs b/examples/e06_sample_bot_structure/src/main.rs deleted file mode 100644 index 5ebcc63f47d..00000000000 --- a/examples/e06_sample_bot_structure/src/main.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Requires the 'framework' feature flag be enabled in your project's `Cargo.toml`. -//! -//! This can be enabled by specifying the feature in the dependency section: -//! -//! ```toml -//! [dependencies.serenity] -//! git = "https://github.com/serenity-rs/serenity.git" -//! features = ["framework", "standard_framework"] -//! ``` -#![allow(deprecated)] // We recommend migrating to poise, instead of using the standard command framework. -mod commands; - -use std::collections::HashSet; -use std::env; -use std::sync::Arc; - -use serenity::async_trait; -use serenity::framework::standard::macros::group; -use serenity::framework::standard::Configuration; -use serenity::framework::StandardFramework; -use serenity::gateway::ShardManager; -use serenity::http::Http; -use serenity::model::event::ResumedEvent; -use serenity::model::gateway::Ready; -use serenity::prelude::*; -use tracing::{error, info}; - -use crate::commands::math::*; -use crate::commands::meta::*; -use crate::commands::owner::*; - -pub struct ShardManagerContainer; - -impl TypeMapKey for ShardManagerContainer { - type Value = Arc; -} - -struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn ready(&self, _: Context, ready: Ready) { - info!("Connected as {}", ready.user.name); - } - - async fn resume(&self, _: Context, _: ResumedEvent) { - info!("Resumed"); - } -} - -#[group] -#[commands(multiply, ping, quit)] -struct General; - -#[tokio::main] -async fn main() { - // This will load the environment variables located at `./.env`, relative to the CWD. - // See `./.env.example` for an example on how to structure this. - dotenv::dotenv().expect("Failed to load .env file"); - - // Initialize the logger to use environment variables. - // - // In this case, a good default is setting the environment variable `RUST_LOG` to `debug`. - tracing_subscriber::fmt::init(); - - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - - let http = Http::new(&token); - - // We will fetch your bot's owners and id - let (owners, _bot_id) = match http.get_current_application_info().await { - Ok(info) => { - let mut owners = HashSet::new(); - if let Some(owner) = &info.owner { - owners.insert(owner.id); - } - - (owners, info.id) - }, - Err(why) => panic!("Could not access application info: {:?}", why), - }; - - // Create the framework - let framework = StandardFramework::new().group(&GENERAL_GROUP); - framework.configure(Configuration::new().owners(owners).prefix("~")); - - let intents = GatewayIntents::GUILD_MESSAGES - | GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(&token, intents) - .framework(framework) - .event_handler(Handler) - .await - .expect("Err creating client"); - - { - let mut data = client.data.write().await; - data.insert::(client.shard_manager.clone()); - } - - let shard_manager = client.shard_manager.clone(); - - tokio::spawn(async move { - tokio::signal::ctrl_c().await.expect("Could not register ctrl+c handler"); - shard_manager.shutdown_all().await; - }); - - if let Err(why) = client.start().await { - error!("Client error: {:?}", why); - } -} diff --git a/examples/e08_shard_manager/Cargo.toml b/examples/e07_shard_manager/Cargo.toml similarity index 91% rename from examples/e08_shard_manager/Cargo.toml rename to examples/e07_shard_manager/Cargo.toml index b11f2acf544..8c6e041f79e 100644 --- a/examples/e08_shard_manager/Cargo.toml +++ b/examples/e07_shard_manager/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e08_shard_manager" +name = "e07_shard_manager" version = "0.1.0" authors = ["my name "] edition = "2018" diff --git a/examples/e06_sample_bot_structure/Makefile.toml b/examples/e07_shard_manager/Makefile.toml similarity index 100% rename from examples/e06_sample_bot_structure/Makefile.toml rename to examples/e07_shard_manager/Makefile.toml diff --git a/examples/e08_shard_manager/src/main.rs b/examples/e07_shard_manager/src/main.rs similarity index 100% rename from examples/e08_shard_manager/src/main.rs rename to examples/e07_shard_manager/src/main.rs diff --git a/examples/e09_create_message_builder/Cargo.toml b/examples/e08_create_message_builder/Cargo.toml similarity index 89% rename from examples/e09_create_message_builder/Cargo.toml rename to examples/e08_create_message_builder/Cargo.toml index b130703c887..272acf60660 100644 --- a/examples/e09_create_message_builder/Cargo.toml +++ b/examples/e08_create_message_builder/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e09_create_message_builder" +name = "e08_create_message_builder" version = "0.1.0" authors = ["my name "] edition = "2018" diff --git a/examples/e08_shard_manager/Makefile.toml b/examples/e08_create_message_builder/Makefile.toml similarity index 100% rename from examples/e08_shard_manager/Makefile.toml rename to examples/e08_create_message_builder/Makefile.toml diff --git a/examples/e09_create_message_builder/ferris_eyes.png b/examples/e08_create_message_builder/ferris_eyes.png similarity index 100% rename from examples/e09_create_message_builder/ferris_eyes.png rename to examples/e08_create_message_builder/ferris_eyes.png diff --git a/examples/e09_create_message_builder/src/main.rs b/examples/e08_create_message_builder/src/main.rs similarity index 98% rename from examples/e09_create_message_builder/src/main.rs rename to examples/e08_create_message_builder/src/main.rs index bb7561f1ca6..712713e4e50 100644 --- a/examples/e09_create_message_builder/src/main.rs +++ b/examples/e08_create_message_builder/src/main.rs @@ -22,7 +22,7 @@ impl EventHandler for Handler { .title("This is a title") .description("This is a description") .image("attachment://ferris_eyes.png") - .fields(vec![ + .fields([ ("This is the first field", "This is a field body", true), ("This is the second field", "Both fields are inline", true), ]) diff --git a/examples/e10_collectors/Cargo.toml b/examples/e09_collectors/Cargo.toml similarity index 67% rename from examples/e10_collectors/Cargo.toml rename to examples/e09_collectors/Cargo.toml index 148f47e3655..edae1438640 100644 --- a/examples/e10_collectors/Cargo.toml +++ b/examples/e09_collectors/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "e10_collectors" +name = "e09_collectors" version = "0.1.0" authors = ["my name "] edition = "2018" [dependencies.serenity] -features = ["framework", "standard_framework", "rustls_backend", "collector"] +features = ["framework", "rustls_backend", "collector"] path = "../../" [dependencies] diff --git a/examples/e09_create_message_builder/Makefile.toml b/examples/e09_collectors/Makefile.toml similarity index 100% rename from examples/e09_create_message_builder/Makefile.toml rename to examples/e09_collectors/Makefile.toml diff --git a/examples/e09_collectors/src/main.rs b/examples/e09_collectors/src/main.rs new file mode 100644 index 00000000000..8b9ed719eaa --- /dev/null +++ b/examples/e09_collectors/src/main.rs @@ -0,0 +1,153 @@ +//! This example will showcase the beauty of collectors. They allow to await messages or reactions +//! from a user in the middle of a control flow, one being a command. +use std::collections::HashSet; +use std::env; +use std::time::Duration; + +use serenity::async_trait; +use serenity::collector::MessageCollector; +// Collectors are streams, that means we can use `StreamExt` and `TryStreamExt`. +use serenity::futures::stream::StreamExt; +use serenity::model::prelude::*; +use serenity::prelude::*; + +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, _: Context, ready: Ready) { + println!("{} is connected!", ready.user.name); + } + + async fn message(&self, ctx: Context, msg: Message) { + let mut score = 0u32; + let _ = + msg.reply(&ctx.http, "How was that crusty crab called again? 10 seconds time!").await; + + // There is a method implemented for some models to conveniently collect replies. They + // return a builder that can be turned into a Stream, or here, where we can await a + // single reply + let collector = msg.author.await_reply(ctx.shard.clone()).timeout(Duration::from_secs(10)); + if let Some(answer) = collector.await { + if answer.content.to_lowercase() == "ferris" { + let _ = answer.reply(&ctx.http, "That's correct!").await; + score += 1; + } else { + let _ = answer.reply(&ctx.http, "Wrong, it's Ferris!").await; + } + } else { + let _ = msg.reply(&ctx.http, "No answer within 10 seconds.").await; + }; + + let react_msg = msg + .reply(&ctx.http, "React with the reaction representing 1, you got 10 seconds!") + .await + .unwrap(); + + // The message model can also be turned into a Collector to collect reactions on it. + let collector = react_msg + .await_reaction(ctx.shard.clone()) + .timeout(Duration::from_secs(10)) + .author_id(msg.author.id); + + if let Some(reaction) = collector.await { + let _ = if reaction.emoji.as_data() == "1️⃣" { + score += 1; + msg.reply(&ctx.http, "That's correct!").await + } else { + msg.reply(&ctx.http, "Wrong!").await + }; + } else { + let _ = msg.reply(&ctx.http, "No reaction within 10 seconds.").await; + }; + + let _ = msg.reply(&ctx.http, "Write 5 messages in 10 seconds").await; + + // We can create a collector from scratch too using this builder future. + let collector = MessageCollector::new(ctx.shard.clone()) + // Only collect messages by this user. + .author_id(msg.author.id) + .channel_id(msg.channel_id) + .timeout(Duration::from_secs(10)) + // Build the collector. + .stream() + .take(5); + + // Let's acquire borrow HTTP to send a message inside the `async move`. + let http = &ctx.http; + + // We want to process each message and get the length. There are a couple of ways to do + // this. Folding the stream with `fold` is one way. + // + // Using `then` to first reply and then create a new stream with all messages is another way + // to do it, which can be nice if you want to further process the messages. + // + // If you don't want to collect the stream, `for_each` may be sufficient. + let collected: Vec<_> = collector + .then(|msg| async move { + let _ = msg.reply(http, format!("I repeat: {}", msg.content)).await; + + msg + }) + .collect() + .await; + + if collected.len() >= 5 { + score += 1; + } + + // We can also collect arbitrary events using the collect() function. For example, here we + // collect updates to the messages that the user sent above and check for them updating all + // 5 of them. + let mut collector = serenity::collector::collect(&ctx.shard, move |event| match event { + // Only collect MessageUpdate events for the 5 MessageIds we're interested in. + Event::MessageUpdate(event) if collected.iter().any(|msg| event.id == msg.id) => { + Some(event.id) + }, + _ => None, + }) + .take_until(Box::pin(tokio::time::sleep(Duration::from_secs(20)))); + + let _ = msg.reply(&ctx.http, "Edit each of those 5 messages in 20 seconds").await; + let mut edited = HashSet::new(); + while let Some(edited_message_id) = collector.next().await { + edited.insert(edited_message_id); + if edited.len() >= 5 { + break; + } + } + + if edited.len() >= 5 { + score += 1; + let _ = msg.reply(&ctx.http, "Great! You edited 5 out of 5").await; + } else { + let _ = + msg.reply(&ctx.http, &format!("You only edited {} out of 5", edited.len())).await; + } + + let _ = msg + .reply( + &ctx.http, + &format!("TIME'S UP! You completed {score} out of 4 tasks correctly!"), + ) + .await; + } +} + +#[tokio::main] +async fn main() { + // Configure the client with your Discord bot token in the environment. + let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + + let intents = GatewayIntents::GUILD_MESSAGES + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::MESSAGE_CONTENT + | GatewayIntents::GUILD_MESSAGE_REACTIONS; + + let mut client = + Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + + if let Err(why) = client.start().await { + println!("Client error: {why:?}"); + } +} diff --git a/examples/e10_collectors/src/main.rs b/examples/e10_collectors/src/main.rs deleted file mode 100644 index c6f25852411..00000000000 --- a/examples/e10_collectors/src/main.rs +++ /dev/null @@ -1,200 +0,0 @@ -//! This example will showcase the beauty of collectors. They allow to await messages or reactions -//! from a user in the middle of a control flow, one being a command. -#![allow(deprecated)] // We recommend migrating to poise, instead of using the standard command framework. -use std::collections::HashSet; -use std::env; -use std::time::Duration; - -use serenity::async_trait; -use serenity::collector::MessageCollector; -use serenity::framework::standard::macros::{command, group, help}; -use serenity::framework::standard::{ - help_commands, - Args, - CommandGroup, - CommandResult, - Configuration, - HelpOptions, - StandardFramework, -}; -// Collectors are streams, that means we can use `StreamExt` and `TryStreamExt`. -use serenity::futures::stream::StreamExt; -use serenity::http::Http; -use serenity::model::prelude::*; -use serenity::prelude::*; - -#[group("collector")] -#[commands(challenge)] -struct Collector; - -#[help] -async fn my_help( - context: &Context, - msg: &Message, - args: Args, - help_options: &'static HelpOptions, - groups: &[&'static CommandGroup], - owners: HashSet, -) -> CommandResult { - let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await; - Ok(()) -} - -struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn ready(&self, _: Context, ready: Ready) { - println!("{} is connected!", ready.user.name); - } -} - -#[tokio::main] -async fn main() { - // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - - let http = Http::new(&token); - - // We will fetch your bot's id. - let bot_id = match http.get_current_user().await { - Ok(info) => info.id, - Err(why) => panic!("Could not access user info: {:?}", why), - }; - - let framework = StandardFramework::new().help(&MY_HELP).group(&COLLECTOR_GROUP); - - framework.configure( - Configuration::new() - .with_whitespace(true) - .on_mention(Some(bot_id)) - .prefix("~") - .delimiters(vec![", ", ","]), - ); - - let intents = GatewayIntents::GUILD_MESSAGES - | GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::MESSAGE_CONTENT - | GatewayIntents::GUILD_MESSAGE_REACTIONS; - - let mut client = Client::builder(&token, intents) - .event_handler(Handler) - .framework(framework) - .await - .expect("Err creating client"); - - if let Err(why) = client.start().await { - println!("Client error: {why:?}"); - } -} - -#[command] -async fn challenge(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let mut score = 0u32; - let _ = msg.reply(ctx, "How was that crusty crab called again? 10 seconds time!").await; - - // There is a method implemented for some models to conveniently collect replies. They return a - // builder that can be turned into a Stream, or here, where we can await a single reply - let collector = msg.author.await_reply(&ctx.shard).timeout(Duration::from_secs(10)); - if let Some(answer) = collector.await { - if answer.content.to_lowercase() == "ferris" { - let _ = answer.reply(ctx, "That's correct!").await; - score += 1; - } else { - let _ = answer.reply(ctx, "Wrong, it's Ferris!").await; - } - } else { - let _ = msg.reply(ctx, "No answer within 10 seconds.").await; - }; - - let react_msg = msg - .reply(ctx, "React with the reaction representing 1, you got 10 seconds!") - .await - .unwrap(); - - // The message model can also be turned into a Collector to collect reactions on it. - let collector = react_msg - .await_reaction(&ctx.shard) - .timeout(Duration::from_secs(10)) - .author_id(msg.author.id); - - if let Some(reaction) = collector.await { - let _ = if reaction.emoji.as_data() == "1️⃣" { - score += 1; - msg.reply(ctx, "That's correct!").await - } else { - msg.reply(ctx, "Wrong!").await - }; - } else { - let _ = msg.reply(ctx, "No reaction within 10 seconds.").await; - }; - - let _ = msg.reply(ctx, "Write 5 messages in 10 seconds").await; - - // We can create a collector from scratch too using this builder future. - let collector = MessageCollector::new(&ctx.shard) - // Only collect messages by this user. - .author_id(msg.author.id) - .channel_id(msg.channel_id) - .timeout(Duration::from_secs(10)) - // Build the collector. - .stream() - .take(5); - - // Let's acquire borrow HTTP to send a message inside the `async move`. - let http = &ctx.http; - - // We want to process each message and get the length. There are a couple of ways to do this. - // Folding the stream with `fold` is one way. - // - // Using `then` to first reply and then create a new stream with all messages is another way to - // do it, which can be nice if you want to further process the messages. - // - // If you don't want to collect the stream, `for_each` may be sufficient. - let collected: Vec<_> = collector - .then(|msg| async move { - let _ = msg.reply(http, format!("I repeat: {}", msg.content)).await; - - msg - }) - .collect() - .await; - - if collected.len() >= 5 { - score += 1; - } - - // We can also collect arbitrary events using the collect() function. For example, here we - // collect updates to the messages that the user sent above and check for them updating all 5 - // of them. - let mut collector = serenity::collector::collect(&ctx.shard, move |event| match event { - // Only collect MessageUpdate events for the 5 MessageIds we're interested in. - Event::MessageUpdate(event) if collected.iter().any(|msg| event.id == msg.id) => { - Some(event.id) - }, - _ => None, - }) - .take_until(Box::pin(tokio::time::sleep(Duration::from_secs(20)))); - - let _ = msg.reply(ctx, "Edit each of those 5 messages in 20 seconds").await; - let mut edited = HashSet::new(); - while let Some(edited_message_id) = collector.next().await { - edited.insert(edited_message_id); - if edited.len() >= 5 { - break; - } - } - - if edited.len() >= 5 { - score += 1; - let _ = msg.reply(ctx, "Great! You edited 5 out of 5").await; - } else { - let _ = msg.reply(ctx, &format!("You only edited {} out of 5", edited.len())).await; - } - - let _ = msg - .reply(ctx, &format!("TIME'S UP! You completed {score} out of 4 tasks correctly!")) - .await; - - Ok(()) -} diff --git a/examples/e11_gateway_intents/Cargo.toml b/examples/e10_gateway_intents/Cargo.toml similarity index 90% rename from examples/e11_gateway_intents/Cargo.toml rename to examples/e10_gateway_intents/Cargo.toml index 34ec998c5ca..2801bc6a2ff 100644 --- a/examples/e11_gateway_intents/Cargo.toml +++ b/examples/e10_gateway_intents/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e11_gateway_intents" +name = "e10_gateway_intents" version = "0.1.0" authors = ["my name "] edition = "2018" diff --git a/examples/e10_collectors/Makefile.toml b/examples/e10_gateway_intents/Makefile.toml similarity index 100% rename from examples/e10_collectors/Makefile.toml rename to examples/e10_gateway_intents/Makefile.toml diff --git a/examples/e11_gateway_intents/src/main.rs b/examples/e10_gateway_intents/src/main.rs similarity index 88% rename from examples/e11_gateway_intents/src/main.rs rename to examples/e10_gateway_intents/src/main.rs index db846bc25ea..ea1ffeca732 100644 --- a/examples/e11_gateway_intents/src/main.rs +++ b/examples/e10_gateway_intents/src/main.rs @@ -16,7 +16,12 @@ impl EventHandler for Handler { // As the intents set in this example, this event shall never be dispatched. // Try it by changing your status. - async fn presence_update(&self, _ctx: Context, _new_data: Presence) { + async fn presence_update( + &self, + _ctx: Context, + _old_data: Option, + _new_data: Presence, + ) { println!("Presence Update"); } @@ -34,7 +39,7 @@ async fn main() { let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; // Build our client. - let mut client = Client::builder(token, intents) + let mut client = Client::builder(&token, intents) .event_handler(Handler) .await .expect("Error creating client"); diff --git a/examples/e12_global_data/Cargo.toml b/examples/e11_global_data/Cargo.toml similarity index 89% rename from examples/e12_global_data/Cargo.toml rename to examples/e11_global_data/Cargo.toml index 0244a75f70c..2a3f10a7a7a 100644 --- a/examples/e12_global_data/Cargo.toml +++ b/examples/e11_global_data/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e12_global_data" +name = "e11_global_data" version = "0.1.0" authors = ["my name "] edition = "2018" diff --git a/examples/e11_gateway_intents/Makefile.toml b/examples/e11_global_data/Makefile.toml similarity index 100% rename from examples/e11_gateway_intents/Makefile.toml rename to examples/e11_global_data/Makefile.toml diff --git a/examples/e11_global_data/src/main.rs b/examples/e11_global_data/src/main.rs new file mode 100644 index 00000000000..e4c2ad81d49 --- /dev/null +++ b/examples/e11_global_data/src/main.rs @@ -0,0 +1,87 @@ +//! In this example, you will be shown how to share data between events. + +use std::borrow::Cow; +use std::env; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +use serenity::async_trait; +use serenity::model::channel::Message; +use serenity::model::gateway::Ready; +use serenity::prelude::*; + +// A container type is created for inserting into the Client's `data`, which allows for data to be +// accessible across all events or anywhere else that has a copy of the `data` Arc. +// These places are usually where either Context or Client is present. +struct UserData { + message_count: AtomicUsize, +} + +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn message(&self, ctx: Context, msg: Message) { + // Since data is located in Context, this means you are able to use it within events! + let data = ctx.data::(); + + // We are verifying if the bot id is the same as the message author id. + let owo_count = if msg.author.id != ctx.cache.current_user().id + && msg.content.to_lowercase().contains("owo") + { + // Here, we are checking how many "owo" there are in the message content. + let owo_in_msg = msg.content.to_ascii_lowercase().matches("owo").count(); + + // Atomic operations with ordering do not require mut to be modified. + // In this case, we want to increase the message count by 1. + // https://doc.rust-lang.org/std/sync/atomic/struct.AtomicUsize.html#method.fetch_add + data.message_count.fetch_add(owo_in_msg, Ordering::SeqCst) + 1 + } else { + // We don't need to check for "owo_count" if "owo" isn't in the message! + return; + }; + + if msg.content.starts_with("~owo_count") { + let response = if owo_count == 1 { + Cow::Borrowed("You are the first one to say owo this session! *because it's on the command name* :P") + } else { + Cow::Owned(format!("OWO Has been said {owo_count} times!")) + }; + + if let Err(err) = msg.reply(&ctx.http, response).await { + eprintln!("Error sending response: {err:?}") + }; + } + } + + async fn ready(&self, _: Context, ready: Ready) { + println!("{} is connected!", ready.user.name); + } +} + +#[tokio::main] +async fn main() { + let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + + // We setup the initial value for our user data, which we will use throughout the rest of our + // program. + let data = UserData { + message_count: AtomicUsize::new(0), + }; + + let intents = GatewayIntents::GUILD_MESSAGES + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::MESSAGE_CONTENT; + let mut client = Client::builder(&token, intents) + // Specifying the data type as a type argument here is optional, but if done, you can + // guarantee that Context::data will not panic if the same type is given, as providing the + // incorrect type will lead to a compiler error, rather than a runtime panic. + .data::(Arc::new(data)) + .event_handler(Handler) + .await + .expect("Err creating client"); + + if let Err(why) = client.start().await { + eprintln!("Client error: {why:?}"); + } +} diff --git a/examples/e12_global_data/src/main.rs b/examples/e12_global_data/src/main.rs deleted file mode 100644 index 5311066094f..00000000000 --- a/examples/e12_global_data/src/main.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! In this example, you will be shown various ways of sharing data between events and commands. -//! And how to use locks correctly to avoid deadlocking the bot. -#![allow(deprecated)] // We recommend migrating to poise, instead of using the standard command framework. - -use std::collections::HashMap; -use std::env; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -use serenity::async_trait; -use serenity::framework::standard::macros::{command, group, hook}; -use serenity::framework::standard::{Args, CommandResult, Configuration, StandardFramework}; -use serenity::model::channel::Message; -use serenity::model::gateway::Ready; -use serenity::prelude::*; - -// A container type is created for inserting into the Client's `data`, which allows for data to be -// accessible across all events and framework commands, or anywhere else that has a copy of the -// `data` Arc. These places are usually where either Context or Client is present. -// -// Documentation about TypeMap can be found here: -// https://docs.rs/typemap_rev/0.1/typemap_rev/struct.TypeMap.html -struct CommandCounter; - -impl TypeMapKey for CommandCounter { - type Value = Arc>>; -} - -struct MessageCount; - -impl TypeMapKey for MessageCount { - // While you will be using RwLock or Mutex most of the time you want to modify data, sometimes - // it's not required; like for example, with static data, or if you are using other kinds of - // atomic operators. - // - // Arc should stay, to allow for the data lock to be closed early. - type Value = Arc; -} - -#[group] -#[commands(ping, command_usage, owo_count)] -struct General; - -#[hook] -async fn before(ctx: &Context, msg: &Message, command_name: &str) -> bool { - println!("Running command '{}' invoked by '{}'", command_name, msg.author.tag()); - - let counter_lock = { - // While data is a RwLock, it's recommended that you always open the lock as read. This is - // mainly done to avoid Deadlocks for having a possible writer waiting for multiple readers - // to close. - let data_read = ctx.data.read().await; - - // Since the CommandCounter Value is wrapped in an Arc, cloning will not duplicate the - // data, instead the reference is cloned. - // We wrap every value on in an Arc, as to keep the data lock open for the least time - // possible, to again, avoid deadlocking it. - data_read.get::().expect("Expected CommandCounter in TypeMap.").clone() - }; - - // Just like with client.data in main, we want to keep write locks open the least time - // possible, so we wrap them on a block so they get automatically closed at the end. - { - // The HashMap of CommandCounter is wrapped in an RwLock; since we want to write to it, we - // will open the lock in write mode. - let mut counter = counter_lock.write().await; - - // And we write the amount of times the command has been called to it. - let entry = counter.entry(command_name.to_string()).or_insert(0); - *entry += 1; - } - - true -} - -struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - // We are verifying if the bot id is the same as the message author id. - if msg.author.id != ctx.cache.current_user().id - && msg.content.to_lowercase().contains("owo") - { - // Since data is located in Context, this means you are able to use it within events! - let count = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected MessageCount in TypeMap.").clone() - }; - - // Here, we are checking how many "owo" there are in the message content. - let owo_in_msg = msg.content.to_ascii_lowercase().matches("owo").count(); - - // Atomic operations with ordering do not require mut to be modified. - // In this case, we want to increase the message count by 1. - // https://doc.rust-lang.org/std/sync/atomic/struct.AtomicUsize.html#method.fetch_add - count.fetch_add(owo_in_msg, Ordering::SeqCst); - } - } - - async fn ready(&self, _: Context, ready: Ready) { - println!("{} is connected!", ready.user.name); - } -} - -#[tokio::main] -async fn main() { - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - - let framework = StandardFramework::new().before(before).group(&GENERAL_GROUP); - framework.configure(Configuration::new().with_whitespace(true).prefix("~")); - - let intents = GatewayIntents::GUILD_MESSAGES - | GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(&token, intents) - .event_handler(Handler) - .framework(framework) - .await - .expect("Err creating client"); - - // This is where we can initially insert the data we desire into the "global" data TypeMap. - // client.data is wrapped on a RwLock, and since we want to insert to it, we have to open it in - // write mode, but there's a small thing catch: There can only be a single writer to a given - // lock open in the entire application, this means you can't open a new write lock until the - // previous write lock has closed. This is not the case with read locks, read locks can be open - // indefinitely, BUT as soon as you need to open the lock in write mode, all the read locks - // must be closed. - // - // You can find more information about deadlocks in the Rust Book, ch16-03: - // https://doc.rust-lang.org/book/ch16-03-shared-state.html - // - // All of this means that we have to keep locks open for the least time possible, so we put - // them inside a block, so they get closed automatically when dropped. If we don't do this, we - // would never be able to open the data lock anywhere else. - // - // Alternatively, you can also use `ClientBuilder::type_map_insert` or - // `ClientBuilder::type_map` to populate the global TypeMap without dealing with the RwLock. - { - // Open the data lock in write mode, so keys can be inserted to it. - let mut data = client.data.write().await; - - // The CommandCounter Value has the type: Arc>> - // So, we have to insert the same type to it. - data.insert::(Arc::new(RwLock::new(HashMap::default()))); - - data.insert::(Arc::new(AtomicUsize::new(0))); - } - - if let Err(why) = client.start().await { - eprintln!("Client error: {why:?}"); - } -} - -#[command] -async fn ping(ctx: &Context, msg: &Message) -> CommandResult { - msg.reply(ctx, "Pong!").await?; - - Ok(()) -} - -/// Usage: `~command_usage ` -/// Example: `~command_usage ping` -#[command] -async fn command_usage(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let command_name = match args.single_quoted::() { - Ok(x) => x, - Err(_) => { - msg.reply(ctx, "I require an argument to run this command.").await?; - return Ok(()); - }, - }; - - // Yet again, we want to keep the locks open for the least time possible. - let amount = { - // Since we only want to read the data and not write to it, we open it in read mode, and - // since this is open in read mode, it means that there can be multiple locks open at the - // same time, and as mentioned earlier, it's heavily recommended that you only open the - // data lock in read mode, as it will avoid a lot of possible deadlocks. - let data_read = ctx.data.read().await; - - // Then we obtain the value we need from data, in this case, we want the command counter. - // The returned value from get() is an Arc, so the reference will be cloned, rather than - // the data. - let command_counter_lock = - data_read.get::().expect("Expected CommandCounter in TypeMap.").clone(); - - let command_counter = command_counter_lock.read().await; - // And we return a usable value from it. - // This time, the value is not Arc, so the data will be cloned. - command_counter.get(&command_name).map_or(0, |x| *x) - }; - - if amount == 0 { - msg.reply(ctx, format!("The command `{command_name}` has not yet been used.")).await?; - } else { - msg.reply( - ctx, - format!("The command `{command_name}` has been used {amount} time/s this session!"), - ) - .await?; - } - - Ok(()) -} - -#[command] -async fn owo_count(ctx: &Context, msg: &Message) -> CommandResult { - let raw_count = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected MessageCount in TypeMap.").clone() - }; - - let count = raw_count.load(Ordering::Relaxed); - - if count == 1 { - msg.reply( - ctx, - "You are the first one to say owo this session! *because it's on the command name* :P", - ) - .await?; - } else { - msg.reply(ctx, format!("OWO Has been said {count} times!")).await?; - } - - Ok(()) -} diff --git a/examples/e13_parallel_loops/Cargo.toml b/examples/e12_parallel_loops/Cargo.toml similarity index 93% rename from examples/e13_parallel_loops/Cargo.toml rename to examples/e12_parallel_loops/Cargo.toml index a682559aa62..fca117aa19e 100644 --- a/examples/e13_parallel_loops/Cargo.toml +++ b/examples/e12_parallel_loops/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e13_parallel_loops" +name = "e12_parallel_loops" version = "0.1.0" authors = ["my name "] edition = "2018" diff --git a/examples/e12_global_data/Makefile.toml b/examples/e12_parallel_loops/Makefile.toml similarity index 100% rename from examples/e12_global_data/Makefile.toml rename to examples/e12_parallel_loops/Makefile.toml diff --git a/examples/e13_parallel_loops/src/main.rs b/examples/e12_parallel_loops/src/main.rs similarity index 90% rename from examples/e13_parallel_loops/src/main.rs rename to examples/e12_parallel_loops/src/main.rs index 14fc79c2737..b0bca910064 100644 --- a/examples/e13_parallel_loops/src/main.rs +++ b/examples/e12_parallel_loops/src/main.rs @@ -1,6 +1,5 @@ use std::env; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; use std::time::Duration; use chrono::offset::Utc; @@ -35,10 +34,6 @@ impl EventHandler for Handler { async fn cache_ready(&self, ctx: Context, _guilds: Vec) { println!("Cache built successfully!"); - // It's safe to clone Context, but Arc is cheaper for this use case. - // Untested claim, just theoretically. :P - let ctx = Arc::new(ctx); - // We need to check that the loop is not already running when this event triggers, as this // event triggers every time the bot enters or leaves a guild, along every time the ready // shard event triggers. @@ -46,8 +41,8 @@ impl EventHandler for Handler { // An AtomicBool is used because it doesn't require a mutable reference to be changed, as // we don't have one due to self being an immutable reference. if !self.is_loop_running.load(Ordering::Relaxed) { - // We have to clone the Arc, as it gets moved into the new thread. - let ctx1 = Arc::clone(&ctx); + // We have to clone the ctx, as it gets moved into the new thread. + let ctx1 = ctx.clone(); // tokio::spawn creates a new green thread that can run in parallel with the rest of // the application. tokio::spawn(async move { @@ -58,10 +53,9 @@ impl EventHandler for Handler { }); // And of course, we can run more than one thread at different timings. - let ctx2 = Arc::clone(&ctx); tokio::spawn(async move { loop { - set_activity_to_current_time(&ctx2); + set_activity_to_current_time(&ctx); tokio::time::sleep(Duration::from_secs(60)).await; } }); @@ -91,7 +85,7 @@ async fn log_system_load(ctx: &Context) { false, ); let builder = CreateMessage::new().embed(embed); - let message = ChannelId::new(381926291785383946).send_message(&ctx, builder).await; + let message = ChannelId::new(381926291785383946).send_message(&ctx.http, builder).await; if let Err(why) = message { eprintln!("Error sending message: {why:?}"); }; diff --git a/examples/e16_sqlite_database/.gitignore b/examples/e13_sqlite_database/.gitignore similarity index 100% rename from examples/e16_sqlite_database/.gitignore rename to examples/e13_sqlite_database/.gitignore diff --git a/examples/e16_sqlite_database/.sqlx/query-597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678.json b/examples/e13_sqlite_database/.sqlx/query-597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678.json similarity index 100% rename from examples/e16_sqlite_database/.sqlx/query-597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678.json rename to examples/e13_sqlite_database/.sqlx/query-597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678.json diff --git a/examples/e16_sqlite_database/.sqlx/query-7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1.json b/examples/e13_sqlite_database/.sqlx/query-7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1.json similarity index 100% rename from examples/e16_sqlite_database/.sqlx/query-7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1.json rename to examples/e13_sqlite_database/.sqlx/query-7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1.json diff --git a/examples/e16_sqlite_database/.sqlx/query-8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb.json b/examples/e13_sqlite_database/.sqlx/query-8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb.json similarity index 100% rename from examples/e16_sqlite_database/.sqlx/query-8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb.json rename to examples/e13_sqlite_database/.sqlx/query-8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb.json diff --git a/examples/e16_sqlite_database/.sqlx/query-90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a.json b/examples/e13_sqlite_database/.sqlx/query-90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a.json similarity index 100% rename from examples/e16_sqlite_database/.sqlx/query-90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a.json rename to examples/e13_sqlite_database/.sqlx/query-90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a.json diff --git a/examples/e16_sqlite_database/Cargo.toml b/examples/e13_sqlite_database/Cargo.toml similarity index 92% rename from examples/e16_sqlite_database/Cargo.toml rename to examples/e13_sqlite_database/Cargo.toml index eeda2a295b1..5e62110b920 100644 --- a/examples/e16_sqlite_database/Cargo.toml +++ b/examples/e13_sqlite_database/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e16_sqlite_database" +name = "e13_sqlite_database" version = "0.1.0" authors = ["my name "] edition = "2018" diff --git a/examples/e13_parallel_loops/Makefile.toml b/examples/e13_sqlite_database/Makefile.toml similarity index 100% rename from examples/e13_parallel_loops/Makefile.toml rename to examples/e13_sqlite_database/Makefile.toml diff --git a/examples/e16_sqlite_database/README.md b/examples/e13_sqlite_database/README.md similarity index 100% rename from examples/e16_sqlite_database/README.md rename to examples/e13_sqlite_database/README.md diff --git a/examples/e16_sqlite_database/migrations/20210906145552_initial_migration.sql b/examples/e13_sqlite_database/migrations/20210906145552_initial_migration.sql similarity index 100% rename from examples/e16_sqlite_database/migrations/20210906145552_initial_migration.sql rename to examples/e13_sqlite_database/migrations/20210906145552_initial_migration.sql diff --git a/examples/e16_sqlite_database/pre-commit b/examples/e13_sqlite_database/pre-commit old mode 100755 new mode 100644 similarity index 100% rename from examples/e16_sqlite_database/pre-commit rename to examples/e13_sqlite_database/pre-commit diff --git a/examples/e16_sqlite_database/src/main.rs b/examples/e13_sqlite_database/src/main.rs similarity index 95% rename from examples/e16_sqlite_database/src/main.rs rename to examples/e13_sqlite_database/src/main.rs index 11e4b500f87..f8bf5d4d2e9 100644 --- a/examples/e16_sqlite_database/src/main.rs +++ b/examples/e13_sqlite_database/src/main.rs @@ -30,7 +30,7 @@ impl EventHandler for Bot { .unwrap(); let response = format!("Successfully added `{task_description}` to your todo list"); - msg.channel_id.say(&ctx, response).await.unwrap(); + msg.channel_id.say(&ctx.http, response).await.unwrap(); } else if let Some(task_index) = msg.content.strip_prefix("~todo remove") { let task_index = task_index.trim().parse::().unwrap() - 1; @@ -51,7 +51,7 @@ impl EventHandler for Bot { .unwrap(); let response = format!("Successfully completed `{}`!", entry.task); - msg.channel_id.say(&ctx, response).await.unwrap(); + msg.channel_id.say(&ctx.http, response).await.unwrap(); } else if msg.content.trim() == "~todo list" { // "SELECT" will return the task of all rows where user_Id column = user_id in todo. let todos = sqlx::query!("SELECT task FROM todo WHERE user_id = ? ORDER BY rowid", user_id) @@ -64,7 +64,7 @@ impl EventHandler for Bot { writeln!(response, "{}. {}", i + 1, todo.task).unwrap(); } - msg.channel_id.say(&ctx, response).await.unwrap(); + msg.channel_id.say(&ctx.http, response).await.unwrap(); } } } diff --git a/examples/e17_message_components/Cargo.toml b/examples/e14_message_components/Cargo.toml similarity index 91% rename from examples/e17_message_components/Cargo.toml rename to examples/e14_message_components/Cargo.toml index f34f727f55c..2c92496f6ea 100644 --- a/examples/e17_message_components/Cargo.toml +++ b/examples/e14_message_components/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e17_message_components" +name = "e14_message_components" version = "0.1.0" authors = ["my name "] edition = "2018" diff --git a/examples/e14_slash_commands/Makefile.toml b/examples/e14_message_components/Makefile.toml similarity index 100% rename from examples/e14_slash_commands/Makefile.toml rename to examples/e14_message_components/Makefile.toml diff --git a/examples/e17_message_components/src/main.rs b/examples/e14_message_components/src/main.rs similarity index 90% rename from examples/e17_message_components/src/main.rs rename to examples/e14_message_components/src/main.rs index c70203354a9..abad0b533df 100644 --- a/examples/e17_message_components/src/main.rs +++ b/examples/e14_message_components/src/main.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::env; use std::time::Duration; @@ -36,16 +37,16 @@ impl EventHandler for Handler { let m = msg .channel_id .send_message( - &ctx, + &ctx.http, CreateMessage::new().content("Please select your favorite animal").select_menu( CreateSelectMenu::new("animal_select", CreateSelectMenuKind::String { - options: vec![ + options: Cow::Borrowed(&[ CreateSelectMenuOption::new("🐈 meow", "Cat"), CreateSelectMenuOption::new("🐕 woof", "Dog"), CreateSelectMenuOption::new("🐎 neigh", "Horse"), CreateSelectMenuOption::new("🦙 hoooooooonk", "Alpaca"), CreateSelectMenuOption::new("🦀 crab rave", "Ferris"), - ], + ]), }) .custom_id("animal_select") .placeholder("No animal selected"), @@ -58,13 +59,13 @@ impl EventHandler for Handler { // This uses a collector to wait for an incoming event without needing to listen for it // manually in the EventHandler. let interaction = match m - .await_component_interaction(&ctx.shard) + .await_component_interaction(ctx.shard.clone()) .timeout(Duration::from_secs(60 * 3)) .await { Some(x) => x, None => { - m.reply(&ctx, "Timed out").await.unwrap(); + m.reply(&ctx.http, "Timed out").await.unwrap(); return; }, }; @@ -81,7 +82,7 @@ impl EventHandler for Handler { // Acknowledge the interaction and edit the message interaction .create_response( - &ctx, + &ctx.http, CreateInteractionResponse::UpdateMessage( CreateInteractionResponseMessage::default() .content(format!("You chose: **{animal}**\nNow choose a sound!")) @@ -105,15 +106,17 @@ impl EventHandler for Handler { .unwrap(); // Wait for multiple interactions - let mut interaction_stream = - m.await_component_interaction(&ctx.shard).timeout(Duration::from_secs(60 * 3)).stream(); + let mut interaction_stream = m + .await_component_interaction(ctx.shard.clone()) + .timeout(Duration::from_secs(60 * 3)) + .stream(); while let Some(interaction) = interaction_stream.next().await { let sound = &interaction.data.custom_id; // Acknowledge the interaction and send a reply interaction .create_response( - &ctx, + &ctx.http, // This time we dont edit the message but reply to it CreateInteractionResponse::Message( CreateInteractionResponseMessage::default() @@ -128,7 +131,7 @@ impl EventHandler for Handler { // Delete the orig message or there will be dangling components (components that still // exist, but no collector is running so any user who presses them sees an error) - m.delete(&ctx).await.unwrap() + m.delete(&ctx.http, None).await.unwrap() } } @@ -142,7 +145,7 @@ async fn main() { let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(token, intents) + let mut client = Client::builder(&token, intents) .event_handler(Handler) .await .expect("Error creating client"); diff --git a/examples/e15_simple_dashboard/Cargo.toml b/examples/e15_simple_dashboard/Cargo.toml deleted file mode 100644 index 153323df7d4..00000000000 --- a/examples/e15_simple_dashboard/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "e15_simple_dashboard" -version = "0.1.0" -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -rillrate = "0.41" -notify = "=5.0.0-pre.14" - -tracing = "0.1" -tracing-subscriber = "0.3" - -webbrowser = "0.8" - -[dependencies.serenity] -path = "../../" - -[dependencies.tokio] -version = "1" -features = ["full"] - -[dependencies.reqwest] -version = "0.11" -default-features = false -features = ["json", "rustls-tls"] - -[features] -post-ping = [] diff --git a/examples/e15_simple_dashboard/src/main.rs b/examples/e15_simple_dashboard/src/main.rs deleted file mode 100644 index 519c2f7d9bc..00000000000 --- a/examples/e15_simple_dashboard/src/main.rs +++ /dev/null @@ -1,475 +0,0 @@ -//! This example shows how you can use `rillrate` to create a web dashboard for your bot! -//! -//! This example is considered advanced and requires the knowledge of other examples. -//! Example 5 is needed for the Gateway latency and Framework usage. -//! Example 7 is needed because tracing is being used. -//! Example 12 is needed because global data and atomic are used. -//! Example 13 is needed for the parallel loops that are running to update data from the dashboard. -#![allow(deprecated)] // We recommend migrating to poise, instead of using the standard command framework. - -// be lazy, import all macros globally! -#[macro_use] -extern crate tracing; - -use std::collections::HashMap; -use std::env; -use std::error::Error; -use std::sync::atomic::*; -use std::sync::Arc; -use std::time::Instant; - -use rillrate::prime::table::{Col, Row}; -use rillrate::prime::*; -use serenity::async_trait; -use serenity::framework::standard::macros::{command, group, hook}; -use serenity::framework::standard::{CommandResult, Configuration, StandardFramework}; -use serenity::gateway::ShardManager; -use serenity::model::prelude::*; -use serenity::prelude::*; -use tokio::time::{sleep, Duration}; - -// Name used to group dashboards. -// You could have multiple packages for different applications, such as a package for the bot -// dashboards, and another package for a web server running alongside the bot. -const PACKAGE: &str = "Bot Dashboards"; -// Dashboards are a part inside of package, they can be used to group different types of dashboards -// that you may want to use, like a dashboard for system status, another dashboard for cache -// status, and another one to configure features or trigger actions on the bot. -const DASHBOARD_STATS: &str = "Statistics"; -const DASHBOARD_CONFIG: &str = "Config Dashboard"; -// This are collapsible menus inside the dashboard, you can use them to group specific sets of data -// inside the same dashboard. -// If you are using constants for this, make sure they don't end in _GROUP or _COMMAND, because -// serenity's command framework uses these internally. -const GROUP_LATENCY: &str = "1 - Discord Latency"; -const GROUP_COMMAND_COUNT: &str = "2 - Command Trigger Count"; -const GROUP_CONF: &str = "1 - Switch Command Configuration"; -// All of the 3 configurable namescapes are sorted alphabetically. - -#[derive(Debug, Clone)] -struct CommandUsageValue { - index: usize, - use_count: usize, -} - -struct Components { - data_switch: AtomicBool, - double_link_value: AtomicU8, - ws_ping_history: Pulse, - get_ping_history: Pulse, - #[cfg(feature = "post-ping")] - post_ping_history: Pulse, - command_usage_table: Table, - command_usage_values: Mutex>, -} - -struct RillRateComponents; - -impl TypeMapKey for RillRateComponents { - // RillRate element types have internal mutability, so we don't need RwLock nor Mutex! - // We do still want to Arc the type so it can be cloned out of `ctx.data`. - // If you wanna bind data between RillRate and the bot that doesn't have Atomics, use fields - // that use RwLock or Mutex, rather than making the enirety of Components one of them, like - // it's being done with `command_usage_values` this will make it considerably less likely to - // deadlock. - type Value = Arc; -} - -struct ShardManagerContainer; - -impl TypeMapKey for ShardManagerContainer { - type Value = Arc; -} - -#[group] -#[commands(ping, switch)] -struct General; - -struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn ready(&self, _ctx: Context, ready: Ready) { - info!("{} is connected!", ready.user.name); - } - - async fn cache_ready(&self, ctx: Context, _guilds: Vec) { - info!("Cache is ready!"); - - let switch = Switch::new( - [PACKAGE, DASHBOARD_CONFIG, GROUP_CONF, "Toggle Switch"], - SwitchOpts::default().label("Switch Me and run the `~switch` command!"), - ); - let switch_instance = switch.clone(); - - let ctx_clone = ctx.clone(); - - tokio::spawn(async move { - // There's currently no way to read the current data stored on RillRate types, so we - // use our own external method of storage, in this case since a switch is essentially - // just a boolean, we use an AtomicBool, stored on the same Components structure. - let elements = { - let data_read = ctx_clone.data.read().await; - data_read.get::().unwrap().clone() - }; - - switch.sync_callback(move |envelope| { - if let Some(action) = envelope.action { - debug!("Switch action: {:?}", action); - - // Here we toggle our internal state for the switch. - elements.data_switch.swap(action, Ordering::Relaxed); - - // If you click the switch, it won't turn on by itself, it will just send an - // event about it's new status. - // We need to manually set the switch to that status. - // If we do it at the end, we can make sure the switch switches it's status - // only if the action was successful. - switch_instance.apply(action); - } - - Ok(()) - }); - }); - - let default_values = { - let mut values = vec![]; - for i in u8::MIN..=u8::MAX { - if i % 32 == 0 { - values.push(i.to_string()) - } - } - values - }; - - // You are also able to have different actions in different elements interact with the same - // data. - // In this example, we have a Selector with preset data, and a Slider for more fine grain - // control of the value. - let selector = Selector::new( - [PACKAGE, DASHBOARD_CONFIG, GROUP_CONF, "Value Selector"], - SelectorOpts::default() - .label("Select from a preset of values!") - .options(default_values), - ); - let selector_instance = selector.clone(); - - let slider = Slider::new( - [PACKAGE, DASHBOARD_CONFIG, GROUP_CONF, "Value Slider"], - SliderOpts::default() - .label("Or slide me for more fine grain control!") - .min(u8::MIN as f64) - .max(u8::MAX as f64) - .step(2), - ); - let slider_instance = slider.clone(); - - let ctx_clone = ctx.clone(); - - tokio::spawn(async move { - let elements = { - let data_read = ctx_clone.data.read().await; - data_read.get::().unwrap().clone() - }; - - selector.sync_callback(move |envelope| { - let mut value: Option = None; - - if let Some(action) = envelope.action { - debug!("Values action (selector): {:?}", action); - value = action.map(|val| val.parse().unwrap()); - } - - if let Some(val) = value { - elements.double_link_value.swap(val, Ordering::Relaxed); - - // This is the selector callback, yet we are switching the data from the - // slider, this is to make sure both fields share the same look in the - // dashboard. - slider_instance.apply(val as f64); - } - - // the sync_callback() closure wants a Result value returned. - Ok(()) - }); - }); - - let ctx_clone = ctx.clone(); - - tokio::spawn(async move { - let elements = { - let data_read = ctx_clone.data.read().await; - data_read.get::().unwrap().clone() - }; - - // Because sync_callback() waits for an action to happen to it's element, we cannot - // have both in the same thread, rather we need to listen to them in parallel, but - // still have both modify the same value in the end. - slider.sync_callback(move |envelope| { - let mut value: Option = None; - - if let Some(action) = envelope.action { - debug!("Values action (slider): {:?}", action); - value = Some(action as u8); - } - - if let Some(val) = value { - elements.double_link_value.swap(val, Ordering::Relaxed); - - selector_instance.apply(Some(val.to_string())); - } - - Ok(()) - }); - }); - - let ctx_clone = ctx.clone(); - - tokio::spawn(async move { - let elements = { - let data_read = ctx_clone.data.read().await; - data_read.get::().unwrap().clone() - }; - - loop { - // Get the REST GET latency by counting how long it takes to do a GET request. - let get_latency = { - let now = Instant::now(); - // `let _` to suppress any errors. If they are a timeout, that will be - // reflected in the plotted graph. - let _ = reqwest::get("https://discordapp.com/api/v6/gateway").await; - now.elapsed().as_millis() as f64 - }; - - // POST Request is feature gated because discord doesn't like bots doing repeated - // tasks in short time periods, as they are considered API abuse; this is specially - // true on bigger bots. If you still wanna see this function though, compile the - // code adding `--features post-ping` to the command. - // - // Get the REST POST latency by posting a message to #testing. - // - // If you don't want to spam, use the DM channel of some random bot, or use some - // other kind of POST request such as reacting to a message, or creating an invite. - // Be aware that if the http request fails, the latency returned may be incorrect. - #[cfg(feature = "post-ping")] - let post_latency = { - let now = Instant::now(); - let _ = - ChannelId::new(381926291785383946).say(&ctx_clone, "Latency Test").await; - now.elapsed().as_millis() as f64 - }; - - // Get the Gateway Heartbeat latency. - // See example 5 for more information about the ShardManager latency. - let ws_latency = { - let data_read = ctx.data.read().await; - let shard_manager = data_read.get::().unwrap(); - - let runners = shard_manager.runners.lock().await; - - let runner = runners.get(&ctx.shard_id).unwrap(); - - if let Some(duration) = runner.latency { - duration.as_millis() as f64 - } else { - f64::NAN // effectively 0.0ms, it won't display on the graph. - } - }; - - elements.ws_ping_history.push(ws_latency); - elements.get_ping_history.push(get_latency); - #[cfg(feature = "post-ping")] - elements.post_ping_history.push(post_latency); - - // Update every heartbeat, when the ws latency also updates. - sleep(Duration::from_millis(42500)).await; - } - }); - } -} - -#[hook] -async fn before_hook(ctx: &Context, _: &Message, cmd_name: &str) -> bool { - let elements = { - let data_read = ctx.data.read().await; - data_read.get::().unwrap().clone() - }; - - let command_count_value = { - let mut count_write = elements.command_usage_values.lock().await; - let command_count_value = count_write.get_mut(cmd_name).unwrap(); - command_count_value.use_count += 1; - command_count_value.clone() - }; - - elements.command_usage_table.set_cell( - Row(command_count_value.index as u64), - Col(1), - command_count_value.use_count, - ); - - info!("Running command {}", cmd_name); - - true -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - env::set_var( - "RUST_LOG", - // TODO: If you are going to copy this to your crate, update the crate name in the string - // with the name of the crate you are using it with. - // This are the recommended log settings for rillrate, otherwise be prepared to be spammed - // with a ton of events. - "info,e15_simple_dashboard=trace,meio=warn,rate_core=warn,rill_engine=warn", - ); - - // Initialize the logger to use environment variables. - // - // In this case, a good default is setting the environment variable `RUST_LOG` to `debug`, but - // for production, use the variable defined above. - tracing_subscriber::fmt::init(); - - // Start a server on `http://0.0.0.0:6361/` - // Currently the port is not configurable, but it will be soon enough; thankfully it's not a - // common port, so it will be fine for most users. - rillrate::install("serenity")?; - - // Because you probably ran this without looking at the source :P - let _ = webbrowser::open("http://localhost:6361"); - - let framework = StandardFramework::new().before(before_hook).group(&GENERAL_GROUP); - framework.configure(Configuration::new().prefix("~")); - - let token = env::var("DISCORD_TOKEN")?; - - // These 3 Pulse are the graphs used to plot the latency overtime. - let ws_ping_tracer = Pulse::new( - [PACKAGE, DASHBOARD_STATS, GROUP_LATENCY, "Websocket Ping Time"], - Default::default(), - PulseOpts::default() - // The seconds of data to retain, this is 30 minutes. - .retain(1800_u32) - - // Column value range - .min(0) - .max(200) - - // Label used along the values on the column. - .suffix("ms".to_string()) - .divisor(1.0), - ); - - let get_ping_tracer = Pulse::new( - [PACKAGE, DASHBOARD_STATS, GROUP_LATENCY, "Rest GET Ping Time"], - Default::default(), - PulseOpts::default().retain(1800_u32).min(0).max(200).suffix("ms".to_string()).divisor(1.0), - ); - - #[cfg(feature = "post-ping")] - let post_ping_tracer = Pulse::new( - [PACKAGE, DASHBOARD_STATS, GROUP_LATENCY, "Rest POST Ping Time"], - Default::default(), - PulseOpts::default() - .retain(1800_u32) - .min(0) - // Post latency is on average higher, so we increase the max value on the graph. - .max(500) - .suffix("ms".to_string()) - .divisor(1.0), - ); - - let command_usage_table = Table::new( - [PACKAGE, DASHBOARD_STATS, GROUP_COMMAND_COUNT, "Command Usage"], - Default::default(), - TableOpts::default() - .columns(vec![(0, "Command Name".to_string()), (1, "Number of Uses".to_string())]), - ); - - let mut command_usage_values = HashMap::new(); - - // Iterate over the commands of the General group and add them to the table. - for (idx, i) in GENERAL_GROUP.options.commands.iter().enumerate() { - command_usage_table.add_row(Row(idx as u64)); - command_usage_table.set_cell(Row(idx as u64), Col(0), i.options.names[0]); - command_usage_table.set_cell(Row(idx as u64), Col(1), 0); - command_usage_values.insert(i.options.names[0], CommandUsageValue { - index: idx, - use_count: 0, - }); - } - - let components = Arc::new(Components { - ws_ping_history: ws_ping_tracer, - get_ping_history: get_ping_tracer, - #[cfg(feature = "post-ping")] - post_ping_history: post_ping_tracer, - data_switch: AtomicBool::new(false), - double_link_value: AtomicU8::new(0), - command_usage_table, - command_usage_values: Mutex::new(command_usage_values), - }); - - let intents = GatewayIntents::GUILD_MESSAGES - | GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(token, intents) - .event_handler(Handler) - .framework(framework) - .type_map_insert::(components) - .await?; - - { - let mut data = client.data.write().await; - - data.insert::(Arc::clone(&client.shard_manager)); - } - - client.start().await?; - - Ok(()) -} - -/// You can use this command to read the current value of the Switch, Slider and Selector. -#[command] -async fn switch(ctx: &Context, msg: &Message) -> CommandResult { - let elements = { - let data_read = ctx.data.read().await; - data_read.get::().unwrap().clone() - }; - - msg.reply( - ctx, - format!( - "The switch is {} and the current value is {}", - if elements.data_switch.load(Ordering::Relaxed) { "ON" } else { "OFF" }, - elements.double_link_value.load(Ordering::Relaxed), - ), - ) - .await?; - - Ok(()) -} - -#[command] -#[aliases("latency", "pong")] -async fn ping(ctx: &Context, msg: &Message) -> CommandResult { - let latency = { - let data_read = ctx.data.read().await; - let shard_manager = data_read.get::().unwrap(); - - let runners = shard_manager.runners.lock().await; - - let runner = runners.get(&ctx.shard_id).unwrap(); - - if let Some(duration) = runner.latency { - format!("{:.2}ms", duration.as_millis()) - } else { - "?ms".to_string() - } - }; - - msg.reply(ctx, format!("The shard latency is {latency}")).await?; - - Ok(()) -} diff --git a/examples/e18_webhook/Cargo.toml b/examples/e15_webhook/Cargo.toml similarity index 92% rename from examples/e18_webhook/Cargo.toml rename to examples/e15_webhook/Cargo.toml index ad995b8f4fe..8b47ff98c58 100644 --- a/examples/e18_webhook/Cargo.toml +++ b/examples/e15_webhook/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e18_webhook" +name = "e15_webhook" version = "0.1.0" authors = ["my name "] edition = "2018" diff --git a/examples/e15_simple_dashboard/Makefile.toml b/examples/e15_webhook/Makefile.toml similarity index 100% rename from examples/e15_simple_dashboard/Makefile.toml rename to examples/e15_webhook/Makefile.toml diff --git a/examples/e18_webhook/src/main.rs b/examples/e15_webhook/src/main.rs similarity index 100% rename from examples/e18_webhook/src/main.rs rename to examples/e15_webhook/src/main.rs diff --git a/examples/e19_interactions_endpoint/Cargo.toml b/examples/e16_interactions_endpoint/Cargo.toml similarity index 81% rename from examples/e19_interactions_endpoint/Cargo.toml rename to examples/e16_interactions_endpoint/Cargo.toml index 203ba8e157a..f1bb0a10781 100644 --- a/examples/e19_interactions_endpoint/Cargo.toml +++ b/examples/e16_interactions_endpoint/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e19_interactions_endpoint" +name = "e16_interactions_endpoint" version = "0.1.0" authors = ["my name "] edition = "2018" @@ -7,3 +7,4 @@ edition = "2018" [dependencies] serenity = { path = "../../", default-features = false, features = ["builder", "interactions_endpoint"] } tiny_http = "0.12.0" +serde_json = "1" diff --git a/examples/e16_sqlite_database/Makefile.toml b/examples/e16_interactions_endpoint/Makefile.toml similarity index 100% rename from examples/e16_sqlite_database/Makefile.toml rename to examples/e16_interactions_endpoint/Makefile.toml diff --git a/examples/e19_interactions_endpoint/src/main.rs b/examples/e16_interactions_endpoint/src/main.rs similarity index 93% rename from examples/e19_interactions_endpoint/src/main.rs rename to examples/e16_interactions_endpoint/src/main.rs index 3f4c784ea63..0f4258bb66d 100644 --- a/examples/e19_interactions_endpoint/src/main.rs +++ b/examples/e16_interactions_endpoint/src/main.rs @@ -1,11 +1,10 @@ use serenity::builder::*; use serenity::interactions_endpoint::Verifier; -use serenity::json; use serenity::model::application::*; type Error = Box; -fn handle_command(interaction: CommandInteraction) -> CreateInteractionResponse { +fn handle_command(interaction: CommandInteraction) -> CreateInteractionResponse<'static> { CreateInteractionResponse::Message(CreateInteractionResponseMessage::new().content(format!( "Hello from interactions webhook HTTP server! <@{}>", interaction.user.id @@ -37,7 +36,7 @@ fn handle_request( } // Build Discord response - let response = match json::from_slice::(body)? { + let response = match serde_json::from_slice::(body)? { // Discord rejects the interaction endpoints URL if pings are not acknowledged Interaction::Ping(_) => CreateInteractionResponse::Pong, Interaction::Command(interaction) => handle_command(interaction), @@ -46,7 +45,7 @@ fn handle_request( // Send the Discord response back via HTTP request.respond( - tiny_http::Response::from_data(json::to_vec(&response)?) + tiny_http::Response::from_data(serde_json::to_vec(&response)?) .with_header("Content-Type: application/json".parse::().unwrap()), )?; diff --git a/examples/e17_message_components/Makefile.toml b/examples/e17_message_components/Makefile.toml deleted file mode 100644 index 2e5db0b5e63..00000000000 --- a/examples/e17_message_components/Makefile.toml +++ /dev/null @@ -1,13 +0,0 @@ -extend = "../../Makefile.toml" - -[tasks.examples_build] -alias = "build" - -[tasks.examples_build_release] -alias = "build_release" - -[tasks.examples_run] -alias = "run" - -[tasks.examples_run_release] -alias = "run_release" diff --git a/examples/e18_webhook/Makefile.toml b/examples/e18_webhook/Makefile.toml deleted file mode 100644 index 2e5db0b5e63..00000000000 --- a/examples/e18_webhook/Makefile.toml +++ /dev/null @@ -1,13 +0,0 @@ -extend = "../../Makefile.toml" - -[tasks.examples_build] -alias = "build" - -[tasks.examples_build_release] -alias = "build_release" - -[tasks.examples_run] -alias = "run" - -[tasks.examples_run_release] -alias = "run_release" diff --git a/examples/e19_interactions_endpoint/Makefile.toml b/examples/e19_interactions_endpoint/Makefile.toml deleted file mode 100644 index 2e5db0b5e63..00000000000 --- a/examples/e19_interactions_endpoint/Makefile.toml +++ /dev/null @@ -1,13 +0,0 @@ -extend = "../../Makefile.toml" - -[tasks.examples_build] -alias = "build" - -[tasks.examples_build_release] -alias = "build_release" - -[tasks.examples_run] -alias = "run" - -[tasks.examples_run_release] -alias = "run_release" diff --git a/examples/testing/src/main.rs b/examples/testing/src/main.rs index 46032b7a93c..4c97da7c101 100644 --- a/examples/testing/src/main.rs +++ b/examples/testing/src/main.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use serenity::builder::*; use serenity::model::prelude::*; use serenity::prelude::*; @@ -12,39 +14,37 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { let guild_id = msg.guild_id.unwrap(); if let Some(_args) = msg.content.strip_prefix("testmessage ") { println!("command message: {msg:#?}"); - } else if msg.content == "globalcommand" { - // Tests https://github.com/serenity-rs/serenity/issues/2259 - // Activate simd_json feature for this - Command::create_global_command( - &ctx, - CreateCommand::new("ping").description("test command"), - ) - .await?; } else if msg.content == "register" { guild_id - .create_command(&ctx, CreateCommand::new("editattachments").description("test command")) + .create_command( + &ctx.http, + CreateCommand::new("editattachments").description("test command"), + ) .await?; guild_id .create_command( - &ctx, + &ctx.http, CreateCommand::new("unifiedattachments1").description("test command"), ) .await?; guild_id .create_command( - &ctx, + &ctx.http, CreateCommand::new("unifiedattachments2").description("test command"), ) .await?; guild_id - .create_command(&ctx, CreateCommand::new("editembeds").description("test command")) + .create_command(&ctx.http, CreateCommand::new("editembeds").description("test command")) .await?; guild_id - .create_command(&ctx, CreateCommand::new("newselectmenu").description("test command")) + .create_command( + &ctx.http, + CreateCommand::new("newselectmenu").description("test command"), + ) .await?; guild_id .create_command( - &ctx, + &ctx.http, CreateCommand::new("autocomplete").description("test command").add_option( CreateCommandOption::new(CommandOptionType::String, "foo", "foo") .set_autocomplete(true), @@ -54,26 +54,30 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { } else if msg.content == "edit" { let mut msg = channel_id .send_message( - &ctx, - CreateMessage::new().add_file(CreateAttachment::url(ctx, IMAGE_URL).await?), + &ctx.http, + CreateMessage::new() + .add_file(CreateAttachment::url(&ctx.http, IMAGE_URL, "testing.png").await?), ) .await?; // Pre-PR, this falsely triggered a MODEL_TYPE_CONVERT Discord error msg.edit(&ctx, EditMessage::new().attachments(EditAttachments::keep_all(&msg))).await?; } else if msg.content == "unifiedattachments" { - let mut msg = channel_id.send_message(ctx, CreateMessage::new().content("works")).await?; + let mut msg = + channel_id.send_message(&ctx.http, CreateMessage::new().content("works")).await?; msg.edit(ctx, EditMessage::new().content("works still")).await?; let mut msg = channel_id .send_message( - ctx, - CreateMessage::new().add_file(CreateAttachment::url(ctx, IMAGE_URL).await?), + &ctx.http, + CreateMessage::new() + .add_file(CreateAttachment::url(&ctx.http, IMAGE_URL, "testing.png").await?), ) .await?; msg.edit( ctx, EditMessage::new().attachments( - EditAttachments::keep_all(&msg).add(CreateAttachment::url(ctx, IMAGE_URL_2).await?), + EditAttachments::keep_all(&msg) + .add(CreateAttachment::url(&ctx.http, IMAGE_URL_2, "testing1.png").await?), ), ) .await?; @@ -83,14 +87,14 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { // Test special characters in audit log reason msg.channel_id .edit( - ctx, + &ctx.http, EditChannel::new().name("new-channel-name").audit_log_reason("hello\nworld\n🙂"), ) .await?; } else if msg.content == "actionrow" { channel_id .send_message( - ctx, + &ctx.http, CreateMessage::new() .button(CreateButton::new("0").label("Foo")) .button(CreateButton::new("1").emoji('🤗').style(ButtonStyle::Secondary)) @@ -98,10 +102,10 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { CreateButton::new_link("https://google.com").emoji('🔍').label("Search"), ) .select_menu(CreateSelectMenu::new("3", CreateSelectMenuKind::String { - options: vec![ + options: Cow::Borrowed(&[ CreateSelectMenuOption::new("foo", "foo"), CreateSelectMenuOption::new("bar", "bar"), - ], + ]), })), ) .await?; @@ -110,17 +114,17 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { loop { let msg = channel_id .send_message( - ctx, + &ctx.http, CreateMessage::new() .button(CreateButton::new(custom_id.clone()).label(custom_id)), ) .await?; let button_press = msg - .await_component_interaction(&ctx.shard) + .await_component_interaction(ctx.shard.clone()) .timeout(std::time::Duration::from_secs(10)) .await; match button_press { - Some(x) => x.defer(ctx).await?, + Some(x) => x.defer(&ctx.http).await?, None => break, } @@ -128,12 +132,12 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { } } else if msg.content == "reactionremoveemoji" { // Test new ReactionRemoveEmoji gateway event: https://github.com/serenity-rs/serenity/issues/2248 - msg.react(ctx, '👍').await?; - msg.delete_reaction_emoji(ctx, '👍').await?; + msg.react(&ctx.http, '👍').await?; + msg.delete_reaction_emoji(&ctx.http, '👍').await?; } else if msg.content == "testautomodregex" { guild_id .create_automod_rule( - ctx, + &ctx.http, EditAutoModRule::new().trigger(Trigger::Keyword { strings: vec!["badword".into()], regex_patterns: vec!["b[o0]{2,}b(ie)?s?".into()], @@ -141,15 +145,15 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { }), ) .await?; - println!("new automod rules: {:?}", guild_id.automod_rules(ctx).await?); + println!("new automod rules: {:?}", guild_id.automod_rules(&ctx.http).await?); } else if let Some(user_id) = msg.content.strip_prefix("ban ") { // Test if banning without a reason actually works let user_id: UserId = user_id.trim().parse().unwrap(); - guild_id.ban(ctx, user_id, 0).await?; + guild_id.ban(&ctx.http, user_id, 0, None).await?; } else if msg.content == "createtags" { channel_id .edit( - &ctx, + &ctx.http, EditChannel::new().available_tags(vec![ CreateForumTag::new("tag1 :)").emoji('👍'), CreateForumTag::new("tag2 (:").moderated(true), @@ -161,8 +165,9 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { let forum = forum_id.to_channel(ctx).await?.guild().unwrap(); channel_id .edit_thread( - &ctx, - EditThread::new().applied_tags(forum.available_tags.iter().map(|t| t.id)), + &ctx.http, + EditThread::new() + .applied_tags(forum.available_tags.iter().map(|t| t.id).collect::>()), ) .await?; } else if msg.content == "embedrace" { @@ -170,7 +175,7 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { use tokio::time::Duration; let mut msg = channel_id - .say(ctx, format!("https://codereview.stackexchange.com/questions/260653/very-slow-discord-bot-to-play-music{}", msg.id)) + .say(&ctx.http, format!("https://codereview.stackexchange.com/questions/260653/very-slow-discord-bot-to-play-music{}", msg.id)) .await?; let msg_id = msg.id; @@ -186,52 +191,48 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { // As of 2023-04-20, bots are still not allowed to sending voice messages let builder = CreateMessage::new() .flags(MessageFlags::IS_VOICE_MESSAGE) - .add_file(CreateAttachment::url(ctx, audio_url).await?); + .add_file(CreateAttachment::url(&ctx.http, audio_url, "testing.ogg").await?); - msg.author.dm(ctx, builder).await?; + msg.author.dm(&ctx.http, builder).await?; } else if let Some(channel) = msg.content.strip_prefix("movetorootandback") { let mut channel = channel.trim().parse::().unwrap().to_channel(ctx).await?.guild().unwrap(); let parent_id = channel.parent_id.unwrap(); - channel.edit(ctx, EditChannel::new().category(None)).await?; - channel.edit(ctx, EditChannel::new().category(Some(parent_id))).await?; + channel.edit(&ctx.http, EditChannel::new().category(None)).await?; + channel.edit(&ctx.http, EditChannel::new().category(Some(parent_id))).await?; } else if msg.content == "channelperms" { - let guild = guild_id.to_guild_cached(ctx).unwrap().clone(); + let guild = guild_id.to_guild_cached(&ctx.cache).unwrap().clone(); let perms = guild.user_permissions_in( &channel_id.to_channel(ctx).await?.guild().unwrap(), - &*guild.member(ctx, msg.author.id).await?, + &*guild.member(&ctx.http, msg.author.id).await?, ); - channel_id.say(ctx, format!("{:?}", perms)).await?; + channel_id.say(&ctx.http, format!("{:?}", perms)).await?; } else if let Some(forum_channel_id) = msg.content.strip_prefix("createforumpostin ") { forum_channel_id .parse::() .unwrap() .create_forum_post( - ctx, + &ctx.http, CreateForumPost::new( "a", CreateMessage::new() - .add_file(CreateAttachment::bytes(b"Hallo welt!", "lul.txt")), + .add_file(CreateAttachment::bytes(b"Hallo welt!".as_slice(), "lul.txt")), ), - // CreateForumPost::new( - // "a", - // CreateMessage::new() - // .content("test, i hope that forum posts without attachments still - // work?") .embed(CreateEmbed::new().title("hmmm"). - // description("do they?")), ), ) .await?; } else if let Some(forum_post_url) = msg.content.strip_prefix("deleteforumpost ") { let (_guild_id, channel_id, _message_id) = serenity::utils::parse_message_url(forum_post_url).unwrap(); - msg.channel_id.say(ctx, format!("Deleting <#{}> in 10 seconds...", channel_id)).await?; + msg.channel_id + .say(&ctx.http, format!("Deleting <#{}> in 10 seconds...", channel_id)) + .await?; tokio::time::sleep(std::time::Duration::from_secs(10)).await; - channel_id.delete(ctx).await?; + channel_id.delete(&ctx.http, None).await?; } else { return Ok(()); } - msg.react(&ctx, '✅').await?; + msg.react(&ctx.http, '✅').await?; Ok(()) } @@ -243,24 +244,25 @@ async fn interaction( // Respond with an image interaction .create_response( - &ctx, + &ctx.http, CreateInteractionResponse::Message( - CreateInteractionResponseMessage::new() - .add_file(CreateAttachment::url(ctx, IMAGE_URL).await?), + CreateInteractionResponseMessage::new().add_file( + CreateAttachment::url(&ctx.http, IMAGE_URL, "testing.png").await?, + ), ), ) .await?; // We need to know the attachments' IDs in order to not lose them in the subsequent edit - let msg = interaction.get_response(ctx).await?; + let msg = interaction.get_response(&ctx.http).await?; // Add another image let msg = interaction .edit_response( - &ctx, + &ctx.http, EditInteractionResponse::new().attachments( EditAttachments::keep_all(&msg) - .add(CreateAttachment::url(ctx, IMAGE_URL_2).await?), + .add(CreateAttachment::url(&ctx.http, IMAGE_URL_2, "testing1.png").await?), ), ) .await?; @@ -270,7 +272,7 @@ async fn interaction( // Only keep the new image, removing the first image let _msg = interaction .edit_response( - &ctx, + &ctx.http, EditInteractionResponse::new() .attachments(EditAttachments::new().keep(msg.attachments[1].id)), ) @@ -278,7 +280,7 @@ async fn interaction( } else if interaction.data.name == "unifiedattachments1" { interaction .create_response( - ctx, + &ctx.http, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new().content("works"), ), @@ -286,45 +288,47 @@ async fn interaction( .await?; interaction - .edit_response(ctx, EditInteractionResponse::new().content("works still")) + .edit_response(&ctx.http, EditInteractionResponse::new().content("works still")) .await?; interaction .create_followup( - ctx, + &ctx.http, CreateInteractionResponseFollowup::new().content("still works still"), ) .await?; } else if interaction.data.name == "unifiedattachments2" { interaction .create_response( - ctx, + &ctx.http, CreateInteractionResponse::Message( - CreateInteractionResponseMessage::new() - .add_file(CreateAttachment::url(ctx, IMAGE_URL).await?), + CreateInteractionResponseMessage::new().add_file( + CreateAttachment::url(&ctx.http, IMAGE_URL, "testing.png").await?, + ), ), ) .await?; interaction .edit_response( - ctx, - EditInteractionResponse::new() - .new_attachment(CreateAttachment::url(ctx, IMAGE_URL_2).await?), + &ctx.http, + EditInteractionResponse::new().new_attachment( + CreateAttachment::url(&ctx.http, IMAGE_URL_2, "testing1.png").await?, + ), ) .await?; interaction .create_followup( - ctx, + &ctx.http, CreateInteractionResponseFollowup::new() - .add_file(CreateAttachment::url(ctx, IMAGE_URL).await?), + .add_file(CreateAttachment::url(&ctx.http, IMAGE_URL, "testing.png").await?), ) .await?; } else if interaction.data.name == "editembeds" { interaction .create_response( - &ctx, + &ctx.http, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new() .content("hi") @@ -334,18 +338,18 @@ async fn interaction( .await?; // Pre-PR, this falsely deleted the embed - interaction.edit_response(&ctx, EditInteractionResponse::new()).await?; + interaction.edit_response(&ctx.http, EditInteractionResponse::new()).await?; } else if interaction.data.name == "newselectmenu" { interaction .create_response( - &ctx, + &ctx.http, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new() .select_menu(CreateSelectMenu::new("0", CreateSelectMenuKind::String { - options: vec![ + options: Cow::Borrowed(&[ CreateSelectMenuOption::new("foo", "foo"), CreateSelectMenuOption::new("bar", "bar"), - ], + ]), })) .select_menu(CreateSelectMenu::new( "1", @@ -385,7 +389,7 @@ impl EventHandler for Handler { Interaction::Component(i) => println!("{:#?}", i.data), Interaction::Autocomplete(i) => { i.create_response( - &ctx, + &ctx.http, CreateInteractionResponse::Autocomplete( CreateAutocompleteResponse::new() .add_string_choice("suggestion", "suggestion"), @@ -415,5 +419,5 @@ async fn main() -> Result<(), serenity::Error> { env_logger::init(); let token = std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT; - Client::builder(token, intents).event_handler(Handler).await?.start().await + Client::builder(&token, intents).event_handler(Handler).await?.start().await } diff --git a/examples/testing/src/model_type_sizes.rs b/examples/testing/src/model_type_sizes.rs index 7160a2ece58..9f0f076a88e 100644 --- a/examples/testing/src/model_type_sizes.rs +++ b/examples/testing/src/model_type_sizes.rs @@ -125,7 +125,6 @@ pub fn print_ranking() { ("MessageFlags", std::mem::size_of::()), ("MessageFlags", std::mem::size_of::()), ("MessageId", std::mem::size_of::()), - ("MessageInteraction", std::mem::size_of::()), ("MessageReaction", std::mem::size_of::()), ("MessageReference", std::mem::size_of::()), ("MessageUpdateEvent", std::mem::size_of::()), @@ -194,7 +193,6 @@ pub fn print_ranking() { ("TriggerMetadata", std::mem::size_of::()), ("TypingStartEvent", std::mem::size_of::()), ("UnavailableGuild", std::mem::size_of::()), - ("UnknownEvent", std::mem::size_of::()), ("User", std::mem::size_of::()), ("UserId", std::mem::size_of::()), ("UserPublicFlags", std::mem::size_of::()), diff --git a/src/builder/add_member.rs b/src/builder/add_member.rs index 2e1dd13d768..72b111ebb6c 100644 --- a/src/builder/add_member.rs +++ b/src/builder/add_member.rs @@ -1,7 +1,7 @@ +use std::borrow::Cow; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -11,25 +11,25 @@ use crate::model::prelude::*; /// [Discord docs](https://discord.com/developers/docs/resources/guild#add-guild-member). #[derive(Clone, Debug, Serialize)] #[must_use] -pub struct AddMember { - access_token: String, +pub struct AddMember<'a> { + access_token: Cow<'a, str>, #[serde(skip_serializing_if = "Option::is_none")] - nick: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - roles: Vec, + nick: Option>, + #[serde(skip_serializing_if = "<[RoleId]>::is_empty")] + roles: Cow<'a, [RoleId]>, #[serde(skip_serializing_if = "Option::is_none")] mute: Option, #[serde(skip_serializing_if = "Option::is_none")] deaf: Option, } -impl AddMember { +impl<'a> AddMember<'a> { /// Constructs a new builder with the given access token, leaving all other fields empty. - pub fn new(access_token: String) -> Self { + pub fn new(access_token: impl Into>) -> Self { Self { - access_token, + access_token: access_token.into(), + roles: Cow::default(), nick: None, - roles: Vec::new(), mute: None, deaf: None, } @@ -38,7 +38,7 @@ impl AddMember { /// Sets the OAuth2 access token for this request, replacing the current one. /// /// Requires the access token to have the `guilds.join` scope granted. - pub fn access_token(mut self, access_token: impl Into) -> Self { + pub fn access_token(mut self, access_token: impl Into>) -> Self { self.access_token = access_token.into(); self } @@ -48,7 +48,7 @@ impl AddMember { /// Requires the [Manage Nicknames] permission. /// /// [Manage Nicknames]: crate::model::permissions::Permissions::MANAGE_NICKNAMES - pub fn nickname(mut self, nickname: impl Into) -> Self { + pub fn nickname(mut self, nickname: impl Into>) -> Self { self.nick = Some(nickname.into()); self } @@ -58,8 +58,8 @@ impl AddMember { /// Requires the [Manage Roles] permission. /// /// [Manage Roles]: crate::model::permissions::Permissions::MANAGE_ROLES - pub fn roles(mut self, roles: impl IntoIterator>) -> Self { - self.roles = roles.into_iter().map(Into::into).collect(); + pub fn roles(mut self, roles: impl Into>) -> Self { + self.roles = roles.into(); self } @@ -82,13 +82,6 @@ impl AddMember { self.deaf = Some(deafen); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for AddMember { - type Context<'ctx> = (GuildId, UserId); - type Built = Option; /// Adds a [`User`] to this guild with a valid OAuth2 access token. /// @@ -98,11 +91,13 @@ impl Builder for AddMember { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().add_guild_member(ctx.0, ctx.1, &self).await + http: &Http, + guild_id: GuildId, + user_id: UserId, + ) -> Result> { + http.add_guild_member(guild_id, user_id, &self).await } } diff --git a/src/builder/bot_auth_parameters.rs b/src/builder/bot_auth_parameters.rs index 8fa4c5f4321..961a6aa7ea8 100644 --- a/src/builder/bot_auth_parameters.rs +++ b/src/builder/bot_auth_parameters.rs @@ -1,4 +1,7 @@ +use std::borrow::Cow; + use arrayvec::ArrayVec; +use to_arraystring::ToArrayString; use url::Url; #[cfg(feature = "http")] @@ -10,15 +13,15 @@ use crate::model::prelude::*; /// A builder for constructing an invite link with custom OAuth2 scopes. #[derive(Debug, Clone, Default)] #[must_use] -pub struct CreateBotAuthParameters { +pub struct CreateBotAuthParameters<'a> { client_id: Option, - scopes: Vec, + scopes: Cow<'a, [Scope]>, permissions: Permissions, guild_id: Option, disable_guild_select: bool, } -impl CreateBotAuthParameters { +impl<'a> CreateBotAuthParameters<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() @@ -27,41 +30,45 @@ impl CreateBotAuthParameters { /// Builds the url with the provided data. #[must_use] pub fn build(self) -> String { + // These bindings have to be defined before `valid_data`, due to Drop order. + let (client_id_str, guild_id_str, scope_str, bits_str); + let mut valid_data = ArrayVec::<_, 5>::new(); let bits = self.permissions.bits(); if let Some(client_id) = self.client_id { - valid_data.push(("client_id", client_id.to_string())); + client_id_str = client_id.to_arraystring(); + valid_data.push(("client_id", client_id_str.as_str())); } if !self.scopes.is_empty() { - valid_data.push(( - "scope", - self.scopes.iter().map(ToString::to_string).collect::>().join(" "), - )); + scope_str = join_to_string(',', self.scopes.iter()); + valid_data.push(("scope", &scope_str)); } if bits != 0 { - valid_data.push(("permissions", bits.to_string())); + bits_str = bits.to_arraystring(); + valid_data.push(("permissions", &bits_str)); } if let Some(guild_id) = self.guild_id { - valid_data.push(("guild", guild_id.to_string())); + guild_id_str = guild_id.to_arraystring(); + valid_data.push(("guild", &guild_id_str)); } if self.disable_guild_select { - valid_data.push(("disable_guild_select", self.disable_guild_select.to_string())); + valid_data.push(("disable_guild_select", "true")); } let url = Url::parse_with_params("https://discord.com/api/oauth2/authorize", &valid_data) .expect("failed to construct URL"); - url.to_string() + url.into() } /// Specify the client Id of your application. - pub fn client_id(mut self, client_id: impl Into) -> Self { - self.client_id = Some(client_id.into()); + pub fn client_id(mut self, client_id: ApplicationId) -> Self { + self.client_id = Some(client_id); self } @@ -74,8 +81,8 @@ impl CreateBotAuthParameters { /// /// [`HttpError::UnsuccessfulRequest`]: crate::http::HttpError::UnsuccessfulRequest #[cfg(feature = "http")] - pub async fn auto_client_id(mut self, http: impl AsRef) -> Result { - self.client_id = http.as_ref().get_current_application_info().await.map(|v| Some(v.id))?; + pub async fn auto_client_id(mut self, http: &Http) -> Result { + self.client_id = http.get_current_application_info().await.map(|v| Some(v.id))?; Ok(self) } @@ -84,8 +91,8 @@ impl CreateBotAuthParameters { /// **Note**: This needs to include the [`Bot`] scope. /// /// [`Bot`]: Scope::Bot - pub fn scopes(mut self, scopes: &[Scope]) -> Self { - self.scopes = scopes.to_vec(); + pub fn scopes(mut self, scopes: impl Into>) -> Self { + self.scopes = scopes.into(); self } @@ -96,8 +103,8 @@ impl CreateBotAuthParameters { } /// Specify the Id of the guild to prefill the dropdown picker for the user. - pub fn guild_id(mut self, guild_id: impl Into) -> Self { - self.guild_id = Some(guild_id.into()); + pub fn guild_id(mut self, guild_id: GuildId) -> Self { + self.guild_id = Some(guild_id); self } diff --git a/src/builder/create_allowed_mentions.rs b/src/builder/create_allowed_mentions.rs index 6655c967405..d74be3bc6b9 100644 --- a/src/builder/create_allowed_mentions.rs +++ b/src/builder/create_allowed_mentions.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use arrayvec::ArrayVec; use serde::{Deserialize, Serialize}; @@ -34,17 +36,20 @@ impl ParseAction { /// ```rust,no_run /// # use serenity::builder::CreateMessage; /// # use serenity::model::channel::Message; +/// # use serenity::model::id::*; /// # /// # async fn run() -> Result<(), Box> { /// use serenity::builder::CreateAllowedMentions as Am; /// /// // Mention only the user 110372470472613888 /// # let m = CreateMessage::new(); -/// m.allowed_mentions(Am::new().users(vec![110372470472613888])); +/// m.allowed_mentions(Am::new().users([UserId::new(110372470472613888)].as_slice())); /// /// // Mention all users and the role 182894738100322304 /// # let m = CreateMessage::new(); -/// m.allowed_mentions(Am::new().all_users(true).roles(vec![182894738100322304])); +/// m.allowed_mentions( +/// Am::new().all_users(true).roles([RoleId::new(182894738100322304)].as_slice()), +/// ); /// /// // Mention all roles and nothing else /// # let m = CreateMessage::new(); @@ -57,29 +62,31 @@ impl ParseAction { /// // Mention everyone and the users 182891574139682816, 110372470472613888 /// # let m = CreateMessage::new(); /// m.allowed_mentions( -/// Am::new().everyone(true).users(vec![182891574139682816, 110372470472613888]), +/// Am::new() +/// .everyone(true) +/// .users([UserId::new(182891574139682816), UserId::new(110372470472613888)].as_slice()), /// ); /// /// // Mention everyone and the message author. /// # let m = CreateMessage::new(); /// # let msg: Message = unimplemented!(); -/// m.allowed_mentions(Am::new().everyone(true).users(vec![msg.author.id])); +/// m.allowed_mentions(Am::new().everyone(true).users([msg.author.id].as_slice())); /// # Ok(()) /// # } /// ``` /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#allowed-mentions-object). -#[derive(Clone, Debug, Default, Serialize, PartialEq)] +#[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct CreateAllowedMentions { +pub struct CreateAllowedMentions<'a> { parse: ArrayVec, - users: Vec, - roles: Vec, + users: Cow<'a, [UserId]>, + roles: Cow<'a, [RoleId]>, #[serde(skip_serializing_if = "Option::is_none")] replied_user: Option, } -impl CreateAllowedMentions { +impl<'a> CreateAllowedMentions<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() @@ -112,35 +119,30 @@ impl CreateAllowedMentions { } /// Sets the *specific* users that will be allowed mentionable. - #[inline] - pub fn users(mut self, users: impl IntoIterator>) -> Self { - self.users = users.into_iter().map(Into::into).collect(); + pub fn users(mut self, users: impl Into>) -> Self { + self.users = users.into(); self } /// Clear the list of mentionable users. - #[inline] pub fn empty_users(mut self) -> Self { - self.users.clear(); + self.users = Cow::default(); self } /// Sets the *specific* roles that will be allowed mentionable. - #[inline] - pub fn roles(mut self, roles: impl IntoIterator>) -> Self { - self.roles = roles.into_iter().map(Into::into).collect(); + pub fn roles(mut self, roles: impl Into>) -> Self { + self.roles = roles.into(); self } /// Clear the list of mentionable roles. - #[inline] pub fn empty_roles(mut self) -> Self { - self.roles.clear(); + self.roles = Cow::default(); self } /// Makes the reply mention/ping the user. - #[inline] pub fn replied_user(mut self, mention_user: bool) -> Self { self.replied_user = Some(mention_user); self diff --git a/src/builder/create_attachment.rs b/src/builder/create_attachment.rs index ddbca56bc1f..021acd40963 100644 --- a/src/builder/create_attachment.rs +++ b/src/builder/create_attachment.rs @@ -1,43 +1,41 @@ +use std::borrow::Cow; use std::path::Path; +use serde::ser::{Serialize, SerializeSeq, Serializer}; use tokio::fs::File; use tokio::io::AsyncReadExt; -#[cfg(feature = "http")] -use url::Url; -use crate::all::Message; -#[cfg(feature = "http")] -use crate::error::Error; -use crate::error::Result; +#[allow(unused)] // Error is used in docs +use crate::error::{Error, Result}; #[cfg(feature = "http")] use crate::http::Http; +use crate::model::channel::Message; use crate::model::id::AttachmentId; -/// Enum that allows a user to pass a [`Path`] or a [`File`] type to [`send_files`] +/// Struct that allows a user to pass a [`Path`] or a [`File`] type to [`send_files`] /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#attachment-object-attachment-structure). /// /// [`send_files`]: crate::model::id::ChannelId::send_files -#[derive(Clone, Debug, Serialize, PartialEq)] +#[derive(Clone, Debug)] #[non_exhaustive] #[must_use] -pub struct CreateAttachment { - pub(crate) id: u64, // Placeholder ID will be filled in when sending the request - pub filename: String, - pub description: Option, - - #[serde(skip)] - pub data: Vec, +pub struct CreateAttachment<'a> { + pub filename: Cow<'static, str>, + pub description: Option>, + pub data: Cow<'static, [u8]>, } -impl CreateAttachment { +impl<'a> CreateAttachment<'a> { /// Builds an [`CreateAttachment`] from the raw attachment data. - pub fn bytes(data: impl Into>, filename: impl Into) -> CreateAttachment { + pub fn bytes( + data: impl Into>, + filename: impl Into>, + ) -> Self { CreateAttachment { data: data.into(), filename: filename.into(), description: None, - id: 0, } } @@ -46,19 +44,23 @@ impl CreateAttachment { /// # Errors /// /// [`Error::Io`] if reading the file fails. - pub async fn path(path: impl AsRef) -> Result { - let mut file = File::open(path.as_ref()).await?; - let mut data = Vec::new(); - file.read_to_end(&mut data).await?; + pub async fn path(path: impl AsRef) -> Result { + async fn inner(path: &Path) -> Result> { + let mut file = File::open(path).await?; + let mut data = Vec::new(); + file.read_to_end(&mut data).await?; - let filename = path.as_ref().file_name().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::Other, - "attachment path must not be a directory", - ) - })?; + let filename = path.file_name().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::Other, + "attachment path must not be a directory", + ) + })?; - Ok(CreateAttachment::bytes(data, filename.to_string_lossy().to_string())) + Ok(CreateAttachment::bytes(data, filename.to_string_lossy().into_owned())) + } + + inner(path.as_ref()).await } /// Builds an [`CreateAttachment`] by reading from a file handler. @@ -66,7 +68,7 @@ impl CreateAttachment { /// # Errors /// /// [`Error::Io`] error if reading the file fails. - pub async fn file(file: &File, filename: impl Into) -> Result { + pub async fn file(file: &File, filename: impl Into>) -> Result { let mut data = Vec::new(); file.try_clone().await?.read_to_end(&mut data).await?; @@ -77,19 +79,16 @@ impl CreateAttachment { /// /// # Errors /// - /// [`Error::Url`] if the URL is invalid, [`Error::Http`] if downloading the data fails. + /// Returns [`Error::Http`] if downloading the data fails. #[cfg(feature = "http")] - pub async fn url(http: impl AsRef, url: &str) -> Result { - let url = Url::parse(url).map_err(|_| Error::Url(url.to_string()))?; - - let response = http.as_ref().client.get(url.clone()).send().await?; + pub async fn url( + http: &Http, + url: impl reqwest::IntoUrl, + filename: impl Into>, + ) -> Result { + let response = http.client.get(url).send().await?; let data = response.bytes().await?.to_vec(); - let filename = url - .path_segments() - .and_then(Iterator::last) - .ok_or_else(|| Error::Url(url.to_string()))?; - Ok(CreateAttachment::bytes(data, filename)) } @@ -108,21 +107,20 @@ impl CreateAttachment { } /// Sets a description for the file (max 1024 characters). - pub fn description(mut self, description: impl Into) -> Self { + pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } } -#[derive(Debug, Clone, serde::Serialize, PartialEq)] +#[derive(Clone, Debug, Serialize)] struct ExistingAttachment { id: AttachmentId, } -#[derive(Debug, Clone, serde::Serialize, PartialEq)] -#[serde(untagged)] -enum NewOrExisting { - New(CreateAttachment), +#[derive(Clone, Debug)] +enum NewOrExisting<'a> { + New(CreateAttachment<'a>), Existing(ExistingAttachment), } @@ -148,7 +146,7 @@ enum NewOrExisting { /// /// ```rust,no_run /// # use serenity::all::*; -/// # async fn _foo(ctx: Http, mut msg: Message, my_attachment: CreateAttachment) -> Result<(), Error> { +/// # async fn _foo(ctx: Http, mut msg: Message, my_attachment: CreateAttachment<'_>) -> Result<(), Error> { /// msg.edit(ctx, EditMessage::new().attachments( /// EditAttachments::keep_all(&msg).add(my_attachment) /// )).await?; @@ -159,7 +157,7 @@ enum NewOrExisting { /// /// ```rust,no_run /// # use serenity::all::*; -/// # async fn _foo(ctx: Http, mut msg: Message, my_attachment: CreateAttachment) -> Result<(), Error> { +/// # async fn _foo(ctx: Http, mut msg: Message, my_attachment: CreateAttachment<'_>) -> Result<(), Error> { /// msg.edit(ctx, EditMessage::new().attachments( /// EditAttachments::new().keep(msg.attachments[0].id) /// )).await?; @@ -170,7 +168,7 @@ enum NewOrExisting { /// /// ```rust,no_run /// # use serenity::all::*; -/// # async fn _foo(ctx: Http, mut msg: Message, my_attachment: CreateAttachment) -> Result<(), Error> { +/// # async fn _foo(ctx: Http, mut msg: Message, my_attachment: CreateAttachment<'_>) -> Result<(), Error> { /// msg.edit(ctx, EditMessage::new().attachments( /// EditAttachments::keep_all(&msg).remove(msg.attachments[0].id) /// )).await?; @@ -181,14 +179,13 @@ enum NewOrExisting { /// /// Internally, this type is used not just for message editing endpoints, but also for message /// creation endpoints. -#[derive(Default, Debug, Clone, serde::Serialize, PartialEq)] -#[serde(transparent)] +#[derive(Default, Debug, Clone)] #[must_use] -pub struct EditAttachments { - new_and_existing_attachments: Vec, +pub struct EditAttachments<'a> { + new_and_existing_attachments: Vec>, } -impl EditAttachments { +impl<'a> EditAttachments<'a> { /// An empty attachments builder. /// /// Existing attachments are not kept by default, either. See [`Self::keep_all()`] or @@ -247,7 +244,7 @@ impl EditAttachments { /// Adds a new attachment to the attachment list. #[allow(clippy::should_implement_trait)] // Clippy thinks add == std::ops::Add::add - pub fn add(mut self, attachment: CreateAttachment) -> Self { + pub fn add(mut self, attachment: CreateAttachment<'a>) -> Self { self.new_and_existing_attachments.push(NewOrExisting::New(attachment)); self } @@ -255,30 +252,52 @@ impl EditAttachments { /// Clones all new attachments into a new Vec, keeping only data and filename, because those /// are needed for the multipart form data. The data is taken out of `self` in the process, so /// this method can only be called once. - pub(crate) fn take_files(&mut self) -> Vec { - let mut id_placeholder = 0; - + pub(crate) fn take_files(&mut self) -> Vec> { let mut files = Vec::new(); for attachment in &mut self.new_and_existing_attachments { if let NewOrExisting::New(attachment) = attachment { - let mut cloned_attachment = CreateAttachment::bytes( + let cloned_attachment = CreateAttachment::bytes( std::mem::take(&mut attachment.data), attachment.filename.clone(), ); - // Assign placeholder IDs so Discord can match metadata to file contents - attachment.id = id_placeholder; - cloned_attachment.id = id_placeholder; files.push(cloned_attachment); - - id_placeholder += 1; } } files } +} - #[cfg(feature = "cache")] - pub(crate) fn is_empty(&self) -> bool { - self.new_and_existing_attachments.is_empty() +impl<'a> Serialize for EditAttachments<'a> { + fn serialize(&self, serializer: S) -> Result { + #[derive(Serialize)] + struct NewAttachment<'a> { + id: u64, + filename: &'a Cow<'static, str>, + description: &'a Option>, + } + + // Instead of an `AttachmentId`, the `id` field for new attachments corresponds to the + // index of the new attachment in the multipart payload. The attachment data will be + // labeled with `files[{id}]` in the multipart body. See `Multipart::build_form`. + let mut id = 0; + let mut seq = serializer.serialize_seq(Some(self.new_and_existing_attachments.len()))?; + for attachment in &self.new_and_existing_attachments { + match attachment { + NewOrExisting::New(new_attachment) => { + let attachment = NewAttachment { + id, + filename: &new_attachment.filename, + description: &new_attachment.description, + }; + id += 1; + seq.serialize_element(&attachment)?; + }, + NewOrExisting::Existing(existing_attachment) => { + seq.serialize_element(existing_attachment)?; + }, + } + } + seq.end() } } diff --git a/src/builder/create_channel.rs b/src/builder/create_channel.rs index 981b5de1f07..a85ecd1c3c2 100644 --- a/src/builder/create_channel.rs +++ b/src/builder/create_channel.rs @@ -1,7 +1,9 @@ +use std::borrow::Cow; + +use nonmax::NonMaxU16; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -14,36 +16,36 @@ use crate::model::prelude::*; #[derive(Clone, Debug, Serialize)] #[must_use] pub struct CreateChannel<'a> { - name: String, + name: Cow<'a, str>, #[serde(rename = "type")] kind: ChannelType, #[serde(skip_serializing_if = "Option::is_none")] - topic: Option, + topic: Option>, #[serde(skip_serializing_if = "Option::is_none")] bitrate: Option, #[serde(skip_serializing_if = "Option::is_none")] - user_limit: Option, + user_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] - rate_limit_per_user: Option, + rate_limit_per_user: Option, #[serde(skip_serializing_if = "Option::is_none")] position: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - permission_overwrites: Vec, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + permission_overwrites: Cow<'a, [PermissionOverwrite]>, #[serde(skip_serializing_if = "Option::is_none")] parent_id: Option, #[serde(skip_serializing_if = "Option::is_none")] nsfw: Option, #[serde(skip_serializing_if = "Option::is_none")] - rtc_region: Option, + rtc_region: Option>, #[serde(skip_serializing_if = "Option::is_none")] video_quality_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] default_auto_archive_duration: Option, #[serde(skip_serializing_if = "Option::is_none")] default_reaction_emoji: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - available_tags: Vec, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + available_tags: Cow<'a, [ForumTag]>, #[serde(skip_serializing_if = "Option::is_none")] default_sort_order: Option, @@ -54,7 +56,7 @@ pub struct CreateChannel<'a> { impl<'a> CreateChannel<'a> { /// Creates a builder with the given name, setting [`Self::kind`] to [`ChannelType::Text`] and /// leaving all other fields empty. - pub fn new(name: impl Into) -> Self { + pub fn new(name: impl Into>) -> Self { Self { name: name.into(), nsfw: None, @@ -65,13 +67,13 @@ impl<'a> CreateChannel<'a> { user_limit: None, rate_limit_per_user: None, kind: ChannelType::Text, - permission_overwrites: Vec::new(), + permission_overwrites: Cow::default(), audit_log_reason: None, rtc_region: None, video_quality_mode: None, default_auto_archive_duration: None, default_reaction_emoji: None, - available_tags: Vec::new(), + available_tags: Cow::default(), default_sort_order: None, } } @@ -79,7 +81,7 @@ impl<'a> CreateChannel<'a> { /// Specify how to call this new channel, replacing the current value as set in [`Self::new`]. /// /// **Note**: Must be between 2 and 100 characters long. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = name.into(); self } @@ -95,15 +97,15 @@ impl<'a> CreateChannel<'a> { /// Only for [`ChannelType::Text`], [`ChannelType::Voice`], [`ChannelType::News`], /// [`ChannelType::Stage`], [`ChannelType::Forum`] #[doc(alias = "parent_id")] - pub fn category(mut self, id: impl Into) -> Self { - self.parent_id = Some(id.into()); + pub fn category(mut self, id: ChannelId) -> Self { + self.parent_id = Some(id); self } /// Channel topic (0-1024 characters) /// /// Only for [`ChannelType::Text`], [`ChannelType::News`], [`ChannelType::Forum`] - pub fn topic(mut self, topic: impl Into) -> Self { + pub fn topic(mut self, topic: impl Into>) -> Self { self.topic = Some(topic.into()); self } @@ -133,7 +135,7 @@ impl<'a> CreateChannel<'a> { /// Set how many users may occupy this voice channel /// /// Only for [`ChannelType::Voice`] and [`ChannelType::Stage`] - pub fn user_limit(mut self, limit: u32) -> Self { + pub fn user_limit(mut self, limit: NonMaxU16) -> Self { self.user_limit = Some(limit); self } @@ -148,7 +150,7 @@ impl<'a> CreateChannel<'a> { /// [`MANAGE_MESSAGES`]: crate::model::permissions::Permissions::MANAGE_MESSAGES /// [`MANAGE_CHANNELS`]: crate::model::permissions::Permissions::MANAGE_CHANNELS #[doc(alias = "slowmode")] - pub fn rate_limit_per_user(mut self, seconds: u16) -> Self { + pub fn rate_limit_per_user(mut self, seconds: NonMaxU16) -> Self { self.rate_limit_per_user = Some(seconds); self } @@ -190,8 +192,8 @@ impl<'a> CreateChannel<'a> { /// # Ok(()) /// # } /// ``` - pub fn permissions(mut self, perms: impl IntoIterator) -> Self { - self.permission_overwrites = perms.into_iter().map(Into::into).collect(); + pub fn permissions(mut self, perms: impl Into>) -> Self { + self.permission_overwrites = perms.into(); self } @@ -204,7 +206,7 @@ impl<'a> CreateChannel<'a> { /// Channel voice region id of the voice or stage channel, automatic when not set /// /// Only for [`ChannelType::Voice`] and [`ChannelType::Stage`] - pub fn rtc_region(mut self, rtc_region: String) -> Self { + pub fn rtc_region(mut self, rtc_region: Cow<'a, str>) -> Self { self.rtc_region = Some(rtc_region); self } @@ -240,8 +242,8 @@ impl<'a> CreateChannel<'a> { /// Set of tags that can be used in a forum channel /// /// Only for [`ChannelType::Forum`] - pub fn available_tags(mut self, available_tags: impl IntoIterator) -> Self { - self.available_tags = available_tags.into_iter().collect(); + pub fn available_tags(mut self, available_tags: impl Into>) -> Self { + self.available_tags = available_tags.into(); self } @@ -252,13 +254,6 @@ impl<'a> CreateChannel<'a> { self.default_sort_order = Some(default_sort_order); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for CreateChannel<'a> { - type Context<'ctx> = GuildId; - type Built = GuildChannel; /// Creates a new [`Channel`] in the guild. /// @@ -266,18 +261,11 @@ impl<'a> Builder for CreateChannel<'a> { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - #[cfg(feature = "cache")] - crate::utils::user_has_guild_perms(&cache_http, ctx, Permissions::MANAGE_CHANNELS)?; - - cache_http.http().create_channel(ctx, &self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, guild_id: GuildId) -> Result { + http.create_channel(guild_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/create_command.rs b/src/builder/create_command.rs index 60a0278ff88..618b353468a 100644 --- a/src/builder/create_command.rs +++ b/src/builder/create_command.rs @@ -1,7 +1,7 @@ +use std::borrow::Cow; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; use crate::internal::prelude::*; use crate::model::prelude::*; @@ -14,17 +14,44 @@ use crate::model::prelude::*; /// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure). #[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateCommandOption(CommandOption); +pub struct CreateCommandOption<'a> { + #[serde(rename = "type")] + kind: CommandOptionType, + name: Cow<'a, str>, + #[serde(skip_serializing_if = "Option::is_none")] + name_localizations: Option, Cow<'a, str>>>, + description: Cow<'a, str>, + #[serde(skip_serializing_if = "Option::is_none")] + description_localizations: Option, Cow<'a, str>>>, + #[serde(default)] + required: bool, + #[serde(default)] + choices: Cow<'a, [CreateCommandOptionChoice<'a>]>, + #[serde(default)] + options: Cow<'a, [CreateCommandOption<'a>]>, + #[serde(default)] + channel_types: Cow<'a, [ChannelType]>, + #[serde(default)] + min_value: Option, + #[serde(default)] + max_value: Option, + #[serde(default)] + min_length: Option, + #[serde(default)] + max_length: Option, + #[serde(default)] + autocomplete: bool, +} -impl CreateCommandOption { +impl<'a> CreateCommandOption<'a> { /// Creates a new builder with the given option type, name, and description, leaving all other /// fields empty. pub fn new( kind: CommandOptionType, - name: impl Into, - description: impl Into, + name: impl Into>, + description: impl Into>, ) -> Self { - Self(CommandOption { + Self { kind, name: name.into(), name_localizations: None, @@ -37,23 +64,23 @@ impl CreateCommandOption { min_length: None, max_length: None, - channel_types: Vec::new(), - choices: Vec::new(), - options: Vec::new(), - }) + channel_types: Cow::default(), + choices: Cow::default(), + options: Cow::default(), + } } /// Sets the `CommandOptionType`, replacing the current value as set in [`Self::new`]. pub fn kind(mut self, kind: CommandOptionType) -> Self { - self.0.kind = kind; + self.kind = kind; self } /// Sets the name of the option, replacing the current value as set in [`Self::new`]. /// /// **Note**: Must be between 1 and 32 lowercase characters, matching `r"^[\w-]{1,32}$"`. - pub fn name(mut self, name: impl Into) -> Self { - self.0.name = name.into(); + pub fn name(mut self, name: impl Into>) -> Self { + self.name = name.into(); self } @@ -67,8 +94,12 @@ impl CreateCommandOption { /// .name_localized("zh-CN", "岁数") /// # ; /// ``` - pub fn name_localized(mut self, locale: impl Into, name: impl Into) -> Self { - let map = self.0.name_localizations.get_or_insert_with(Default::default); + pub fn name_localized( + mut self, + locale: impl Into>, + name: impl Into>, + ) -> Self { + let map = self.name_localizations.get_or_insert_with(Default::default); map.insert(locale.into(), name.into()); self } @@ -76,8 +107,8 @@ impl CreateCommandOption { /// Sets the description for the option, replacing the current value as set in [`Self::new`]. /// /// **Note**: Must be between 1 and 100 characters. - pub fn description(mut self, description: impl Into) -> Self { - self.0.description = description.into(); + pub fn description(mut self, description: impl Into>) -> Self { + self.description = description.into(); self } /// Specifies a localized description of the option. @@ -92,10 +123,10 @@ impl CreateCommandOption { /// ``` pub fn description_localized( mut self, - locale: impl Into, - description: impl Into, + locale: impl Into>, + description: impl Into>, ) -> Self { - let map = self.0.description_localizations.get_or_insert_with(Default::default); + let map = self.description_localizations.get_or_insert_with(Default::default); map.insert(locale.into(), description.into()); self } @@ -104,7 +135,7 @@ impl CreateCommandOption { /// /// **Note**: This defaults to `false`. pub fn required(mut self, required: bool) -> Self { - self.0.required = required; + self.required = required; self } @@ -112,8 +143,8 @@ impl CreateCommandOption { /// /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 /// characters. Value must be between -2^53 and 2^53. - pub fn add_int_choice(self, name: impl Into, value: i32) -> Self { - self.add_choice(CommandOptionChoice { + pub fn add_int_choice(self, name: impl Into>, value: i64) -> Self { + self.add_choice(CreateCommandOptionChoice { name: name.into(), value: Value::from(value), name_localizations: None, @@ -123,16 +154,14 @@ impl CreateCommandOption { /// Adds a localized optional int-choice. See [`Self::add_int_choice`] for more info. pub fn add_int_choice_localized( self, - name: impl Into, - value: i32, - locales: impl IntoIterator, impl Into)>, + name: impl Into>, + value: i64, + locales: impl Into, Cow<'a, str>>>, ) -> Self { - self.add_choice(CommandOptionChoice { + self.add_choice(CreateCommandOptionChoice { name: name.into(), - value: Value::from(value), - name_localizations: Some( - locales.into_iter().map(|(l, n)| (l.into(), n.into())).collect(), - ), + value: value.into(), + name_localizations: Some(locales.into()), }) } @@ -140,8 +169,12 @@ impl CreateCommandOption { /// /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 /// characters. Value must be up to 100 characters. - pub fn add_string_choice(self, name: impl Into, value: impl Into) -> Self { - self.add_choice(CommandOptionChoice { + pub fn add_string_choice( + self, + name: impl Into>, + value: impl Into, + ) -> Self { + self.add_choice(CreateCommandOptionChoice { name: name.into(), value: Value::String(value.into()), name_localizations: None, @@ -151,16 +184,14 @@ impl CreateCommandOption { /// Adds a localized optional string-choice. See [`Self::add_string_choice`] for more info. pub fn add_string_choice_localized( self, - name: impl Into, + name: impl Into>, value: impl Into, - locales: impl IntoIterator, impl Into)>, + locales: impl Into, Cow<'a, str>>>, ) -> Self { - self.add_choice(CommandOptionChoice { + self.add_choice(CreateCommandOptionChoice { name: name.into(), value: Value::String(value.into()), - name_localizations: Some( - locales.into_iter().map(|(l, n)| (l.into(), n.into())).collect(), - ), + name_localizations: Some(locales.into()), }) } @@ -168,8 +199,8 @@ impl CreateCommandOption { /// /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 /// characters. Value must be between -2^53 and 2^53. - pub fn add_number_choice(self, name: impl Into, value: f64) -> Self { - self.add_choice(CommandOptionChoice { + pub fn add_number_choice(self, name: impl Into>, value: f64) -> Self { + self.add_choice(CreateCommandOptionChoice { name: name.into(), value: Value::from(value), name_localizations: None, @@ -179,21 +210,19 @@ impl CreateCommandOption { /// Adds a localized optional number-choice. See [`Self::add_number_choice`] for more info. pub fn add_number_choice_localized( self, - name: impl Into, + name: impl Into>, value: f64, - locales: impl IntoIterator, impl Into)>, + locales: impl Into, Cow<'a, str>>>, ) -> Self { - self.add_choice(CommandOptionChoice { + self.add_choice(CreateCommandOptionChoice { name: name.into(), value: Value::from(value), - name_localizations: Some( - locales.into_iter().map(|(l, n)| (l.into(), n.into())).collect(), - ), + name_localizations: Some(locales.into()), }) } - fn add_choice(mut self, value: CommandOptionChoice) -> Self { - self.0.choices.push(value); + fn add_choice(mut self, value: CreateCommandOptionChoice<'a>) -> Self { + self.choices.to_mut().push(value); self } @@ -203,7 +232,7 @@ impl CreateCommandOption { /// - May not be set to `true` if `choices` are set /// - Options using `autocomplete` are not confined to only use given choices pub fn set_autocomplete(mut self, value: bool) -> Self { - self.0.autocomplete = value; + self.autocomplete = value; self } @@ -219,9 +248,9 @@ impl CreateCommandOption { /// [`SubCommand`]: crate::model::application::CommandOptionType::SubCommand pub fn set_sub_options( mut self, - sub_options: impl IntoIterator, + sub_options: impl Into]>>, ) -> Self { - self.0.options = sub_options.into_iter().map(|o| o.0).collect(); + self.options = sub_options.into(); self } @@ -232,40 +261,40 @@ impl CreateCommandOption { /// /// [`SubCommandGroup`]: crate::model::application::CommandOptionType::SubCommandGroup /// [`SubCommand`]: crate::model::application::CommandOptionType::SubCommand - pub fn add_sub_option(mut self, sub_option: CreateCommandOption) -> Self { - self.0.options.push(sub_option.0); + pub fn add_sub_option(mut self, sub_option: CreateCommandOption<'a>) -> Self { + self.options.to_mut().push(sub_option); self } /// If the option is a [`Channel`], it will only be able to show these types. /// /// [`Channel`]: crate::model::application::CommandOptionType::Channel - pub fn channel_types(mut self, channel_types: Vec) -> Self { - self.0.channel_types = channel_types; + pub fn channel_types(mut self, channel_types: impl Into>) -> Self { + self.channel_types = channel_types.into(); self } /// Sets the minimum permitted value for this integer option - pub fn min_int_value(mut self, value: u64) -> Self { - self.0.min_value = Some(value.into()); + pub fn min_int_value(mut self, value: i64) -> Self { + self.min_value = Some(value.into()); self } /// Sets the maximum permitted value for this integer option - pub fn max_int_value(mut self, value: u64) -> Self { - self.0.max_value = Some(value.into()); + pub fn max_int_value(mut self, value: i64) -> Self { + self.max_value = Some(value.into()); self } /// Sets the minimum permitted value for this number option pub fn min_number_value(mut self, value: f64) -> Self { - self.0.min_value = serde_json::Number::from_f64(value); + self.min_value = serde_json::Number::from_f64(value); self } /// Sets the maximum permitted value for this number option pub fn max_number_value(mut self, value: f64) -> Self { - self.0.max_value = serde_json::Number::from_f64(value); + self.max_value = serde_json::Number::from_f64(value); self } @@ -273,7 +302,7 @@ impl CreateCommandOption { /// /// The value of `min_length` must be greater or equal to `0`. pub fn min_length(mut self, value: u16) -> Self { - self.0.min_length = Some(value); + self.min_length = Some(value); self } @@ -282,7 +311,7 @@ impl CreateCommandOption { /// /// The value of `max_length` must be greater or equal to `1`. pub fn max_length(mut self, value: u16) -> Self { - self.0.max_length = Some(value); + self.max_length = Some(value); self } @@ -299,32 +328,33 @@ impl CreateCommandOption { /// - [guild command](https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command-json-params) #[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateCommand { - name: String, - name_localizations: HashMap, +pub struct CreateCommand<'a> { + name: Cow<'a, str>, + name_localizations: HashMap, Cow<'a, str>>, #[serde(skip_serializing_if = "Option::is_none")] - description: Option, - description_localizations: HashMap, - options: Vec, + description: Option>, + description_localizations: HashMap, Cow<'a, str>>, + options: Cow<'a, [CreateCommandOption<'a>]>, #[serde(skip_serializing_if = "Option::is_none")] - default_member_permissions: Option, + default_member_permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(not(feature = "unstable"))] dm_permission: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "type")] kind: Option, - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] #[serde(skip_serializing_if = "Option::is_none")] integration_types: Option>, - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] #[serde(skip_serializing_if = "Option::is_none")] contexts: Option>, nsfw: bool, } -impl CreateCommand { +impl<'a> CreateCommand<'a> { /// Creates a new builder with the given name, leaving all other fields empty. - pub fn new(name: impl Into) -> Self { + pub fn new(name: impl Into>) -> Self { Self { kind: None, @@ -333,14 +363,15 @@ impl CreateCommand { description: None, description_localizations: HashMap::new(), default_member_permissions: None, + #[cfg(not(feature = "unstable"))] dm_permission: None, - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] integration_types: None, - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] contexts: None, - options: Vec::new(), + options: Cow::default(), nsfw: false, } } @@ -351,7 +382,7 @@ impl CreateCommand { /// **Note**: Must be between 1 and 32 lowercase characters, matching `r"^[\w-]{1,32}$"`. Two /// global commands of the same app cannot have the same name. Two guild-specific commands of /// the same app cannot have the same name. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = name.into(); self } @@ -365,7 +396,11 @@ impl CreateCommand { /// .name_localized("el", "γενέθλια") /// # ; /// ``` - pub fn name_localized(mut self, locale: impl Into, name: impl Into) -> Self { + pub fn name_localized( + mut self, + locale: impl Into>, + name: impl Into>, + ) -> Self { self.name_localizations.insert(locale.into(), name.into()); self } @@ -378,12 +413,12 @@ impl CreateCommand { /// Specifies the default permissions required to execute the command. pub fn default_member_permissions(mut self, permissions: Permissions) -> Self { - self.default_member_permissions = Some(permissions.bits().to_string()); + self.default_member_permissions = Some(permissions); self } /// Specifies if the command is available in DMs. - #[cfg_attr(feature = "unstable_discord_api", deprecated = "Use contexts instead")] + #[cfg(not(feature = "unstable"))] pub fn dm_permission(mut self, enabled: bool) -> Self { self.dm_permission = Some(enabled); self @@ -392,7 +427,7 @@ impl CreateCommand { /// Specifies the description of the application command. /// /// **Note**: Must be between 1 and 100 characters long. - pub fn description(mut self, description: impl Into) -> Self { + pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } @@ -407,8 +442,8 @@ impl CreateCommand { /// ``` pub fn description_localized( mut self, - locale: impl Into, - description: impl Into, + locale: impl Into>, + description: impl Into>, ) -> Self { self.description_localizations.insert(locale.into(), description.into()); self @@ -417,41 +452,41 @@ impl CreateCommand { /// Adds an application command option for the application command. /// /// **Note**: Application commands can have up to 25 options. - pub fn add_option(mut self, option: CreateCommandOption) -> Self { - self.options.push(option); + pub fn add_option(mut self, option: CreateCommandOption<'a>) -> Self { + self.options.to_mut().push(option); self } /// Sets all the application command options for the application command. /// /// **Note**: Application commands can have up to 25 options. - pub fn set_options(mut self, options: Vec) -> Self { - self.options = options; + pub fn set_options(mut self, options: impl Into]>>) -> Self { + self.options = options.into(); self } - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] /// Adds an installation context that this application command can be used in. pub fn add_integration_type(mut self, integration_type: InstallationContext) -> Self { self.integration_types.get_or_insert_with(Vec::default).push(integration_type); self } - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] /// Sets the installation contexts that this application command can be used in. pub fn integration_types(mut self, integration_types: Vec) -> Self { self.integration_types = Some(integration_types); self } - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] /// Adds an interaction context that this application command can be used in. pub fn add_context(mut self, context: InteractionContext) -> Self { self.contexts.get_or_insert_with(Vec::default).push(context); self } - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] /// Sets the interaction contexts that this application command can be used in. pub fn contexts(mut self, contexts: Vec) -> Self { self.contexts = Some(contexts); @@ -463,13 +498,6 @@ impl CreateCommand { self.nsfw = nsfw; self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for CreateCommand { - type Context<'ctx> = (Option, Option); - type Built = Command; /// Create a [`Command`], overriding an existing one with the same name if it exists. /// @@ -485,13 +513,14 @@ impl Builder for CreateCommand { /// May also return [`Error::Json`] if there is an error in deserializing the API response. /// /// [Discord's docs]: https://discord.com/developers/docs/interactions/slash-commands - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - let http = cache_http.http(); - match ctx { + http: &Http, + guild_id: Option, + command_id: Option, + ) -> Result { + match (guild_id, command_id) { (Some(guild_id), Some(cmd_id)) => { http.edit_guild_command(guild_id, cmd_id, &self).await }, @@ -501,3 +530,11 @@ impl Builder for CreateCommand { } } } + +#[derive(Clone, Debug, Serialize)] +struct CreateCommandOptionChoice<'a> { + pub name: Cow<'a, str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub name_localizations: Option, Cow<'a, str>>>, + pub value: Value, +} diff --git a/src/builder/create_command_permission.rs b/src/builder/create_command_permission.rs index e5ffbcaf9cc..554a1efa443 100644 --- a/src/builder/create_command_permission.rs +++ b/src/builder/create_command_permission.rs @@ -1,7 +1,7 @@ +use std::borrow::Cow; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -14,23 +14,16 @@ use crate::model::prelude::*; // `permissions` is added to the HTTP endpoint #[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct EditCommandPermissions { - permissions: Vec, +pub struct EditCommandPermissions<'a> { + permissions: Cow<'a, [CreateCommandPermission]>, } -impl EditCommandPermissions { - pub fn new(permissions: Vec) -> Self { +impl<'a> EditCommandPermissions<'a> { + pub fn new(permissions: impl Into>) -> Self { Self { - permissions, + permissions: permissions.into(), } } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for EditCommandPermissions { - type Context<'ctx> = (GuildId, CommandId); - type Built = CommandPermissions; /// Create permissions for a guild application command. These will overwrite any existing /// permissions for that command. @@ -45,12 +38,13 @@ impl Builder for EditCommandPermissions { /// /// [Discord's docs]: https://discord.com/developers/docs/interactions/slash-commands #[cfg(feature = "http")] - async fn execute( + pub async fn execute( self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().edit_guild_command_permissions(ctx.0, ctx.1, &self).await + http: &Http, + guild_id: GuildId, + command_id: CommandId, + ) -> Result { + http.edit_guild_command_permissions(guild_id, command_id, &self).await } } @@ -101,7 +95,7 @@ impl CreateCommandPermission { /// Creates a permission overwrite for all channels in a guild pub fn all_channels(guild_id: GuildId, allow: bool) -> Self { Self(CommandPermission { - id: std::num::NonZeroU64::new(guild_id.get() - 1).expect("guild ID was 1").into(), + id: CommandPermissionId::new(guild_id.get() - 1), kind: CommandPermissionType::Channel, permission: allow, }) diff --git a/src/builder/create_components.rs b/src/builder/create_components.rs index c662c178558..769de0f9618 100644 --- a/src/builder/create_components.rs +++ b/src/builder/create_components.rs @@ -1,30 +1,32 @@ +use std::borrow::Cow; + use serde::Serialize; +use serde_json::json; -use crate::json::{self, json}; use crate::model::prelude::*; /// A builder for creating a components action row in a message. /// /// [Discord docs](https://discord.com/developers/docs/interactions/message-components#component-object). -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] #[must_use] -pub enum CreateActionRow { - Buttons(Vec), - SelectMenu(CreateSelectMenu), +pub enum CreateActionRow<'a> { + Buttons(Vec>), + SelectMenu(CreateSelectMenu<'a>), /// Only valid in modals! - InputText(CreateInputText), + InputText(CreateInputText<'a>), } -impl serde::Serialize for CreateActionRow { +impl<'a> serde::Serialize for CreateActionRow<'a> { fn serialize(&self, serializer: S) -> Result { use serde::ser::Error as _; json!({ "type": 1, "components": match self { - Self::Buttons(x) => json::to_value(x).map_err(S::Error::custom)?, - Self::SelectMenu(x) => json::to_value(vec![x]).map_err(S::Error::custom)?, - Self::InputText(x) => json::to_value(vec![x]).map_err(S::Error::custom)?, + Self::Buttons(x) => serde_json::to_value(x).map_err(S::Error::custom)?, + Self::SelectMenu(x) => serde_json::to_value(vec![x]).map_err(S::Error::custom)?, + Self::InputText(x) => serde_json::to_value(vec![x]).map_err(S::Error::custom)?, } }) .serialize(serializer) @@ -32,53 +34,64 @@ impl serde::Serialize for CreateActionRow { } /// A builder for creating a button component in a message -#[derive(Clone, Debug, Serialize, PartialEq)] +#[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateButton(Button); +pub struct CreateButton<'a> { + style: ButtonStyle, + #[serde(rename = "type")] + kind: ComponentType, + #[serde(skip_serializing_if = "Option::is_none")] + url: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + label: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + custom_id: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + emoji: Option, + #[serde(default)] + disabled: bool, +} -impl CreateButton { +impl<'a> CreateButton<'a> { /// Creates a link button to the given URL. You must also set [`Self::label`] and/or /// [`Self::emoji`] after this. /// /// Clicking this button _will not_ trigger an interaction event in your bot. - pub fn new_link(url: impl Into) -> Self { - Self(Button { + pub fn new_link(url: impl Into>) -> Self { + Self { + style: ButtonStyle::Unknown(5), kind: ComponentType::Button, - data: ButtonKind::Link { - url: url.into(), - }, + url: Some(url.into()), + custom_id: None, label: None, emoji: None, disabled: false, - }) + } } /// Creates a normal button with the given custom ID. You must also set [`Self::label`] and/or /// [`Self::emoji`] after this. - pub fn new(custom_id: impl Into) -> Self { - Self(Button { + pub fn new(custom_id: impl Into>) -> Self { + Self { kind: ComponentType::Button, - data: ButtonKind::NonLink { - style: ButtonStyle::Primary, - custom_id: custom_id.into(), - }, + style: ButtonStyle::Primary, + custom_id: Some(custom_id.into()), + url: None, label: None, emoji: None, disabled: false, - }) + } } /// Sets the custom id of the button, a developer-defined identifier. Replaces the current /// value as set in [`Self::new`]. /// /// Has no effect on link buttons. - pub fn custom_id(mut self, id: impl Into) -> Self { - if let ButtonKind::NonLink { - custom_id, .. - } = &mut self.0.data - { - *custom_id = id.into(); + pub fn custom_id(mut self, id: impl Into>) -> Self { + if self.url.is_none() { + self.custom_id = Some(id.into()); } + self } @@ -86,30 +99,28 @@ impl CreateButton { /// /// Has no effect on link buttons. pub fn style(mut self, new_style: ButtonStyle) -> Self { - if let ButtonKind::NonLink { - style, .. - } = &mut self.0.data - { - *style = new_style; + if self.url.is_none() { + self.style = new_style; } + self } /// Sets label of the button. - pub fn label(mut self, label: impl Into) -> Self { - self.0.label = Some(label.into()); + pub fn label(mut self, label: impl Into>) -> Self { + self.label = Some(label.into()); self } /// Sets emoji of the button. pub fn emoji(mut self, emoji: impl Into) -> Self { - self.0.emoji = Some(emoji.into()); + self.emoji = Some(emoji.into()); self } /// Sets the disabled state for the button. pub fn disabled(mut self, disabled: bool) -> Self { - self.0.disabled = disabled; + self.disabled = disabled; self } } @@ -128,34 +139,51 @@ impl Serialize for CreateSelectMenuDefault { } /// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure). -#[derive(Clone, Debug, PartialEq)] -pub enum CreateSelectMenuKind { - String { options: Vec }, - User { default_users: Option> }, - Role { default_roles: Option> }, - Mentionable { default_users: Option>, default_roles: Option> }, - Channel { channel_types: Option>, default_channels: Option> }, +#[derive(Clone, Debug)] +pub enum CreateSelectMenuKind<'a> { + String { + options: Cow<'a, [CreateSelectMenuOption<'a>]>, + }, + User { + default_users: Option>, + }, + Role { + default_roles: Option>, + }, + Mentionable { + default_users: Option>, + default_roles: Option>, + }, + Channel { + channel_types: Option>, + default_channels: Option>, + }, } -impl Serialize for CreateSelectMenuKind { +impl<'a> Serialize for CreateSelectMenuKind<'a> { fn serialize(&self, serializer: S) -> Result { #[derive(Serialize)] struct Json<'a> { #[serde(rename = "type")] kind: u8, #[serde(skip_serializing_if = "Option::is_none")] - options: Option<&'a [CreateSelectMenuOption]>, + options: Option<&'a [CreateSelectMenuOption<'a>]>, #[serde(skip_serializing_if = "Option::is_none")] channel_types: Option<&'a [ChannelType]>, - #[serde(skip_serializing_if = "Vec::is_empty")] - default_values: Vec, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + default_values: &'a [CreateSelectMenuDefault], } - fn map + Copy>( - values: &Option>, - ) -> impl Iterator + '_ { + fn map<'a>( + values: &'a Option + Copy]>>, + ) -> impl Iterator + 'a { // Calling `.iter().flatten()` on the `Option` treats `None` like an empty vec - values.iter().flatten().map(|&i| CreateSelectMenuDefault(i.into())) + values + .as_ref() + .map(|s| s.iter()) + .into_iter() + .flatten() + .map(|&i| CreateSelectMenuDefault(i.into())) } #[rustfmt::skip] @@ -188,7 +216,7 @@ impl Serialize for CreateSelectMenuKind { Self::Channel { channel_types, default_channels: _ } => channel_types.as_deref(), _ => None, }, - default_values, + default_values: &default_values, }; json.serialize(serializer) @@ -198,12 +226,12 @@ impl Serialize for CreateSelectMenuKind { /// A builder for creating a select menu component in a message /// /// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure). -#[derive(Clone, Debug, Serialize, PartialEq)] +#[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateSelectMenu { - custom_id: String, +pub struct CreateSelectMenu<'a> { + custom_id: Cow<'a, str>, #[serde(skip_serializing_if = "Option::is_none")] - placeholder: Option, + placeholder: Option>, #[serde(skip_serializing_if = "Option::is_none")] min_values: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -212,13 +240,13 @@ pub struct CreateSelectMenu { disabled: Option, #[serde(flatten)] - kind: CreateSelectMenuKind, + kind: CreateSelectMenuKind<'a>, } -impl CreateSelectMenu { +impl<'a> CreateSelectMenu<'a> { /// Creates a builder with given custom id (a developer-defined identifier), and a list of /// options, leaving all other fields empty. - pub fn new(custom_id: impl Into, kind: CreateSelectMenuKind) -> Self { + pub fn new(custom_id: impl Into>, kind: CreateSelectMenuKind<'a>) -> Self { Self { custom_id: custom_id.into(), placeholder: None, @@ -230,14 +258,14 @@ impl CreateSelectMenu { } /// The placeholder of the select menu. - pub fn placeholder(mut self, label: impl Into) -> Self { + pub fn placeholder(mut self, label: impl Into>) -> Self { self.placeholder = Some(label.into()); self } /// Sets the custom id of the select menu, a developer-defined identifier. Replaces the current /// value as set in [`Self::new`]. - pub fn custom_id(mut self, id: impl Into) -> Self { + pub fn custom_id(mut self, id: impl Into>) -> Self { self.custom_id = id.into(); self } @@ -264,23 +292,23 @@ impl CreateSelectMenu { /// A builder for creating an option of a select menu component in a message /// /// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure) -#[derive(Clone, Debug, Serialize, PartialEq)] +#[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateSelectMenuOption { - label: String, - value: String, +pub struct CreateSelectMenuOption<'a> { + label: Cow<'a, str>, + value: Cow<'a, str>, #[serde(skip_serializing_if = "Option::is_none")] - description: Option, + description: Option>, #[serde(skip_serializing_if = "Option::is_none")] emoji: Option, #[serde(skip_serializing_if = "Option::is_none")] default: Option, } -impl CreateSelectMenuOption { +impl<'a> CreateSelectMenuOption<'a> { /// Creates a select menu option with the given label and value, leaving all other fields /// empty. - pub fn new(label: impl Into, value: impl Into) -> Self { + pub fn new(label: impl Into>, value: impl Into>) -> Self { Self { label: label.into(), value: value.into(), @@ -291,19 +319,19 @@ impl CreateSelectMenuOption { } /// Sets the label of this option, replacing the current value as set in [`Self::new`]. - pub fn label(mut self, label: impl Into) -> Self { + pub fn label(mut self, label: impl Into>) -> Self { self.label = label.into(); self } /// Sets the value of this option, replacing the current value as set in [`Self::new`]. - pub fn value(mut self, value: impl Into) -> Self { + pub fn value(mut self, value: impl Into>) -> Self { self.value = value.into(); self } /// Sets the description shown on this option. - pub fn description(mut self, description: impl Into) -> Self { + pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } @@ -324,20 +352,33 @@ impl CreateSelectMenuOption { /// A builder for creating an input text component in a modal /// /// [Discord docs](https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure). -#[derive(Clone, Debug, Serialize, PartialEq)] +#[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateInputText(InputText); +pub struct CreateInputText<'a> { + #[serde(rename = "type")] + kind: ComponentType, + custom_id: Cow<'a, str>, + style: InputTextStyle, + label: Option>, + min_length: Option, + max_length: Option, + required: bool, + #[serde(skip_serializing_if = "Option::is_none")] + value: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + placeholder: Option>, +} -impl CreateInputText { +impl<'a> CreateInputText<'a> { /// Creates a text input with the given style, label, and custom id (a developer-defined /// identifier), leaving all other fields empty. pub fn new( style: InputTextStyle, - label: impl Into, - custom_id: impl Into, + label: impl Into>, + custom_id: impl Into>, ) -> Self { - Self(InputText { - style: Some(style), + Self { + style, label: Some(label.into()), custom_id: custom_id.into(), @@ -348,55 +389,55 @@ impl CreateInputText { required: true, kind: ComponentType::InputText, - }) + } } /// Sets the style of this input text. Replaces the current value as set in [`Self::new`]. pub fn style(mut self, kind: InputTextStyle) -> Self { - self.0.style = Some(kind); + self.style = kind; self } /// Sets the label of this input text. Replaces the current value as set in [`Self::new`]. - pub fn label(mut self, label: impl Into) -> Self { - self.0.label = Some(label.into()); + pub fn label(mut self, label: impl Into>) -> Self { + self.label = Some(label.into()); self } /// Sets the custom id of the input text, a developer-defined identifier. Replaces the current /// value as set in [`Self::new`]. - pub fn custom_id(mut self, id: impl Into) -> Self { - self.0.custom_id = id.into(); + pub fn custom_id(mut self, id: impl Into>) -> Self { + self.custom_id = id.into(); self } /// Sets the placeholder of this input text. - pub fn placeholder(mut self, label: impl Into) -> Self { - self.0.placeholder = Some(label.into()); + pub fn placeholder(mut self, label: impl Into>) -> Self { + self.placeholder = Some(label.into()); self } /// Sets the minimum length required for the input text pub fn min_length(mut self, min: u16) -> Self { - self.0.min_length = Some(min); + self.min_length = Some(min); self } /// Sets the maximum length required for the input text pub fn max_length(mut self, max: u16) -> Self { - self.0.max_length = Some(max); + self.max_length = Some(max); self } /// Sets the value of this input text. - pub fn value(mut self, value: impl Into) -> Self { - self.0.value = Some(value.into()); + pub fn value(mut self, value: impl Into>) -> Self { + self.value = Some(value.into()); self } /// Sets if the input text is required pub fn required(mut self, required: bool) -> Self { - self.0.required = required; + self.required = required; self } } diff --git a/src/builder/create_embed.rs b/src/builder/create_embed.rs index 2432d45ddd8..8fc72c8cf51 100644 --- a/src/builder/create_embed.rs +++ b/src/builder/create_embed.rs @@ -14,6 +14,8 @@ //! [`ExecuteWebhook::embeds`]: crate::builder::ExecuteWebhook::embeds //! [here]: https://discord.com/developers/docs/resources/channel#embed-object +use std::borrow::Cow; + #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -21,11 +23,38 @@ use crate::model::prelude::*; /// A builder to create an embed in a message /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#embed-object) -#[derive(Clone, Debug, Serialize, PartialEq)] +#[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateEmbed(Embed); +pub struct CreateEmbed<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + title: Option>, + #[serde(rename = "type")] + #[serde(skip_serializing_if = "Option::is_none")] + kind: Option<&'static str>, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + url: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + timestamp: Option, + #[serde(rename = "color")] + #[serde(skip_serializing_if = "Option::is_none")] + colour: Option, + #[serde(skip_serializing_if = "Option::is_none")] + footer: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + image: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + thumbnail: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + author: Option>, + /// No point using a Cow slice, as there is no set_fields method + /// and CreateEmbedField is not public. + #[serde(skip_serializing_if = "<[_]>::is_empty")] + fields: Vec>, +} -impl CreateEmbed { +impl<'a> CreateEmbed<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() @@ -34,32 +63,29 @@ impl CreateEmbed { /// Set the author of the embed. /// /// Refer to the documentation for [`CreateEmbedAuthor`] for more information. - pub fn author(mut self, author: CreateEmbedAuthor) -> Self { - self.0.author = Some(author.0); + pub fn author(mut self, author: CreateEmbedAuthor<'a>) -> Self { + self.author = Some(author); self } /// Set the colour of the left-hand side of the embed. /// /// This is an alias of [`Self::colour`]. - #[inline] pub fn color>(self, colour: C) -> Self { self.colour(colour) } /// Set the colour of the left-hand side of the embed. - #[inline] pub fn colour>(mut self, colour: C) -> Self { - self.0.colour = Some(colour.into()); + self.colour = Some(colour.into()); self } /// Set the description of the embed. /// /// **Note**: This can't be longer than 4096 characters. - #[inline] - pub fn description(mut self, description: impl Into) -> Self { - self.0.description = Some(description.into()); + pub fn description(mut self, description: impl Into>) -> Self { + self.description = Some(description.into()); self } @@ -67,14 +93,17 @@ impl CreateEmbed { /// /// **Note**: Maximum amount of characters you can put is 256 in a field name and 1024 in a /// field value. - #[inline] pub fn field( mut self, - name: impl Into, - value: impl Into, + name: impl Into>, + value: impl Into>, inline: bool, ) -> Self { - self.0.fields.push(EmbedField::new(name, value, inline)); + self.fields.push(CreateEmbedField { + name: name.into(), + value: value.into(), + inline, + }); self } @@ -83,43 +112,39 @@ impl CreateEmbed { /// This is sugar to reduce the need of calling [`Self::field`] manually multiple times. pub fn fields(mut self, fields: impl IntoIterator) -> Self where - N: Into, - V: Into, + N: Into>, + V: Into>, { - let fields = - fields.into_iter().map(|(name, value, inline)| EmbedField::new(name, value, inline)); - self.0.fields.extend(fields); + let fields = fields.into_iter().map(|(name, value, inline)| CreateEmbedField { + name: name.into(), + value: value.into(), + inline, + }); + + self.fields.extend(fields); self } /// Set the footer of the embed. /// /// Refer to the documentation for [`CreateEmbedFooter`] for more information. - pub fn footer(mut self, footer: CreateEmbedFooter) -> Self { - self.0.footer = Some(footer.0); + pub fn footer(mut self, footer: CreateEmbedFooter<'a>) -> Self { + self.footer = Some(footer); self } /// Set the image associated with the embed. This only supports HTTP(S). - #[inline] - pub fn image(mut self, url: impl Into) -> Self { - self.0.image = Some(EmbedImage { + pub fn image(mut self, url: impl Into>) -> Self { + self.image = Some(CreateEmbedImage { url: url.into(), - proxy_url: None, - height: None, - width: None, }); self } /// Set the thumbnail of the embed. This only supports HTTP(S). - #[inline] - pub fn thumbnail(mut self, url: impl Into) -> Self { - self.0.thumbnail = Some(EmbedThumbnail { + pub fn thumbnail(mut self, url: impl Into>) -> Self { + self.thumbnail = Some(CreateEmbedImage { url: url.into(), - proxy_url: None, - height: None, - width: None, }); self } @@ -138,23 +163,20 @@ impl CreateEmbed { /// let timestamp: Timestamp = "2004-06-08T16:04:23Z".parse().expect("Invalid timestamp!"); /// let embed = CreateEmbed::new().title("hello").timestamp(timestamp); /// ``` - #[inline] pub fn timestamp>(mut self, timestamp: T) -> Self { - self.0.timestamp = Some(timestamp.into()); + self.timestamp = Some(timestamp.into()); self } /// Set the title of the embed. - #[inline] - pub fn title(mut self, title: impl Into) -> Self { - self.0.title = Some(title.into()); + pub fn title(mut self, title: impl Into>) -> Self { + self.title = Some(title.into()); self } /// Set the URL to direct to when clicking on the title. - #[inline] - pub fn url(mut self, url: impl Into) -> Self { - self.0.url = Some(url.into()); + pub fn url(mut self, url: impl Into>) -> Self { + self.url = Some(url.into()); self } @@ -164,7 +186,6 @@ impl CreateEmbed { /// with the provided filename. Or else this won't work. /// /// [`ChannelId::send_files`]: crate::model::id::ChannelId::send_files - #[inline] pub fn attachment(self, filename: impl Into) -> Self { let mut filename = filename.into(); filename.insert_str(0, "attachment://"); @@ -172,105 +193,120 @@ impl CreateEmbed { } #[cfg(feature = "http")] - pub(super) fn check_length(&self) -> Result<()> { + pub(super) fn get_length(&self) -> usize { let mut length = 0; - if let Some(ref author) = self.0.author { + if let Some(author) = &self.author { length += author.name.chars().count(); } - if let Some(ref description) = self.0.description { + if let Some(description) = &self.description { length += description.chars().count(); } - for field in &self.0.fields { + for field in &self.fields { length += field.name.chars().count(); length += field.value.chars().count(); } - if let Some(ref footer) = self.0.footer { + if let Some(footer) = &self.footer { length += footer.text.chars().count(); } - if let Some(ref title) = self.0.title { + if let Some(title) = &self.title { length += title.chars().count(); } - super::check_overflow(length, crate::constants::EMBED_MAX_LENGTH) - .map_err(|overflow| Error::Model(ModelError::EmbedTooLarge(overflow))) + length } } -impl Default for CreateEmbed { +impl<'a> Default for CreateEmbed<'a> { /// Creates a builder with default values, setting the `type` to `rich`. fn default() -> Self { - Self(Embed { + Self { fields: Vec::new(), description: None, thumbnail: None, timestamp: None, - kind: Some("rich".into()), + kind: Some("rich"), author: None, colour: None, footer: None, image: None, title: None, url: None, - video: None, - provider: None, - }) + } } } -impl From for CreateEmbed { +impl<'a> From for CreateEmbed<'a> { fn from(embed: Embed) -> Self { - Self(embed) + Self { + fields: embed.fields.into_iter().map(Into::into).collect(), + description: embed.description.map(FixedString::into_string).map(Into::into), + thumbnail: embed.thumbnail.map(Into::into), + timestamp: embed.timestamp, + kind: Some("rich"), + author: embed.author.map(Into::into), + colour: embed.colour, + footer: embed.footer.map(Into::into), + image: embed.image.map(Into::into), + title: embed.title.map(FixedString::into_string).map(Into::into), + url: embed.url.map(FixedString::into_string).map(Into::into), + } } } /// A builder to create the author data of an emebd. See [`CreateEmbed::author`] #[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateEmbedAuthor(EmbedAuthor); +pub struct CreateEmbedAuthor<'a> { + name: Cow<'a, str>, + url: Option>, + icon_url: Option>, +} -impl CreateEmbedAuthor { +impl<'a> CreateEmbedAuthor<'a> { /// Creates an author object with the given name, leaving all other fields empty. - pub fn new(name: impl Into) -> Self { - Self(EmbedAuthor { + pub fn new(name: impl Into>) -> Self { + Self { name: name.into(), icon_url: None, url: None, - // Has no builder method because I think this field is only relevant when receiving (?) - proxy_icon_url: None, - }) + } } /// Set the author's name, replacing the current value as set in [`Self::new`]. - pub fn name(mut self, name: impl Into) -> Self { - self.0.name = name.into(); + pub fn name(mut self, name: impl Into>) -> Self { + self.name = name.into(); self } /// Set the URL of the author's icon. - pub fn icon_url(mut self, icon_url: impl Into) -> Self { - self.0.icon_url = Some(icon_url.into()); + pub fn icon_url(mut self, icon_url: impl Into>) -> Self { + self.icon_url = Some(icon_url.into()); self } /// Set the author's URL. - pub fn url(mut self, url: impl Into) -> Self { - self.0.url = Some(url.into()); + pub fn url(mut self, url: impl Into>) -> Self { + self.url = Some(url.into()); self } } -impl From for CreateEmbedAuthor { +impl<'a> From for CreateEmbedAuthor<'a> { fn from(author: EmbedAuthor) -> Self { - Self(author) + Self { + name: author.name.into_string().into(), + url: author.url.map(|f| f.into_string().into()), + icon_url: author.icon_url.map(|f| f.into_string().into()), + } } } #[cfg(feature = "model")] -impl From for CreateEmbedAuthor { +impl From for CreateEmbedAuthor<'_> { fn from(user: User) -> Self { let avatar_icon = user.face(); Self::new(user.name).icon_url(avatar_icon) @@ -280,34 +316,86 @@ impl From for CreateEmbedAuthor { /// A builder to create the footer data for an embed. See [`CreateEmbed::footer`] #[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateEmbedFooter(EmbedFooter); +pub struct CreateEmbedFooter<'a> { + text: Cow<'a, str>, + icon_url: Option>, +} -impl CreateEmbedFooter { +impl<'a> CreateEmbedFooter<'a> { /// Creates a new footer object with the given text, leaving all other fields empty. - pub fn new(text: impl Into) -> Self { - Self(EmbedFooter { + pub fn new(text: impl Into>) -> Self { + Self { text: text.into(), icon_url: None, - // Has no builder method because I think this field is only relevant when receiving (?) - proxy_icon_url: None, - }) + } } /// Set the footer's text, replacing the current value as set in [`Self::new`]. - pub fn text(mut self, text: impl Into) -> Self { - self.0.text = text.into(); + pub fn text(mut self, text: impl Into>) -> Self { + self.text = text.into(); self } /// Set the icon URL's value. This only supports HTTP(S). - pub fn icon_url(mut self, icon_url: impl Into) -> Self { - self.0.icon_url = Some(icon_url.into()); + pub fn icon_url(mut self, icon_url: impl Into>) -> Self { + self.icon_url = Some(icon_url.into()); self } } -impl From for CreateEmbedFooter { +impl<'a> From for CreateEmbedFooter<'a> { fn from(footer: EmbedFooter) -> Self { - Self(footer) + Self { + text: footer.text.into_string().into(), + icon_url: footer.icon_url.map(|f| f.into_string().into()), + } + } +} + +#[derive(Clone, Debug, Serialize)] +struct CreateEmbedField<'a> { + name: Cow<'a, str>, + value: Cow<'a, str>, + inline: bool, +} + +impl<'a> From<&'a EmbedField> for CreateEmbedField<'a> { + fn from(field: &'a EmbedField) -> Self { + Self { + name: field.name.as_str().into(), + value: field.value.as_str().into(), + inline: field.inline, + } + } +} + +impl<'a> From for CreateEmbedField<'a> { + fn from(field: EmbedField) -> Self { + Self { + name: field.name.into_string().into(), + value: field.value.into_string().into(), + inline: field.inline, + } + } +} + +#[derive(Clone, Debug, Serialize)] +struct CreateEmbedImage<'a> { + url: Cow<'a, str>, +} + +impl<'a> From for CreateEmbedImage<'a> { + fn from(field: EmbedImage) -> Self { + Self { + url: field.url.into_string().into(), + } + } +} + +impl<'a> From for CreateEmbedImage<'a> { + fn from(field: EmbedThumbnail) -> Self { + Self { + url: field.url.into_string().into(), + } } } diff --git a/src/builder/create_forum_post.rs b/src/builder/create_forum_post.rs index 8a0f6872593..5cf358b083b 100644 --- a/src/builder/create_forum_post.rs +++ b/src/builder/create_forum_post.rs @@ -1,8 +1,10 @@ -#[cfg(feature = "http")] -use super::Builder; +use std::borrow::Cow; + +use nonmax::NonMaxU16; + use super::CreateMessage; #[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -11,14 +13,14 @@ use crate::model::prelude::*; #[derive(Clone, Debug, Serialize)] #[must_use] pub struct CreateForumPost<'a> { - name: String, + name: Cow<'a, str>, #[serde(skip_serializing_if = "Option::is_none")] auto_archive_duration: Option, #[serde(skip_serializing_if = "Option::is_none")] - rate_limit_per_user: Option, - message: CreateMessage, - #[serde(skip_serializing_if = "Vec::is_empty")] - applied_tags: Vec, + rate_limit_per_user: Option, + message: CreateMessage<'a>, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + applied_tags: Cow<'a, [ForumTagId]>, #[serde(skip)] audit_log_reason: Option<&'a str>, @@ -26,13 +28,13 @@ pub struct CreateForumPost<'a> { impl<'a> CreateForumPost<'a> { /// Creates a builder with the given name and message content, leaving all other fields empty. - pub fn new(name: impl Into, message: CreateMessage) -> Self { + pub fn new(name: impl Into>, message: CreateMessage<'a>) -> Self { Self { name: name.into(), message, auto_archive_duration: None, rate_limit_per_user: None, - applied_tags: Vec::new(), + applied_tags: Cow::default(), audit_log_reason: None, } } @@ -40,7 +42,7 @@ impl<'a> CreateForumPost<'a> { /// The name of the forum post. Replaces the current value as set in [`Self::new`]. /// /// **Note**: Must be between 2 and 100 characters long. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = name.into(); self } @@ -48,7 +50,7 @@ impl<'a> CreateForumPost<'a> { /// The contents of the first message in the forum post. /// /// See [`CreateMessage`] for restrictions around message size. - pub fn message(mut self, message: CreateMessage) -> Self { + pub fn message(mut self, message: CreateMessage<'a>) -> Self { self.message = message; self } @@ -69,21 +71,18 @@ impl<'a> CreateForumPost<'a> { /// [`MANAGE_MESSAGES`]: crate::model::permissions::Permissions::MANAGE_MESSAGES /// [`MANAGE_CHANNELS`]: crate::model::permissions::Permissions::MANAGE_CHANNELS #[doc(alias = "slowmode")] - pub fn rate_limit_per_user(mut self, seconds: u16) -> Self { + pub fn rate_limit_per_user(mut self, seconds: NonMaxU16) -> Self { self.rate_limit_per_user = Some(seconds); self } pub fn add_applied_tag(mut self, applied_tag: ForumTagId) -> Self { - self.applied_tags.push(applied_tag); + self.applied_tags.to_mut().push(applied_tag); self } - pub fn set_applied_tags( - mut self, - applied_tags: impl IntoIterator>, - ) -> Self { - self.applied_tags = applied_tags.into_iter().map(Into::into).collect(); + pub fn set_applied_tags(mut self, applied_tags: impl Into>) -> Self { + self.applied_tags = applied_tags.into(); self } @@ -92,28 +91,15 @@ impl<'a> CreateForumPost<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for CreateForumPost<'a> { - type Context<'ctx> = ChannelId; - type Built = GuildChannel; /// Creates a forum post in the given channel. /// /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - async fn execute( - mut self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { + #[cfg(feature = "http")] + pub async fn execute(mut self, http: &Http, channel_id: ChannelId) -> Result { let files = self.message.attachments.take_files(); - cache_http - .http() - .create_forum_post_with_attachments(ctx, &self, files, self.audit_log_reason) - .await + http.create_forum_post(channel_id, &self, files, self.audit_log_reason).await } } diff --git a/src/builder/create_forum_tag.rs b/src/builder/create_forum_tag.rs index 31db70451d5..bdb6fe98df4 100644 --- a/src/builder/create_forum_tag.rs +++ b/src/builder/create_forum_tag.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use crate::model::prelude::*; /// [Discord docs](https://discord.com/developers/docs/resources/channel#forum-tag-object-forum-tag-structure) @@ -5,15 +7,15 @@ use crate::model::prelude::*; /// Contrary to the [`ForumTag`] struct, only the name field is required. #[must_use] #[derive(Clone, Debug, Serialize)] -pub struct CreateForumTag { - name: String, +pub struct CreateForumTag<'a> { + name: Cow<'a, str>, moderated: bool, emoji_id: Option, - emoji_name: Option, + emoji_name: Option>, } -impl CreateForumTag { - pub fn new(name: impl Into) -> Self { +impl<'a> CreateForumTag<'a> { + pub fn new(name: impl Into>) -> Self { Self { name: name.into(), moderated: false, @@ -37,7 +39,7 @@ impl CreateForumTag { }, ReactionType::Unicode(unicode_emoji) => { self.emoji_id = None; - self.emoji_name = Some(unicode_emoji); + self.emoji_name = Some(unicode_emoji.into_string().into()); }, } self diff --git a/src/builder/create_interaction_response.rs b/src/builder/create_interaction_response.rs index 3d29f98df04..855ae2d2d44 100644 --- a/src/builder/create_interaction_response.rs +++ b/src/builder/create_interaction_response.rs @@ -1,5 +1,7 @@ -#[cfg(feature = "http")] -use super::{check_overflow, Builder}; +use std::borrow::Cow; + +use serde_json::json; + use super::{ CreateActionRow, CreateAllowedMentions, @@ -8,16 +10,13 @@ use super::{ EditAttachments, }; #[cfg(feature = "http")] -use crate::constants; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; use crate::internal::prelude::*; -use crate::json::{self, json}; use crate::model::prelude::*; /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object). #[derive(Clone, Debug)] -pub enum CreateInteractionResponse { +pub enum CreateInteractionResponse<'a> { /// Acknowledges a Ping (only required when your bot uses an HTTP endpoint URL). /// /// Corresponds to Discord's `PONG`. @@ -25,12 +24,12 @@ pub enum CreateInteractionResponse { /// Responds to an interaction with a message. /// /// Corresponds to Discord's `CHANNEL_MESSAGE_WITH_SOURCE`. - Message(CreateInteractionResponseMessage), + Message(CreateInteractionResponseMessage<'a>), /// Acknowledges the interaction in order to edit a response later. The user sees a loading /// state. /// /// Corresponds to Discord's `DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE`. - Defer(CreateInteractionResponseMessage), + Defer(CreateInteractionResponseMessage<'a>), /// Only valid for component-based interactions (seems to work for modal submit interactions /// too even though it's not documented). /// @@ -44,19 +43,19 @@ pub enum CreateInteractionResponse { /// Edits the message the component was attached to. /// /// Corresponds to Discord's `UPDATE_MESSAGE`. - UpdateMessage(CreateInteractionResponseMessage), + UpdateMessage(CreateInteractionResponseMessage<'a>), /// Only valid for autocomplete interactions. /// /// Responds to the autocomplete interaction with suggested choices. /// /// Corresponds to Discord's `APPLICATION_COMMAND_AUTOCOMPLETE_RESULT`. - Autocomplete(CreateAutocompleteResponse), + Autocomplete(CreateAutocompleteResponse<'a>), /// Not valid for Modal and Ping interactions /// /// Responds to the interaction with a popup modal. /// /// Corresponds to Discord's `MODAL`. - Modal(CreateModal), + Modal(CreateModal<'a>), /// Not valid for autocomplete and Ping interactions. Only available for applications with /// monetization enabled. /// @@ -66,7 +65,7 @@ pub enum CreateInteractionResponse { PremiumRequired, } -impl serde::Serialize for CreateInteractionResponse { +impl serde::Serialize for CreateInteractionResponse<'_> { fn serialize(&self, serializer: S) -> StdResult { use serde::ser::Error as _; @@ -83,50 +82,32 @@ impl serde::Serialize for CreateInteractionResponse { Self::PremiumRequired => 10, }, "data": match self { - Self::Pong => json::NULL, - Self::Message(x) => json::to_value(x).map_err(S::Error::custom)?, - Self::Defer(x) => json::to_value(x).map_err(S::Error::custom)?, - Self::Acknowledge => json::NULL, - Self::UpdateMessage(x) => json::to_value(x).map_err(S::Error::custom)?, - Self::Autocomplete(x) => json::to_value(x).map_err(S::Error::custom)?, - Self::Modal(x) => json::to_value(x).map_err(S::Error::custom)?, - Self::PremiumRequired => json::NULL, + Self::Pong => Value::Null, + Self::Message(x) => serde_json::to_value(x).map_err(S::Error::custom)?, + Self::Defer(x) => serde_json::to_value(x).map_err(S::Error::custom)?, + Self::Acknowledge => Value::Null, + Self::UpdateMessage(x) => serde_json::to_value(x).map_err(S::Error::custom)?, + Self::Autocomplete(x) => serde_json::to_value(x).map_err(S::Error::custom)?, + Self::Modal(x) => serde_json::to_value(x).map_err(S::Error::custom)?, + Self::PremiumRequired => Value::Null, } }) .serialize(serializer) } } -impl CreateInteractionResponse { +impl CreateInteractionResponse<'_> { #[cfg(feature = "http")] - fn check_length(&self) -> Result<()> { + fn check_length(&self) -> Result<(), ModelError> { if let CreateInteractionResponse::Message(data) | CreateInteractionResponse::Defer(data) | CreateInteractionResponse::UpdateMessage(data) = self { - if let Some(content) = &data.content { - check_overflow(content.chars().count(), constants::MESSAGE_CODE_LIMIT) - .map_err(|overflow| Error::Model(ModelError::MessageTooLong(overflow)))?; - } - - if let Some(embeds) = &data.embeds { - check_overflow(embeds.len(), constants::EMBED_MAX_COUNT) - .map_err(|_| Error::Model(ModelError::EmbedAmount))?; - - for embed in embeds { - embed.check_length()?; - } - } + super::check_lengths(data.content.as_deref(), data.embeds.as_deref(), 0) + } else { + Ok(()) } - Ok(()) } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for CreateInteractionResponse { - type Context<'ctx> = (InteractionId, &'ctx str); - type Built = (); /// Creates a response to the interaction received. /// @@ -138,11 +119,13 @@ impl Builder for CreateInteractionResponse { /// Returns an [`Error::Model`] if the message content is too long. May also return an /// [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is an error in /// deserializing the API response. - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( mut self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { + http: &Http, + interaction_id: InteractionId, + interaction_token: &str, + ) -> Result<()> { self.check_length()?; let files = match &mut self { CreateInteractionResponse::Message(msg) @@ -151,37 +134,36 @@ impl Builder for CreateInteractionResponse { _ => Vec::new(), }; - let http = cache_http.http(); if let Self::Message(msg) | Self::Defer(msg) | Self::UpdateMessage(msg) = &mut self { if msg.allowed_mentions.is_none() { msg.allowed_mentions.clone_from(&http.default_allowed_mentions); } }; - http.create_interaction_response(ctx.0, ctx.1, &self, files).await + http.create_interaction_response(interaction_id, interaction_token, &self, files).await } } /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-messages). #[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct CreateInteractionResponseMessage { +pub struct CreateInteractionResponseMessage<'a> { #[serde(skip_serializing_if = "Option::is_none")] tts: Option, #[serde(skip_serializing_if = "Option::is_none")] - content: Option, + content: Option>, #[serde(skip_serializing_if = "Option::is_none")] - embeds: Option>, + embeds: Option]>>, #[serde(skip_serializing_if = "Option::is_none")] - allowed_mentions: Option, + allowed_mentions: Option>, #[serde(skip_serializing_if = "Option::is_none")] flags: Option, #[serde(skip_serializing_if = "Option::is_none")] - components: Option>, - attachments: EditAttachments, + components: Option]>>, + attachments: EditAttachments<'a>, } -impl CreateInteractionResponseMessage { +impl<'a> CreateInteractionResponseMessage<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() @@ -198,13 +180,13 @@ impl CreateInteractionResponseMessage { } /// Appends a file to the message. - pub fn add_file(mut self, file: CreateAttachment) -> Self { + pub fn add_file(mut self, file: CreateAttachment<'a>) -> Self { self.attachments = self.attachments.add(file); self } /// Appends a list of files to the message. - pub fn add_files(mut self, files: impl IntoIterator) -> Self { + pub fn add_files(mut self, files: impl IntoIterator>) -> Self { for file in files { self.attachments = self.attachments.add(file); } @@ -215,7 +197,7 @@ impl CreateInteractionResponseMessage { /// /// Calling this multiple times will overwrite the file list. To append files, call /// [`Self::add_file`] or [`Self::add_files`] instead. - pub fn files(mut self, files: impl IntoIterator) -> Self { + pub fn files(mut self, files: impl IntoIterator>) -> Self { self.attachments = EditAttachments::new(); self.add_files(files) } @@ -223,8 +205,7 @@ impl CreateInteractionResponseMessage { /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. - #[inline] - pub fn content(mut self, content: impl Into) -> Self { + pub fn content(mut self, content: impl Into>) -> Self { self.content = Some(content.into()); self } @@ -232,16 +213,16 @@ impl CreateInteractionResponseMessage { /// Adds an embed to the message. /// /// Calling this while editing a message will overwrite existing embeds. - pub fn add_embed(mut self, embed: CreateEmbed) -> Self { - self.embeds.get_or_insert_with(Vec::new).push(embed); + pub fn add_embed(mut self, embed: CreateEmbed<'a>) -> Self { + self.embeds.get_or_insert_with(Cow::default).to_mut().push(embed); self } /// Adds multiple embeds for the message. /// /// Calling this while editing a message will overwrite existing embeds. - pub fn add_embeds(mut self, embeds: Vec) -> Self { - self.embeds.get_or_insert_with(Vec::new).extend(embeds); + pub fn add_embeds(mut self, embeds: impl IntoIterator>) -> Self { + self.embeds.get_or_insert_with(Cow::default).to_mut().extend(embeds); self } @@ -249,7 +230,7 @@ impl CreateInteractionResponseMessage { /// /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embed`] /// instead. - pub fn embed(self, embed: CreateEmbed) -> Self { + pub fn embed(self, embed: CreateEmbed<'a>) -> Self { self.embeds(vec![embed]) } @@ -257,13 +238,13 @@ impl CreateInteractionResponseMessage { /// /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embeds`] /// instead. - pub fn embeds(mut self, embeds: Vec) -> Self { - self.embeds = Some(embeds); + pub fn embeds(mut self, embeds: impl Into]>>) -> Self { + self.embeds = Some(embeds.into()); self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions<'a>) -> Self { self.allowed_mentions = Some(allowed_mentions); self } @@ -289,8 +270,8 @@ impl CreateInteractionResponseMessage { } /// Sets the components of this message. - pub fn components(mut self, components: Vec) -> Self { - self.components = Some(components); + pub fn components(mut self, components: impl Into]>>) -> Self { + self.components = Some(components.into()); self } super::button_and_select_menu_convenience_methods!(self.components); @@ -300,31 +281,35 @@ impl CreateInteractionResponseMessage { // [Autocomplete docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-autocomplete). #[must_use] #[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(transparent)] -pub struct AutocompleteChoice(CommandOptionChoice); -impl AutocompleteChoice { - pub fn new(name: impl Into, value: impl Into) -> Self { - Self(CommandOptionChoice { +pub struct AutocompleteChoice<'a> { + pub name: Cow<'a, str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub name_localizations: Option, Cow<'a, str>>>, + pub value: Value, +} + +impl<'a> AutocompleteChoice<'a> { + pub fn new(name: impl Into>, value: impl Into) -> Self { + Self { name: name.into(), name_localizations: None, value: value.into(), - }) + } } pub fn add_localized_name( mut self, - locale: impl Into, - localized_name: impl Into, + locale: impl Into>, + localized_name: impl Into>, ) -> Self { - self.0 - .name_localizations + self.name_localizations .get_or_insert_with(Default::default) .insert(locale.into(), localized_name.into()); self } } -impl> From for AutocompleteChoice { +impl<'a, S: Into>> From for AutocompleteChoice<'a> { fn from(value: S) -> Self { let value = value.into(); let name = value.clone(); @@ -335,11 +320,11 @@ impl> From for AutocompleteChoice { /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-autocomplete) #[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct CreateAutocompleteResponse { - choices: Vec, +pub struct CreateAutocompleteResponse<'a> { + choices: Cow<'a, [AutocompleteChoice<'a>]>, } -impl CreateAutocompleteResponse { +impl<'a> CreateAutocompleteResponse<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() @@ -350,8 +335,8 @@ impl CreateAutocompleteResponse { /// See the official docs on [`Application Command Option Choices`] for more information. /// /// [`Application Command Option Choices`]: https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-choice-structure - pub fn set_choices(mut self, choices: Vec) -> Self { - self.choices = choices; + pub fn set_choices(mut self, choices: impl Into]>>) -> Self { + self.choices = choices.into(); self } @@ -359,7 +344,7 @@ impl CreateAutocompleteResponse { /// /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 /// characters. Value must be between -2^53 and 2^53. - pub fn add_int_choice(self, name: impl Into, value: i64) -> Self { + pub fn add_int_choice(self, name: impl Into>, value: i64) -> Self { self.add_choice(AutocompleteChoice::new(name, value)) } @@ -367,7 +352,11 @@ impl CreateAutocompleteResponse { /// /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 /// characters. Value must be up to 100 characters. - pub fn add_string_choice(self, name: impl Into, value: impl Into) -> Self { + pub fn add_string_choice( + self, + name: impl Into>, + value: impl Into>, + ) -> Self { self.add_choice(AutocompleteChoice::new(name, value.into())) } @@ -375,50 +364,45 @@ impl CreateAutocompleteResponse { /// /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 /// characters. Value must be between -2^53 and 2^53. - pub fn add_number_choice(self, name: impl Into, value: f64) -> Self { + pub fn add_number_choice(self, name: impl Into>, value: f64) -> Self { self.add_choice(AutocompleteChoice::new(name, value)) } - fn add_choice(mut self, value: AutocompleteChoice) -> Self { - self.choices.push(value); + fn add_choice(mut self, value: AutocompleteChoice<'a>) -> Self { + self.choices.to_mut().push(value); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for CreateAutocompleteResponse { - type Context<'ctx> = (InteractionId, &'ctx str); - type Built = (); /// Creates a response to an autocomplete interaction. /// /// # Errors /// /// Returns an [`Error::Http`] if the API returns an error. - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().create_interaction_response(ctx.0, ctx.1, &self, Vec::new()).await + http: &Http, + interaction_id: InteractionId, + interaction_token: &str, + ) -> Result<()> { + http.create_interaction_response(interaction_id, interaction_token, &self, Vec::new()).await } } /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-modal). #[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct CreateModal { - components: Vec, - custom_id: String, - title: String, +pub struct CreateModal<'a> { + components: Cow<'a, [CreateActionRow<'a>]>, + custom_id: Cow<'a, str>, + title: Cow<'a, str>, } -impl CreateModal { +impl<'a> CreateModal<'a> { /// Creates a new modal. - pub fn new(custom_id: impl Into, title: impl Into) -> Self { + pub fn new(custom_id: impl Into>, title: impl Into>) -> Self { Self { - components: Vec::new(), + components: Cow::default(), custom_id: custom_id.into(), title: title.into(), } @@ -427,8 +411,8 @@ impl CreateModal { /// Sets the components of this message. /// /// Overwrites existing components. - pub fn components(mut self, components: Vec) -> Self { - self.components = components; + pub fn components(mut self, components: impl Into]>>) -> Self { + self.components = components.into(); self } } diff --git a/src/builder/create_interaction_response_followup.rs b/src/builder/create_interaction_response_followup.rs index 153313ce21d..5bef0096c16 100644 --- a/src/builder/create_interaction_response_followup.rs +++ b/src/builder/create_interaction_response_followup.rs @@ -1,5 +1,5 @@ -#[cfg(feature = "http")] -use super::{check_overflow, Builder}; +use std::borrow::Cow; + use super::{ CreateActionRow, CreateAllowedMentions, @@ -8,9 +8,7 @@ use super::{ EditAttachments, }; #[cfg(feature = "http")] -use crate::constants; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -18,50 +16,38 @@ use crate::model::prelude::*; /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#create-followup-message) #[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct CreateInteractionResponseFollowup { +pub struct CreateInteractionResponseFollowup<'a> { #[serde(skip_serializing_if = "Option::is_none")] - content: Option, + content: Option>, // [Omitting username: not supported in interaction followups] // [Omitting avatar_url: not supported in interaction followups] #[serde(skip_serializing_if = "Option::is_none")] tts: Option, - embeds: Vec, + embeds: Cow<'a, [CreateEmbed<'a>]>, #[serde(skip_serializing_if = "Option::is_none")] - allowed_mentions: Option, + allowed_mentions: Option>, #[serde(skip_serializing_if = "Option::is_none")] - components: Option>, + components: Option]>>, #[serde(skip_serializing_if = "Option::is_none")] flags: Option, - attachments: EditAttachments, + attachments: EditAttachments<'a>, } -impl CreateInteractionResponseFollowup { +impl<'a> CreateInteractionResponseFollowup<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() } #[cfg(feature = "http")] - fn check_length(&self) -> Result<()> { - if let Some(content) = &self.content { - check_overflow(content.chars().count(), constants::MESSAGE_CODE_LIMIT) - .map_err(|overflow| Error::Model(ModelError::MessageTooLong(overflow)))?; - } - - check_overflow(self.embeds.len(), constants::EMBED_MAX_COUNT) - .map_err(|_| Error::Model(ModelError::EmbedAmount))?; - for embed in &self.embeds { - embed.check_length()?; - } - - Ok(()) + fn check_length(&self) -> Result<(), ModelError> { + super::check_lengths(self.content.as_deref(), Some(&self.embeds), 0) } /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. - #[inline] - pub fn content(mut self, content: impl Into) -> Self { + pub fn content(mut self, content: impl Into>) -> Self { self.content = Some(content.into()); self } @@ -77,13 +63,13 @@ impl CreateInteractionResponseFollowup { } /// Appends a file to the message. - pub fn add_file(mut self, file: CreateAttachment) -> Self { + pub fn add_file(mut self, file: CreateAttachment<'a>) -> Self { self.attachments = self.attachments.add(file); self } /// Appends a list of files to the message. - pub fn add_files(mut self, files: impl IntoIterator) -> Self { + pub fn add_files(mut self, files: impl IntoIterator>) -> Self { for file in files { self.attachments = self.attachments.add(file); } @@ -94,20 +80,20 @@ impl CreateInteractionResponseFollowup { /// /// Calling this multiple times will overwrite the file list. To append files, call /// [`Self::add_file`] or [`Self::add_files`] instead. - pub fn files(mut self, files: impl IntoIterator) -> Self { + pub fn files(mut self, files: impl IntoIterator>) -> Self { self.attachments = EditAttachments::new(); self.add_files(files) } /// Adds an embed to the message. - pub fn add_embed(mut self, embed: CreateEmbed) -> Self { - self.embeds.push(embed); + pub fn add_embed(mut self, embed: CreateEmbed<'a>) -> Self { + self.embeds.to_mut().push(embed); self } /// Adds multiple embeds to the message. - pub fn add_embeds(mut self, embeds: Vec) -> Self { - self.embeds.extend(embeds); + pub fn add_embeds(mut self, embeds: impl IntoIterator>) -> Self { + self.embeds.to_mut().extend(embeds); self } @@ -115,7 +101,7 @@ impl CreateInteractionResponseFollowup { /// /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embed`] /// instead. - pub fn embed(self, embed: CreateEmbed) -> Self { + pub fn embed(self, embed: CreateEmbed<'a>) -> Self { self.embeds(vec![embed]) } @@ -123,13 +109,13 @@ impl CreateInteractionResponseFollowup { /// /// Calling this multiple times will overwrite the embed list. To append embeds, call /// [`Self::add_embeds`] instead. - pub fn embeds(mut self, embeds: Vec) -> Self { - self.embeds = embeds; + pub fn embeds(mut self, embeds: impl Into]>>) -> Self { + self.embeds = embeds.into(); self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions<'a>) -> Self { self.allowed_mentions = Some(allowed_mentions); self } @@ -155,18 +141,11 @@ impl CreateInteractionResponseFollowup { } /// Sets the components of this message. - pub fn components(mut self, components: Vec) -> Self { - self.components = Some(components); + pub fn components(mut self, components: impl Into]>>) -> Self { + self.components = Some(components.into()); self } super::button_and_select_menu_convenience_methods!(self.components); -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for CreateInteractionResponseFollowup { - type Context<'ctx> = (Option, &'ctx str); - type Built = Message; /// Creates or edits a followup response to the response sent. If a [`MessageId`] is provided, /// then the corresponding message will be edited. Otherwise, a new message will be created. @@ -179,23 +158,24 @@ impl Builder for CreateInteractionResponseFollowup { /// Returns [`Error::Model`] if the content is too long. May also return [`Error::Http`] if the /// API returns an error, or [`Error::Json`] if there is an error in deserializing the /// response. - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( mut self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { + http: &Http, + message_id: Option, + interaction_token: &str, + ) -> Result { self.check_length()?; let files = self.attachments.take_files(); - let http = cache_http.http(); if self.allowed_mentions.is_none() { self.allowed_mentions.clone_from(&http.default_allowed_mentions); } - match ctx.0 { - Some(id) => http.as_ref().edit_followup_message(ctx.1, id, &self, files).await, - None => http.as_ref().create_followup_message(ctx.1, &self, files).await, + match message_id { + Some(id) => http.edit_followup_message(interaction_token, id, &self, files).await, + None => http.create_followup_message(interaction_token, &self, files).await, } } } diff --git a/src/builder/create_invite.rs b/src/builder/create_invite.rs index 39caf60cf0e..3695da08782 100644 --- a/src/builder/create_invite.rs +++ b/src/builder/create_invite.rs @@ -1,7 +1,5 @@ #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -17,9 +15,10 @@ use crate::model::prelude::*; /// ```rust,no_run /// # use serenity::{prelude::*, model::prelude::*}; /// use serenity::builder::CreateInvite; -/// # async fn run(context: impl CacheHttp, channel: GuildChannel) -> Result<(), Box> { +/// use serenity::http::Http; +/// # async fn run(http: &Http, channel: GuildChannel) -> Result<(), Box> { /// let builder = CreateInvite::new().max_age(3600).max_uses(10); -/// let creation = channel.create_invite(&context, builder).await?; +/// let creation = channel.create_invite(http, builder).await?; /// # Ok(()) /// # } /// ``` @@ -66,11 +65,11 @@ impl<'a> CreateInvite<'a> { /// ```rust,no_run /// # use serenity::model::prelude::*; /// # use serenity::builder::CreateInvite; - /// # use serenity::http::CacheHttp; + /// # use serenity::http::Http; /// # - /// # async fn example(context: impl CacheHttp, channel: GuildChannel) -> Result<(), Box> { + /// # async fn example(http: &Http, channel: GuildChannel) -> Result<(), Box> { /// let builder = CreateInvite::new().max_age(3600); - /// let invite = channel.create_invite(context, builder).await?; + /// let invite = channel.create_invite(http, builder).await?; /// # Ok(()) /// # } /// ``` @@ -94,11 +93,11 @@ impl<'a> CreateInvite<'a> { /// ```rust,no_run /// # use serenity::model::prelude::*; /// # use serenity::builder::CreateInvite; - /// # use serenity::http::CacheHttp; + /// # use serenity::http::Http; /// # - /// # async fn example(context: impl CacheHttp, channel: GuildChannel) -> Result<(), Box> { + /// # async fn example(http: &Http, channel: GuildChannel) -> Result<(), Box> { /// let builder = CreateInvite::new().max_uses(5); - /// let invite = channel.create_invite(context, builder).await?; + /// let invite = channel.create_invite(http, builder).await?; /// # Ok(()) /// # } /// ``` @@ -118,11 +117,11 @@ impl<'a> CreateInvite<'a> { /// ```rust,no_run /// # use serenity::model::prelude::*; /// # use serenity::builder::CreateInvite; - /// # use serenity::http::CacheHttp; + /// # use serenity::http::Http; /// # - /// # async fn example(context: impl CacheHttp, channel: GuildChannel) -> Result<(), Box> { + /// # async fn example(http: &Http, channel: GuildChannel) -> Result<(), Box> { /// let builder = CreateInvite::new().temporary(true); - /// let invite = channel.create_invite(context, builder).await?; + /// let invite = channel.create_invite(http, builder).await?; /// # Ok(()) /// # } /// ``` @@ -142,11 +141,11 @@ impl<'a> CreateInvite<'a> { /// ```rust,no_run /// # use serenity::model::prelude::*; /// # use serenity::builder::CreateInvite; - /// # use serenity::http::CacheHttp; + /// # use serenity::http::Http; /// # - /// # async fn example(context: impl CacheHttp, channel: GuildChannel) -> Result<(), Box> { + /// # async fn example(http: &Http, channel: GuildChannel) -> Result<(), Box> { /// let builder = CreateInvite::new().unique(true); - /// let invite = channel.create_invite(context, builder).await?; + /// let invite = channel.create_invite(&http, builder).await?; /// # Ok(()) /// # } /// ``` @@ -194,13 +193,6 @@ impl<'a> CreateInvite<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for CreateInvite<'a> { - type Context<'ctx> = ChannelId; - type Built = RichInvite; /// Creates an invite for the given channel. /// @@ -208,22 +200,12 @@ impl<'a> Builder for CreateInvite<'a> { /// /// # Errors /// - /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is + /// given. /// /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - crate::utils::user_has_perms_cache(cache, ctx, Permissions::CREATE_INSTANT_INVITE)?; - } - } - - cache_http.http().create_invite(ctx, &self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, channel_id: ChannelId) -> Result { + http.create_invite(channel_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/create_message.rs b/src/builder/create_message.rs index 874ba3682ea..6047f019b11 100644 --- a/src/builder/create_message.rs +++ b/src/builder/create_message.rs @@ -1,6 +1,6 @@ +use std::borrow::Cow; + use super::create_poll::Ready; -#[cfg(feature = "http")] -use super::{check_overflow, Builder}; use super::{ CreateActionRow, CreateAllowedMentions, @@ -10,9 +10,7 @@ use super::{ EditAttachments, }; #[cfg(feature = "http")] -use crate::constants; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -53,61 +51,46 @@ use crate::model::prelude::*; /// [Discord docs](https://discord.com/developers/docs/resources/channel#create-message) #[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct CreateMessage { +pub struct CreateMessage<'a> { #[serde(skip_serializing_if = "Option::is_none")] - content: Option, + content: Option>, #[serde(skip_serializing_if = "Option::is_none")] nonce: Option, tts: bool, - embeds: Vec, + embeds: Cow<'a, [CreateEmbed<'a>]>, #[serde(skip_serializing_if = "Option::is_none")] - allowed_mentions: Option, + allowed_mentions: Option>, #[serde(skip_serializing_if = "Option::is_none")] message_reference: Option, #[serde(skip_serializing_if = "Option::is_none")] - components: Option>, - sticker_ids: Vec, + components: Option]>>, + sticker_ids: Cow<'a, [StickerId]>, #[serde(skip_serializing_if = "Option::is_none")] flags: Option, - pub(crate) attachments: EditAttachments, + pub(crate) attachments: EditAttachments<'a>, enforce_nonce: bool, #[serde(skip_serializing_if = "Option::is_none")] - poll: Option>, + poll: Option>, // The following fields are handled separately. #[serde(skip)] - reactions: Vec, + reactions: Cow<'a, [ReactionType]>, } -impl CreateMessage { +impl<'a> CreateMessage<'a> { pub fn new() -> Self { Self::default() } #[cfg(feature = "http")] - fn check_length(&self) -> Result<()> { - if let Some(content) = &self.content { - check_overflow(content.chars().count(), constants::MESSAGE_CODE_LIMIT) - .map_err(|overflow| Error::Model(ModelError::MessageTooLong(overflow)))?; - } - - check_overflow(self.embeds.len(), constants::EMBED_MAX_COUNT) - .map_err(|_| Error::Model(ModelError::EmbedAmount))?; - for embed in &self.embeds { - embed.check_length()?; - } - - check_overflow(self.sticker_ids.len(), constants::STICKER_MAX_COUNT) - .map_err(|_| Error::Model(ModelError::StickerAmount))?; - - Ok(()) + fn check_length(&self) -> Result<(), ModelError> { + super::check_lengths(self.content.as_deref(), Some(&self.embeds), self.sticker_ids.len()) } /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. - #[inline] - pub fn content(mut self, content: impl Into) -> Self { + pub fn content(mut self, content: impl Into>) -> Self { self.content = Some(content.into()); self } @@ -116,8 +99,8 @@ impl CreateMessage { /// /// **Note**: This will keep all existing embeds. Use [`Self::embed()`] to replace existing /// embeds. - pub fn add_embed(mut self, embed: CreateEmbed) -> Self { - self.embeds.push(embed); + pub fn add_embed(mut self, embed: CreateEmbed<'a>) -> Self { + self.embeds.to_mut().push(embed); self } @@ -125,8 +108,8 @@ impl CreateMessage { /// /// **Note**: This will keep all existing embeds. Use [`Self::embeds()`] to replace existing /// embeds. - pub fn add_embeds(mut self, embeds: Vec) -> Self { - self.embeds.extend(embeds); + pub fn add_embeds(mut self, embeds: impl IntoIterator>) -> Self { + self.embeds.to_mut().extend(embeds); self } @@ -134,7 +117,7 @@ impl CreateMessage { /// /// **Note**: This will replace all existing embeds. Use [`Self::add_embed()`] to keep existing /// embeds. - pub fn embed(self, embed: CreateEmbed) -> Self { + pub fn embed(self, embed: CreateEmbed<'a>) -> Self { self.embeds(vec![embed]) } @@ -142,8 +125,8 @@ impl CreateMessage { /// /// **Note**: This will replace all existing embeds. Use [`Self::add_embeds()`] to keep existing /// embeds. - pub fn embeds(mut self, embeds: Vec) -> Self { - self.embeds = embeds; + pub fn embeds(mut self, embeds: impl Into]>>) -> Self { + self.embeds = embeds.into(); self } @@ -158,12 +141,8 @@ impl CreateMessage { } /// Adds a list of reactions to create after the message's sent. - #[inline] - pub fn reactions>( - mut self, - reactions: impl IntoIterator, - ) -> Self { - self.reactions = reactions.into_iter().map(Into::into).collect(); + pub fn reactions(mut self, reactions: impl Into>) -> Self { + self.reactions = reactions.into(); self } @@ -172,7 +151,7 @@ impl CreateMessage { /// **Note**: Requires the [Attach Files] permission. /// /// [Attach Files]: Permissions::ATTACH_FILES - pub fn add_file(mut self, file: CreateAttachment) -> Self { + pub fn add_file(mut self, file: CreateAttachment<'a>) -> Self { self.attachments = self.attachments.add(file); self } @@ -182,7 +161,7 @@ impl CreateMessage { /// **Note**: Requires the [Attach Files] permission. /// /// [Attach Files]: Permissions::ATTACH_FILES - pub fn add_files(mut self, files: impl IntoIterator) -> Self { + pub fn add_files(mut self, files: impl IntoIterator>) -> Self { for file in files { self.attachments = self.attachments.add(file); } @@ -197,13 +176,13 @@ impl CreateMessage { /// **Note**: Requires the [Attach Files] permission. /// /// [Attach Files]: Permissions::ATTACH_FILES - pub fn files(mut self, files: impl IntoIterator) -> Self { + pub fn files(mut self, files: impl IntoIterator>) -> Self { self.attachments = EditAttachments::new(); self.add_files(files) } /// Set the allowed mentions for the message. - pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions<'a>) -> Self { self.allowed_mentions = Some(allowed_mentions); self } @@ -215,8 +194,8 @@ impl CreateMessage { } /// Sets the components of this message. - pub fn components(mut self, components: Vec) -> Self { - self.components = Some(components); + pub fn components(mut self, components: impl Into]>>) -> Self { + self.components = Some(components.into()); self } super::button_and_select_menu_convenience_methods!(self.components); @@ -231,8 +210,8 @@ impl CreateMessage { /// /// **Note**: This will replace all existing stickers. Use [`Self::add_sticker_id()`] to keep /// existing stickers. - pub fn sticker_id(self, sticker_id: impl Into) -> Self { - self.sticker_ids(vec![sticker_id.into()]) + pub fn sticker_id(self, sticker_id: StickerId) -> Self { + self.sticker_ids(vec![sticker_id]) } /// Sets a list of sticker IDs to include in the message. @@ -241,11 +220,8 @@ impl CreateMessage { /// /// **Note**: This will replace all existing stickers. Use [`Self::add_sticker_id()`] or /// [`Self::add_sticker_ids()`] to keep existing stickers. - pub fn sticker_ids>( - mut self, - sticker_ids: impl IntoIterator, - ) -> Self { - self.sticker_ids = sticker_ids.into_iter().map(Into::into).collect(); + pub fn sticker_ids(mut self, sticker_ids: impl Into>) -> Self { + self.sticker_ids = sticker_ids.into(); self } @@ -255,8 +231,8 @@ impl CreateMessage { /// /// **Note**: This will keep all existing stickers. Use [`Self::sticker_id()`] to replace /// existing sticker. - pub fn add_sticker_id(mut self, sticker_id: impl Into) -> Self { - self.sticker_ids.push(sticker_id.into()); + pub fn add_sticker_id(mut self, sticker_id: StickerId) -> Self { + self.sticker_ids.to_mut().push(sticker_id); self } @@ -266,13 +242,8 @@ impl CreateMessage { /// /// **Note**: This will keep all existing stickers. Use [`Self::sticker_ids()`] to replace /// existing stickers. - pub fn add_sticker_ids>( - mut self, - sticker_ids: impl IntoIterator, - ) -> Self { - for sticker_id in sticker_ids { - self = self.add_sticker_id(sticker_id); - } + pub fn add_sticker_ids(mut self, sticker_ids: impl IntoIterator) -> Self { + self.sticker_ids.to_mut().extend(sticker_ids); self } @@ -294,17 +265,10 @@ impl CreateMessage { } /// Sets the [`Poll`] for this message. - pub fn poll(mut self, poll: CreatePoll) -> Self { + pub fn poll(mut self, poll: CreatePoll<'a, Ready>) -> Self { self.poll = Some(poll); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for CreateMessage { - type Context<'ctx> = (ChannelId, Option); - type Built = Message; /// Send a message to the channel. /// @@ -316,43 +280,30 @@ impl Builder for CreateMessage { /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the message contents are over the above limits. + /// Returns a [`ModelError::TooLarge`] if the message contents are over the above limits. /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Send Messages]: Permissions::SEND_MESSAGES /// [Attach Files]: Permissions::ATTACH_FILES - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( mut self, - cache_http: impl CacheHttp, - (channel_id, guild_id): Self::Context<'_>, - ) -> Result { - #[cfg(feature = "cache")] - { - let mut req = Permissions::SEND_MESSAGES; - if !self.attachments.is_empty() { - req |= Permissions::ATTACH_FILES; - } - if let Some(cache) = cache_http.cache() { - crate::utils::user_has_perms_cache(cache, channel_id, req)?; - } - } - + http: &Http, + channel_id: ChannelId, + guild_id: Option, + ) -> Result { self.check_length()?; - let http = cache_http.http(); - let files = self.attachments.take_files(); if self.allowed_mentions.is_none() { self.allowed_mentions.clone_from(&http.default_allowed_mentions); } - #[cfg_attr(not(feature = "cache"), allow(unused_mut))] let mut message = http.send_message(channel_id, files, &self).await?; - for reaction in self.reactions { - http.create_reaction(channel_id, message.id, &reaction).await?; + for reaction in self.reactions.iter() { + http.create_reaction(channel_id, message.id, reaction).await?; } // HTTP sent Messages don't have guild_id set, so we fill it in ourselves by best effort diff --git a/src/builder/create_poll.rs b/src/builder/create_poll.rs index fc9ce629143..78f32a76640 100644 --- a/src/builder/create_poll.rs +++ b/src/builder/create_poll.rs @@ -1,4 +1,6 @@ -use crate::model::channel::{PollLayoutType, PollMedia, PollMediaEmoji}; +use std::borrow::Cow; + +use crate::model::channel::{PollLayoutType, PollMediaEmoji}; #[derive(serde::Serialize, Clone, Debug)] pub struct NeedsQuestion; @@ -24,15 +26,15 @@ use sealed::*; /// "Only text is supported." #[derive(serde::Serialize, Clone, Debug)] -struct CreatePollMedia { - text: String, +struct CreatePollMedia<'a> { + text: Cow<'a, str>, } #[derive(serde::Serialize, Clone, Debug)] #[must_use = "Builders do nothing unless built"] -pub struct CreatePoll { - question: CreatePollMedia, - answers: Vec, +pub struct CreatePoll<'a, Stage: Sealed> { + question: CreatePollMedia<'a>, + answers: Cow<'a, [CreatePollAnswer<'a>]>, duration: u8, allow_multiselect: bool, layout_type: Option, @@ -41,16 +43,16 @@ pub struct CreatePoll { _stage: Stage, } -impl Default for CreatePoll { +impl Default for CreatePoll<'_, NeedsQuestion> { /// See the documentation of [`Self::new`]. fn default() -> Self { // Producing dummy values is okay as we must transition through all `Stage`s before firing, // which fills in the values with real values. Self { question: CreatePollMedia { - text: String::default(), + text: Cow::default(), }, - answers: Vec::default(), + answers: Cow::default(), duration: u8::default(), allow_multiselect: false, layout_type: None, @@ -60,7 +62,7 @@ impl Default for CreatePoll { } } -impl CreatePoll { +impl<'a> CreatePoll<'a, NeedsQuestion> { /// Creates a builder for creating a Poll. /// /// This must be transitioned through in order, to provide all required fields. @@ -84,7 +86,7 @@ impl CreatePoll { } /// Sets the question to be polled. - pub fn question(self, text: impl Into) -> CreatePoll { + pub fn question(self, text: impl Into>) -> CreatePoll<'a, NeedsAnswers> { CreatePoll { question: CreatePollMedia { text: text.into(), @@ -98,12 +100,15 @@ impl CreatePoll { } } -impl CreatePoll { +impl<'a> CreatePoll<'a, NeedsAnswers> { /// Sets the answers that can be picked from. - pub fn answers(self, answers: Vec) -> CreatePoll { + pub fn answers( + self, + answers: impl Into]>>, + ) -> CreatePoll<'a, NeedsDuration> { CreatePoll { question: self.question, - answers, + answers: answers.into(), duration: self.duration, allow_multiselect: self.allow_multiselect, layout_type: self.layout_type, @@ -112,11 +117,11 @@ impl CreatePoll { } } -impl CreatePoll { +impl<'a> CreatePoll<'a, NeedsDuration> { /// Sets the duration for the Poll to run for. /// /// This must be less than a week, and will be rounded to hours towards zero. - pub fn duration(self, duration: std::time::Duration) -> CreatePoll { + pub fn duration(self, duration: std::time::Duration) -> CreatePoll<'a, Ready> { let hours = duration.as_secs() / 3600; CreatePoll { @@ -130,7 +135,7 @@ impl CreatePoll { } } -impl CreatePoll { +impl CreatePoll<'_, Stage> { /// Sets the layout type for the Poll to take. /// /// This is currently only ever [`PollLayoutType::Default`], and is optional. @@ -146,13 +151,19 @@ impl CreatePoll { } } +#[derive(serde::Serialize, Clone, Debug, Default)] +struct CreatePollAnswerMedia<'a> { + text: Option>, + emoji: Option, +} + #[derive(serde::Serialize, Clone, Debug, Default)] #[must_use = "Builders do nothing unless built"] -pub struct CreatePollAnswer { - poll_media: PollMedia, +pub struct CreatePollAnswer<'a> { + poll_media: CreatePollAnswerMedia<'a>, } -impl CreatePollAnswer { +impl<'a> CreatePollAnswer<'a> { /// Creates a builder for a Poll answer. /// /// [`Self::text`] or [`Self::emoji`] must be provided. @@ -160,7 +171,7 @@ impl CreatePollAnswer { Self::default() } - pub fn text(mut self, text: impl Into) -> Self { + pub fn text(mut self, text: impl Into>) -> Self { self.poll_media.text = Some(text.into()); self } diff --git a/src/builder/create_scheduled_event.rs b/src/builder/create_scheduled_event.rs index 4b88aa52e10..2ee1c2fc9ea 100644 --- a/src/builder/create_scheduled_event.rs +++ b/src/builder/create_scheduled_event.rs @@ -1,8 +1,8 @@ -#[cfg(feature = "http")] -use super::Builder; +use std::borrow::Cow; + use super::CreateAttachment; #[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -14,14 +14,14 @@ pub struct CreateScheduledEvent<'a> { #[serde(skip_serializing_if = "Option::is_none")] channel_id: Option, #[serde(skip_serializing_if = "Option::is_none")] - entity_metadata: Option, - name: String, + entity_metadata: Option>, + name: Cow<'a, str>, privacy_level: ScheduledEventPrivacyLevel, - scheduled_start_time: String, + scheduled_start_time: Timestamp, #[serde(skip_serializing_if = "Option::is_none")] - scheduled_end_time: Option, + scheduled_end_time: Option, #[serde(skip_serializing_if = "Option::is_none")] - description: Option, + description: Option>, entity_type: ScheduledEventType, #[serde(skip_serializing_if = "Option::is_none")] image: Option, @@ -35,13 +35,13 @@ impl<'a> CreateScheduledEvent<'a> { /// empty. pub fn new( kind: ScheduledEventType, - name: impl Into, + name: impl Into>, scheduled_start_time: impl Into, ) -> Self { Self { name: name.into(), entity_type: kind, - scheduled_start_time: scheduled_start_time.into().to_string(), + scheduled_start_time: scheduled_start_time.into(), image: None, channel_id: None, @@ -60,19 +60,19 @@ impl<'a> CreateScheduledEvent<'a> { /// Sets the channel id of the scheduled event. Required if [`Self::kind`] is /// [`ScheduledEventType::StageInstance`] or [`ScheduledEventType::Voice`]. - pub fn channel_id>(mut self, channel_id: C) -> Self { - self.channel_id = Some(channel_id.into()); + pub fn channel_id(mut self, channel_id: ChannelId) -> Self { + self.channel_id = Some(channel_id); self } /// Sets the name of the scheduled event, replacing the current value as set in [`Self::new`]. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = name.into(); self } /// Sets the description of the scheduled event. - pub fn description(mut self, description: impl Into) -> Self { + pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } @@ -80,14 +80,14 @@ impl<'a> CreateScheduledEvent<'a> { /// Sets the start time of the scheduled event, replacing the current value as set in /// [`Self::new`]. pub fn start_time(mut self, timestamp: impl Into) -> Self { - self.scheduled_start_time = timestamp.into().to_string(); + self.scheduled_start_time = timestamp.into(); self } /// Sets the end time of the scheduled event. Required if [`Self::kind`] is /// [`ScheduledEventType::External`]. pub fn end_time(mut self, timestamp: impl Into) -> Self { - self.scheduled_end_time = Some(timestamp.into().to_string()); + self.scheduled_end_time = Some(timestamp.into()); self } @@ -102,15 +102,15 @@ impl<'a> CreateScheduledEvent<'a> { /// [`Self::kind`] is [`ScheduledEventType::External`]. /// /// [`External`]: ScheduledEventType::External - pub fn location(mut self, location: impl Into) -> Self { - self.entity_metadata = Some(ScheduledEventMetadata { + pub fn location(mut self, location: impl Into>) -> Self { + self.entity_metadata = Some(CreateScheduledEventMetadata { location: Some(location.into()), }); self } /// Sets the cover image for the scheduled event. - pub fn image(mut self, image: &CreateAttachment) -> Self { + pub fn image(mut self, image: &CreateAttachment<'_>) -> Self { self.image = Some(image.to_base64()); self } @@ -120,13 +120,6 @@ impl<'a> CreateScheduledEvent<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for CreateScheduledEvent<'a> { - type Context<'ctx> = GuildId; - type Built = ScheduledEvent; /// Creates a new scheduled event in the guild with the data set, if any. /// @@ -134,18 +127,16 @@ impl<'a> Builder for CreateScheduledEvent<'a> { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Events]: Permissions::CREATE_EVENTS - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - #[cfg(feature = "cache")] - crate::utils::user_has_guild_perms(&cache_http, ctx, Permissions::CREATE_EVENTS)?; - - cache_http.http().create_scheduled_event(ctx, &self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, channel_id: GuildId) -> Result { + http.create_scheduled_event(channel_id, &self, self.audit_log_reason).await } } + +#[derive(Clone, Debug, Default, serde::Serialize)] +pub(crate) struct CreateScheduledEventMetadata<'a> { + pub(crate) location: Option>, +} diff --git a/src/builder/create_stage_instance.rs b/src/builder/create_stage_instance.rs index 9b3292d2c46..8dc589f76a6 100644 --- a/src/builder/create_stage_instance.rs +++ b/src/builder/create_stage_instance.rs @@ -1,7 +1,7 @@ +use std::borrow::Cow; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -13,7 +13,7 @@ use crate::model::prelude::*; #[must_use] pub struct CreateStageInstance<'a> { channel_id: Option, // required field, filled in Builder impl - topic: String, + topic: Cow<'a, str>, privacy_level: StageInstancePrivacyLevel, #[serde(skip_serializing_if = "Option::is_none")] send_start_notification: Option, @@ -24,7 +24,7 @@ pub struct CreateStageInstance<'a> { impl<'a> CreateStageInstance<'a> { /// Creates a builder with the provided topic. - pub fn new(topic: impl Into) -> Self { + pub fn new(topic: impl Into>) -> Self { Self { channel_id: None, topic: topic.into(), @@ -36,7 +36,7 @@ impl<'a> CreateStageInstance<'a> { /// Sets the topic of the stage channel instance, replacing the current value as set in /// [`Self::new`]. - pub fn topic(mut self, topic: impl Into) -> Self { + pub fn topic(mut self, topic: impl Into>) -> Self { self.topic = topic.into(); self } @@ -52,25 +52,15 @@ impl<'a> CreateStageInstance<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for CreateStageInstance<'a> { - type Context<'ctx> = ChannelId; - type Built = StageInstance; /// Creates the stage instance. /// /// # Errors /// /// Returns [`Error::Http`] if there is already a stage instance currently. - async fn execute( - mut self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - self.channel_id = Some(ctx); - cache_http.http().create_stage_instance(&self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(mut self, http: &Http, channel_id: ChannelId) -> Result { + self.channel_id = Some(channel_id); + http.create_stage_instance(&self, self.audit_log_reason).await } } diff --git a/src/builder/create_sticker.rs b/src/builder/create_sticker.rs index 068fac365fa..50bffc67bca 100644 --- a/src/builder/create_sticker.rs +++ b/src/builder/create_sticker.rs @@ -1,8 +1,8 @@ -#[cfg(feature = "http")] -use super::Builder; +use std::borrow::Cow; + use super::CreateAttachment; #[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; #[cfg(feature = "http")] @@ -14,20 +14,20 @@ use crate::model::prelude::*; #[derive(Clone, Debug)] #[must_use] pub struct CreateSticker<'a> { - name: String, - description: String, - tags: String, - file: CreateAttachment, + name: Cow<'static, str>, + description: Cow<'static, str>, + tags: Cow<'static, str>, + file: CreateAttachment<'a>, audit_log_reason: Option<&'a str>, } impl<'a> CreateSticker<'a> { /// Creates a new builder with the given data. All of this builder's fields are required. - pub fn new(name: impl Into, file: CreateAttachment) -> Self { + pub fn new(name: impl Into>, file: CreateAttachment<'a>) -> Self { Self { name: name.into(), - tags: String::new(), - description: String::new(), + tags: Cow::default(), + description: Cow::default(), file, audit_log_reason: None, } @@ -36,7 +36,7 @@ impl<'a> CreateSticker<'a> { /// Set the name of the sticker, replacing the current value as set in [`Self::new`]. /// /// **Note**: Must be between 2 and 30 characters long. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = name.into(); self } @@ -44,7 +44,7 @@ impl<'a> CreateSticker<'a> { /// Set the description of the sticker. /// /// **Note**: Must be empty or 2-100 characters. - pub fn description(mut self, description: impl Into) -> Self { + pub fn description(mut self, description: impl Into>) -> Self { self.description = description.into(); self } @@ -52,7 +52,7 @@ impl<'a> CreateSticker<'a> { /// The Discord name of a unicode emoji representing the sticker's expression. /// /// **Note**: Max 200 characters long. - pub fn tags(mut self, tags: impl Into) -> Self { + pub fn tags(mut self, tags: impl Into>) -> Self { self.tags = tags.into(); self } @@ -60,7 +60,7 @@ impl<'a> CreateSticker<'a> { /// Set the sticker file. Replaces the current value as set in [`Self::new`]. /// /// **Note**: Must be a PNG, APNG, or Lottie JSON file, max 500 KB. - pub fn file(mut self, file: CreateAttachment) -> Self { + pub fn file(mut self, file: CreateAttachment<'a>) -> Self { self.file = file; self } @@ -70,13 +70,6 @@ impl<'a> CreateSticker<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for CreateSticker<'a> { - type Context<'ctx> = GuildId; - type Built = Sticker; /// Creates a new sticker in the guild with the data set, if any. /// @@ -84,23 +77,17 @@ impl<'a> Builder for CreateSticker<'a> { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - #[cfg(feature = "cache")] - crate::utils::user_has_guild_perms( - &cache_http, - ctx, - Permissions::CREATE_GUILD_EXPRESSIONS, - )?; + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, guild_id: GuildId) -> Result { + let map = vec![ + ("name".into(), self.name), + ("tags".into(), self.tags), + ("description".into(), self.description), + ]; - let map = [("name", self.name), ("tags", self.tags), ("description", self.description)]; - cache_http.http().create_sticker(ctx, map, self.file, self.audit_log_reason).await + http.create_sticker(guild_id, map, self.file, self.audit_log_reason).await } } diff --git a/src/builder/create_thread.rs b/src/builder/create_thread.rs index 4358d78c9f9..04b6711ca40 100644 --- a/src/builder/create_thread.rs +++ b/src/builder/create_thread.rs @@ -1,7 +1,9 @@ +use std::borrow::Cow; + +use nonmax::NonMaxU16; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -12,7 +14,7 @@ use crate::model::prelude::*; #[derive(Clone, Debug, Serialize)] #[must_use] pub struct CreateThread<'a> { - name: String, + name: Cow<'a, str>, #[serde(skip_serializing_if = "Option::is_none")] auto_archive_duration: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -21,7 +23,7 @@ pub struct CreateThread<'a> { #[serde(skip_serializing_if = "Option::is_none")] invitable: Option, #[serde(skip_serializing_if = "Option::is_none")] - rate_limit_per_user: Option, + rate_limit_per_user: Option, #[serde(skip)] audit_log_reason: Option<&'a str>, @@ -29,7 +31,7 @@ pub struct CreateThread<'a> { impl<'a> CreateThread<'a> { /// Creates a builder with the given thread name, leaving all other fields empty. - pub fn new(name: impl Into) -> Self { + pub fn new(name: impl Into>) -> Self { Self { name: name.into(), auto_archive_duration: None, @@ -43,7 +45,7 @@ impl<'a> CreateThread<'a> { /// The name of the thread. Replaces the current value as set in [`Self::new`]. /// /// **Note**: Must be between 2 and 100 characters long. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = name.into(); self } @@ -64,7 +66,7 @@ impl<'a> CreateThread<'a> { /// [`MANAGE_MESSAGES`]: crate::model::permissions::Permissions::MANAGE_MESSAGES /// [`MANAGE_CHANNELS`]: crate::model::permissions::Permissions::MANAGE_CHANNELS #[doc(alias = "slowmode")] - pub fn rate_limit_per_user(mut self, seconds: u16) -> Self { + pub fn rate_limit_per_user(mut self, seconds: NonMaxU16) -> Self { self.rate_limit_per_user = Some(seconds); self } @@ -92,13 +94,6 @@ impl<'a> CreateThread<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for CreateThread<'a> { - type Context<'ctx> = (ChannelId, Option); - type Built = GuildChannel; /// Creates a thread, either private or public. Public threads require a message to connect the /// thread to. @@ -106,17 +101,18 @@ impl<'a> Builder for CreateThread<'a> { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, + http: &Http, + channel_id: ChannelId, + message_id: Option, ) -> Result { - let http = cache_http.http(); - match ctx.1 { + match message_id { Some(id) => { - http.create_thread_from_message(ctx.0, id, &self, self.audit_log_reason).await + http.create_thread_from_message(channel_id, id, &self, self.audit_log_reason).await }, - None => http.create_thread(ctx.0, &self, self.audit_log_reason).await, + None => http.create_thread(channel_id, &self, self.audit_log_reason).await, } } } diff --git a/src/builder/create_webhook.rs b/src/builder/create_webhook.rs index 40efee3c009..3b267b8a494 100644 --- a/src/builder/create_webhook.rs +++ b/src/builder/create_webhook.rs @@ -1,8 +1,8 @@ -#[cfg(feature = "http")] -use super::Builder; +use std::borrow::Cow; + use super::CreateAttachment; #[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; #[cfg(feature = "http")] @@ -12,7 +12,7 @@ use crate::model::prelude::*; #[derive(Clone, Debug, Serialize)] #[must_use] pub struct CreateWebhook<'a> { - name: String, + name: Cow<'a, str>, #[serde(skip_serializing_if = "Option::is_none")] avatar: Option, @@ -22,7 +22,7 @@ pub struct CreateWebhook<'a> { impl<'a> CreateWebhook<'a> { /// Creates a new builder with the given webhook name, leaving all other fields empty. - pub fn new(name: impl Into) -> Self { + pub fn new(name: impl Into>) -> Self { Self { name: name.into(), avatar: None, @@ -33,13 +33,13 @@ impl<'a> CreateWebhook<'a> { /// Set the webhook's name, replacing the current value as set in [`Self::new`]. /// /// This must be between 1-80 characters. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = name.into(); self } /// Set the webhook's default avatar. - pub fn avatar(mut self, avatar: &CreateAttachment) -> Self { + pub fn avatar(mut self, avatar: &CreateAttachment<'_>) -> Self { self.avatar = Some(avatar.to_base64()); self } @@ -49,37 +49,24 @@ impl<'a> CreateWebhook<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for CreateWebhook<'a> { - type Context<'ctx> = ChannelId; - type Built = Webhook; /// Creates the webhook. /// /// # Errors /// - /// If the provided name is less than 2 characters, returns [`ModelError::NameTooShort`]. If it - /// is more than 100 characters, returns [`ModelError::NameTooLong`]. + /// If the provided name is less than 2 characters, returns [`ModelError::TooSmall`]. If it + /// is more than 100 characters, returns [`ModelError::TooLarge`]. /// /// Returns a [`Error::Http`] if the current user lacks permission, or if invalid data is /// given. /// /// [`Text`]: ChannelType::Text /// [`News`]: ChannelType::News - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - if self.name.len() < 2 { - return Err(Error::Model(ModelError::NameTooShort)); - } else if self.name.len() > 100 { - return Err(Error::Model(ModelError::NameTooLong)); - } + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, channel_id: ChannelId) -> Result { + crate::model::error::Minimum::WebhookName.check_underflow(self.name.chars().count())?; + crate::model::error::Maximum::WebhookName.check_overflow(self.name.chars().count())?; - cache_http.http().create_webhook(ctx, &self, self.audit_log_reason).await + http.create_webhook(channel_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/edit_automod_rule.rs b/src/builder/edit_automod_rule.rs index 338e03d0c44..70ca302fd9e 100644 --- a/src/builder/edit_automod_rule.rs +++ b/src/builder/edit_automod_rule.rs @@ -1,7 +1,7 @@ +use std::borrow::Cow; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::guild::automod::EventType; @@ -18,18 +18,18 @@ use crate::model::prelude::*; #[must_use] pub struct EditAutoModRule<'a> { #[serde(skip_serializing_if = "Option::is_none")] - name: Option, + name: Option>, event_type: EventType, #[serde(flatten, skip_serializing_if = "Option::is_none")] trigger: Option, #[serde(skip_serializing_if = "Option::is_none")] - actions: Option>, + actions: Option>, #[serde(skip_serializing_if = "Option::is_none")] enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] - exempt_roles: Option>, + exempt_roles: Option>, #[serde(skip_serializing_if = "Option::is_none")] - exempt_channels: Option>, + exempt_channels: Option>, #[serde(skip)] audit_log_reason: Option<&'a str>, @@ -42,7 +42,7 @@ impl<'a> EditAutoModRule<'a> { } /// The display name of the rule. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = Some(name.into()); self } @@ -62,8 +62,8 @@ impl<'a> EditAutoModRule<'a> { } /// Set the actions which will execute when the rule is triggered. - pub fn actions(mut self, actions: Vec) -> Self { - self.actions = Some(actions); + pub fn actions(mut self, actions: impl Into>) -> Self { + self.actions = Some(actions.into()); self } @@ -76,19 +76,16 @@ impl<'a> EditAutoModRule<'a> { /// Set roles that should not be affected by the rule. /// /// Maximum of 20. - pub fn exempt_roles(mut self, roles: impl IntoIterator>) -> Self { - self.exempt_roles = Some(roles.into_iter().map(Into::into).collect()); + pub fn exempt_roles(mut self, roles: impl Into>) -> Self { + self.exempt_roles = Some(roles.into()); self } /// Set channels that should not be affected by the rule. /// /// Maximum of 50. - pub fn exempt_channels( - mut self, - channels: impl IntoIterator>, - ) -> Self { - self.exempt_channels = Some(channels.into_iter().map(Into::into).collect()); + pub fn exempt_channels(mut self, channels: impl Into>) -> Self { + self.exempt_channels = Some(channels.into()); self } @@ -97,28 +94,6 @@ impl<'a> EditAutoModRule<'a> { self.audit_log_reason = Some(reason); self } -} - -impl<'a> Default for EditAutoModRule<'a> { - fn default() -> Self { - Self { - name: None, - trigger: None, - actions: None, - enabled: None, - exempt_roles: None, - exempt_channels: None, - event_type: EventType::MessageSend, - audit_log_reason: None, - } - } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditAutoModRule<'a> { - type Context<'ctx> = (GuildId, Option); - type Built = Rule; /// Creates or edits an AutoMod [`Rule`] in a guild. Providing a [`RuleId`] will edit that /// corresponding rule, otherwise a new rule will be created. @@ -130,17 +105,33 @@ impl<'a> Builder for EditAutoModRule<'a> { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - let http = cache_http.http(); - match ctx.1 { - Some(id) => http.edit_automod_rule(ctx.0, id, &self, self.audit_log_reason).await, + http: &Http, + guild_id: GuildId, + rule_id: Option, + ) -> Result { + match rule_id { + Some(id) => http.edit_automod_rule(guild_id, id, &self, self.audit_log_reason).await, // Automod Rule creation has required fields, whereas modifying a rule does not. // TODO: Enforce these fields (maybe with a separate CreateAutoModRule builder). - None => http.create_automod_rule(ctx.0, &self, self.audit_log_reason).await, + None => http.create_automod_rule(guild_id, &self, self.audit_log_reason).await, + } + } +} + +impl<'a> Default for EditAutoModRule<'a> { + fn default() -> Self { + Self { + name: None, + trigger: None, + actions: None, + enabled: None, + exempt_roles: None, + exempt_channels: None, + event_type: EventType::MessageSend, + audit_log_reason: None, } } } diff --git a/src/builder/edit_channel.rs b/src/builder/edit_channel.rs index d5b6572838c..c35012eff80 100644 --- a/src/builder/edit_channel.rs +++ b/src/builder/edit_channel.rs @@ -1,8 +1,10 @@ -#[cfg(feature = "http")] -use super::Builder; +use std::borrow::Cow; + +use nonmax::NonMaxU16; + use super::CreateForumTag; #[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -34,28 +36,28 @@ use crate::model::prelude::*; #[must_use] pub struct EditChannel<'a> { #[serde(skip_serializing_if = "Option::is_none")] - name: Option, + name: Option>, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "type")] kind: Option, #[serde(skip_serializing_if = "Option::is_none")] position: Option, #[serde(skip_serializing_if = "Option::is_none")] - topic: Option, + topic: Option>, #[serde(skip_serializing_if = "Option::is_none")] nsfw: Option, #[serde(skip_serializing_if = "Option::is_none")] - rate_limit_per_user: Option, + rate_limit_per_user: Option, #[serde(skip_serializing_if = "Option::is_none")] bitrate: Option, #[serde(skip_serializing_if = "Option::is_none")] - user_limit: Option, + user_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] - permission_overwrites: Option>, + permission_overwrites: Option>, #[serde(skip_serializing_if = "Option::is_none")] parent_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] - rtc_region: Option>, + rtc_region: Option>>, #[serde(skip_serializing_if = "Option::is_none")] video_quality_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -63,17 +65,17 @@ pub struct EditChannel<'a> { #[serde(skip_serializing_if = "Option::is_none")] flags: Option, #[serde(skip_serializing_if = "Option::is_none")] - available_tags: Option>, + available_tags: Option]>>, #[serde(skip_serializing_if = "Option::is_none")] default_reaction_emoji: Option>, #[serde(skip_serializing_if = "Option::is_none")] - default_thread_rate_limit_per_user: Option, + default_thread_rate_limit_per_user: Option, #[serde(skip_serializing_if = "Option::is_none")] default_sort_order: Option, #[serde(skip_serializing_if = "Option::is_none")] default_forum_layout: Option, #[serde(skip_serializing_if = "Option::is_none")] - status: Option, + status: Option>, #[serde(skip)] audit_log_reason: Option<&'a str>, @@ -110,7 +112,7 @@ impl<'a> EditChannel<'a> { /// This is for [voice] channels only. /// /// [voice]: ChannelType::Voice - pub fn voice_region(mut self, id: Option) -> Self { + pub fn voice_region(mut self, id: Option>) -> Self { self.rtc_region = Some(id); self } @@ -118,7 +120,7 @@ impl<'a> EditChannel<'a> { /// The name of the channel. /// /// Must be between 2 and 100 characters long. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = Some(name.into()); self } @@ -136,7 +138,7 @@ impl<'a> EditChannel<'a> { /// This is for [text] channels only. /// /// [text]: ChannelType::Text - pub fn topic(mut self, topic: impl Into) -> Self { + pub fn topic(mut self, topic: impl Into>) -> Self { self.topic = Some(topic.into()); self } @@ -153,10 +155,11 @@ impl<'a> EditChannel<'a> { /// The number of users that may be in the channel simultaneously. /// - /// This is for [voice] channels only. + /// This is for [voice] and [stage] channels only. /// /// [voice]: ChannelType::Voice - pub fn user_limit(mut self, user_limit: u32) -> Self { + /// [stage]: ChannelType::Stage + pub fn user_limit(mut self, user_limit: NonMaxU16) -> Self { self.user_limit = Some(user_limit); self } @@ -167,9 +170,8 @@ impl<'a> EditChannel<'a> { /// /// [text]: ChannelType::Text /// [voice]: ChannelType::Voice - #[inline] - pub fn category>>(mut self, category: C) -> Self { - self.parent_id = Some(category.into()); + pub fn category(mut self, category: Option) -> Self { + self.parent_id = Some(category); self } @@ -183,7 +185,7 @@ impl<'a> EditChannel<'a> { /// [`MANAGE_MESSAGES`]: Permissions::MANAGE_MESSAGES /// [`MANAGE_CHANNELS`]: Permissions::MANAGE_CHANNELS #[doc(alias = "slowmode")] - pub fn rate_limit_per_user(mut self, seconds: u16) -> Self { + pub fn rate_limit_per_user(mut self, seconds: NonMaxU16) -> Self { self.rate_limit_per_user = Some(seconds); self } @@ -215,20 +217,18 @@ impl<'a> EditChannel<'a> { /// }]; /// /// let builder = EditChannel::new().name("my_edited_cool_channel").permissions(permissions); - /// channel.edit(http, builder).await?; + /// channel.edit(&http, builder).await?; /// # Ok(()) /// # } /// ``` - pub fn permissions(mut self, perms: impl IntoIterator) -> Self { - let overwrites = perms.into_iter().map(Into::into).collect::>(); - - self.permission_overwrites = Some(overwrites); + pub fn permissions(mut self, overwrites: impl Into>) -> Self { + self.permission_overwrites = Some(overwrites.into()); self } /// If this is a forum channel, sets the tags that can be assigned to forum posts. - pub fn available_tags(mut self, tags: impl IntoIterator) -> Self { - self.available_tags = Some(tags.into_iter().collect()); + pub fn available_tags(mut self, tags: impl Into]>>) -> Self { + self.available_tags = Some(tags.into()); self } @@ -272,7 +272,7 @@ impl<'a> EditChannel<'a> { /// copied to the thread at creation time and does not live update. pub fn default_thread_rate_limit_per_user( mut self, - default_thread_rate_limit_per_user: u16, + default_thread_rate_limit_per_user: NonMaxU16, ) -> Self { self.default_thread_rate_limit_per_user = Some(default_thread_rate_limit_per_user); self @@ -289,13 +289,6 @@ impl<'a> EditChannel<'a> { self.default_forum_layout = Some(default_forum_layout); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditChannel<'a> { - type Context<'ctx> = ChannelId; - type Built = GuildChannel; /// Edits the channel's settings. /// @@ -304,26 +297,12 @@ impl<'a> Builder for EditChannel<'a> { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS /// [Manage Roles]: Permissions::MANAGE_ROLES - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - crate::utils::user_has_perms_cache(cache, ctx, Permissions::MANAGE_CHANNELS)?; - if self.permission_overwrites.is_some() { - crate::utils::user_has_perms_cache(cache, ctx, Permissions::MANAGE_ROLES)?; - } - } - } - - cache_http.http().edit_channel(ctx, &self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, channel_id: ChannelId) -> Result { + http.edit_channel(channel_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/edit_guild.rs b/src/builder/edit_guild.rs index c02fbb86011..abfe4044418 100644 --- a/src/builder/edit_guild.rs +++ b/src/builder/edit_guild.rs @@ -1,8 +1,8 @@ -#[cfg(feature = "http")] -use super::Builder; +use std::borrow::Cow; + use super::CreateAttachment; #[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -14,7 +14,7 @@ use crate::model::prelude::*; #[must_use] pub struct EditGuild<'a> { #[serde(skip_serializing_if = "Option::is_none")] - name: Option, + name: Option>, // [Omitting region because Discord deprecated it] #[serde(skip_serializing_if = "Option::is_none")] verification_level: Option, @@ -31,11 +31,11 @@ pub struct EditGuild<'a> { #[serde(skip_serializing_if = "Option::is_none")] owner_id: Option, #[serde(skip_serializing_if = "Option::is_none")] - splash: Option>, + splash: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - discovery_splash: Option>, + discovery_splash: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - banner: Option>, + banner: Option>>, #[serde(skip_serializing_if = "Option::is_none")] system_channel_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -45,11 +45,11 @@ pub struct EditGuild<'a> { #[serde(skip_serializing_if = "Option::is_none")] public_updates_channel_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] - preferred_locale: Option>, + preferred_locale: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - features: Option>, + features: Option]>>, #[serde(skip_serializing_if = "Option::is_none")] - description: Option, + description: Option>, #[serde(skip_serializing_if = "Option::is_none")] premium_progress_bar_enabled: Option, @@ -65,7 +65,6 @@ impl<'a> EditGuild<'a> { /// Set the "AFK voice channel" that users are to move to if they have been AFK for an amount /// of time, configurable by [`Self::afk_timeout`]. Pass [`None`] to unset the current value. - #[inline] pub fn afk_channel(mut self, channel: Option) -> Self { self.afk_channel_id = Some(channel); self @@ -100,7 +99,7 @@ impl<'a> EditGuild<'a> { /// # Ok(()) /// # } /// ``` - pub fn icon(mut self, icon: Option<&CreateAttachment>) -> Self { + pub fn icon(mut self, icon: Option<&CreateAttachment<'_>>) -> Self { self.icon = Some(icon.map(CreateAttachment::to_base64)); self } @@ -114,7 +113,7 @@ impl<'a> EditGuild<'a> { /// Set the name of the guild. /// /// **Note**: Must be between (and including) 2-100 characters. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = Some(name.into()); self } @@ -125,7 +124,7 @@ impl<'a> EditGuild<'a> { /// this through a guild's [`features`] list. /// /// [`features`]: Guild::features - pub fn description(mut self, name: impl Into) -> Self { + pub fn description(mut self, name: impl Into>) -> Self { self.description = Some(name.into()); self } @@ -136,17 +135,16 @@ impl<'a> EditGuild<'a> { /// this through a guild's [`features`] list. /// /// [`features`]: Guild::features - pub fn features(mut self, features: Vec) -> Self { - self.features = Some(features); + pub fn features(mut self, features: impl Into]>>) -> Self { + self.features = Some(features.into()); self } /// Transfers the ownership of the guild to another user by Id. /// /// **Note**: The current user must be the owner of the guild. - #[inline] - pub fn owner(mut self, user_id: impl Into) -> Self { - self.owner_id = Some(user_id.into()); + pub fn owner(mut self, user_id: UserId) -> Self { + self.owner_id = Some(user_id); self } @@ -158,7 +156,7 @@ impl<'a> EditGuild<'a> { /// through a guild's [`features`] list. /// /// [`features`]: Guild::features - pub fn splash(mut self, splash: Option) -> Self { + pub fn splash(mut self, splash: Option>) -> Self { self.splash = Some(splash); self } @@ -171,7 +169,7 @@ impl<'a> EditGuild<'a> { /// a guild's [`features`] list. /// /// [`features`]: Guild::features - pub fn discovery_splash(mut self, splash: Option) -> Self { + pub fn discovery_splash(mut self, splash: Option>) -> Self { self.discovery_splash = Some(splash); self } @@ -184,8 +182,8 @@ impl<'a> EditGuild<'a> { /// guild's [`features`] list. /// /// [`features`]: Guild::features - pub fn banner(mut self, banner: Option) -> Self { - self.banner = Some(banner); + pub fn banner(mut self, banner: Option<&CreateAttachment<'_>>) -> Self { + self.banner = Some(banner.map(CreateAttachment::to_base64).map(Cow::from)); self } @@ -216,7 +214,7 @@ impl<'a> EditGuild<'a> { /// If this is not set, the locale will default to "en-US"; /// /// **Note**: This feature is for Community guilds only. - pub fn preferred_locale(mut self, locale: Option) -> Self { + pub fn preferred_locale(mut self, locale: Option>) -> Self { self.preferred_locale = Some(locale); self } @@ -264,7 +262,6 @@ impl<'a> EditGuild<'a> { /// # Ok(()) /// # } /// ``` - #[inline] pub fn verification_level(mut self, verification_level: impl Into) -> Self { self.verification_level = Some(verification_level.into()); self @@ -310,13 +307,6 @@ impl<'a> EditGuild<'a> { self.premium_progress_bar_enabled = Some(premium_progress_bar_enabled); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditGuild<'a> { - type Context<'ctx> = GuildId; - type Built = PartialGuild; /// Edits the given guild. /// @@ -324,18 +314,11 @@ impl<'a> Builder for EditGuild<'a> { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - #[cfg(feature = "cache")] - crate::utils::user_has_guild_perms(&cache_http, ctx, Permissions::MANAGE_GUILD)?; - - cache_http.http().edit_guild(ctx, &self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, guild_id: GuildId) -> Result { + http.edit_guild(guild_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/edit_guild_welcome_screen.rs b/src/builder/edit_guild_welcome_screen.rs index 8d7b7c4d1af..679337e7c9d 100644 --- a/src/builder/edit_guild_welcome_screen.rs +++ b/src/builder/edit_guild_welcome_screen.rs @@ -1,7 +1,7 @@ +use std::borrow::Cow; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -14,10 +14,10 @@ use crate::model::prelude::*; pub struct EditGuildWelcomeScreen<'a> { #[serde(skip_serializing_if = "Option::is_none")] enabled: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - welcome_channels: Vec, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + welcome_channels: Cow<'a, [CreateGuildWelcomeChannel<'a>]>, #[serde(skip_serializing_if = "Option::is_none")] - description: Option, + description: Option>, #[serde(skip)] audit_log_reason: Option<&'a str>, @@ -36,19 +36,22 @@ impl<'a> EditGuildWelcomeScreen<'a> { } /// The server description shown in the welcome screen. - pub fn description(mut self, description: impl Into) -> Self { + pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } - pub fn add_welcome_channel(mut self, channel: CreateGuildWelcomeChannel) -> Self { - self.welcome_channels.push(channel); + pub fn add_welcome_channel(mut self, channel: CreateGuildWelcomeChannel<'a>) -> Self { + self.welcome_channels.to_mut().push(channel); self } /// Channels linked in the welcome screen and their display options - pub fn set_welcome_channels(mut self, channels: Vec) -> Self { - self.welcome_channels = channels; + pub fn set_welcome_channels( + mut self, + channels: impl Into]>>, + ) -> Self { + self.welcome_channels = channels.into(); self } @@ -57,13 +60,6 @@ impl<'a> EditGuildWelcomeScreen<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditGuildWelcomeScreen<'a> { - type Context<'ctx> = GuildId; - type Built = GuildWelcomeScreen; /// Edits the guild's welcome screen. /// @@ -74,12 +70,9 @@ impl<'a> Builder for EditGuildWelcomeScreen<'a> { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().edit_guild_welcome_screen(ctx, &self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, guild_id: GuildId) -> Result { + http.edit_guild_welcome_screen(guild_id, &self, self.audit_log_reason).await } } @@ -88,32 +81,51 @@ impl<'a> Builder for EditGuildWelcomeScreen<'a> { /// [Discord docs](https://discord.com/developers/docs/resources/guild#welcome-screen-object-welcome-screen-channel-structure) #[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateGuildWelcomeChannel(GuildWelcomeChannel); +pub struct CreateGuildWelcomeChannel<'a> { + channel_id: ChannelId, + emoji_name: Option, + emoji_id: Option, + description: Cow<'a, str>, +} -impl CreateGuildWelcomeChannel { - pub fn new(channel_id: ChannelId, description: String) -> Self { - Self(GuildWelcomeChannel { +impl<'a> CreateGuildWelcomeChannel<'a> { + pub fn new(channel_id: ChannelId, description: impl Into>) -> Self { + Self { channel_id, - description, - emoji: None, - }) + emoji_id: None, + emoji_name: None, + description: description.into(), + } } /// The Id of the channel to show. - pub fn id(mut self, id: impl Into) -> Self { - self.0.channel_id = id.into(); + pub fn id(mut self, id: ChannelId) -> Self { + self.channel_id = id; self } /// The description shown for the channel. - pub fn description(mut self, description: impl Into) -> Self { - self.0.description = description.into(); + pub fn description(mut self, description: impl Into>) -> Self { + self.description = description.into(); self } /// The emoji shown for the channel. pub fn emoji(mut self, emoji: GuildWelcomeChannelEmoji) -> Self { - self.0.emoji = Some(emoji); + match emoji { + GuildWelcomeChannelEmoji::Custom { + id, + name, + } => { + self.emoji_id = Some(id); + self.emoji_name = Some(name.into()); + }, + GuildWelcomeChannelEmoji::Unicode(name) => { + self.emoji_id = None; + self.emoji_name = Some(name.into()); + }, + }; + self } } diff --git a/src/builder/edit_guild_widget.rs b/src/builder/edit_guild_widget.rs index 5bd23f9ab98..294cf6ac8f0 100644 --- a/src/builder/edit_guild_widget.rs +++ b/src/builder/edit_guild_widget.rs @@ -1,7 +1,5 @@ #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -34,8 +32,8 @@ impl<'a> EditGuildWidget<'a> { } /// The server description shown in the welcome screen. - pub fn channel_id(mut self, id: impl Into) -> Self { - self.channel_id = Some(id.into()); + pub fn channel_id(mut self, id: ChannelId) -> Self { + self.channel_id = Some(id); self } @@ -44,13 +42,6 @@ impl<'a> EditGuildWidget<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditGuildWidget<'a> { - type Context<'ctx> = GuildId; - type Built = GuildWidget; /// Edits the guild's widget. /// @@ -61,11 +52,8 @@ impl<'a> Builder for EditGuildWidget<'a> { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().edit_guild_widget(ctx, &self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, guild_id: GuildId) -> Result { + http.edit_guild_widget(guild_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/edit_interaction_response.rs b/src/builder/edit_interaction_response.rs index f6bd18b843f..ea3172bb1c3 100644 --- a/src/builder/edit_interaction_response.rs +++ b/src/builder/edit_interaction_response.rs @@ -1,5 +1,5 @@ -#[cfg(feature = "http")] -use super::Builder; +use std::borrow::Cow; + use super::{ CreateActionRow, CreateAllowedMentions, @@ -9,7 +9,7 @@ use super::{ EditWebhookMessage, }; #[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -17,9 +17,9 @@ use crate::model::prelude::*; /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#edit-original-interaction-response) #[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct EditInteractionResponse(EditWebhookMessage); +pub struct EditInteractionResponse<'a>(EditWebhookMessage<'a>); -impl EditInteractionResponse { +impl<'a> EditInteractionResponse<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() @@ -28,22 +28,21 @@ impl EditInteractionResponse { /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. - #[inline] - pub fn content(self, content: impl Into) -> Self { + pub fn content(self, content: impl Into>) -> Self { Self(self.0.content(content)) } /// Adds an embed for the message. /// /// Embeds from the original message are reset when adding new embeds and must be re-added. - pub fn add_embed(self, embed: CreateEmbed) -> Self { + pub fn add_embed(self, embed: CreateEmbed<'a>) -> Self { Self(self.0.add_embed(embed)) } /// Adds multiple embeds to the message. /// /// Embeds from the original message are reset when adding new embeds and must be re-added. - pub fn add_embeds(self, embeds: Vec) -> Self { + pub fn add_embeds(self, embeds: impl IntoIterator>) -> Self { Self(self.0.add_embeds(embeds)) } @@ -51,7 +50,7 @@ impl EditInteractionResponse { /// /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embed`] /// instead. - pub fn embed(self, embed: CreateEmbed) -> Self { + pub fn embed(self, embed: CreateEmbed<'a>) -> Self { Self(self.0.embed(embed)) } @@ -61,30 +60,30 @@ impl EditInteractionResponse { /// /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embeds`] /// instead. - pub fn embeds(self, embeds: Vec) -> Self { + pub fn embeds(self, embeds: impl Into]>>) -> Self { Self(self.0.embeds(embeds)) } /// Set the allowed mentions for the message. - pub fn allowed_mentions(self, allowed_mentions: CreateAllowedMentions) -> Self { + pub fn allowed_mentions(self, allowed_mentions: CreateAllowedMentions<'a>) -> Self { Self(self.0.allowed_mentions(allowed_mentions)) } /// Sets the components of this message. - pub fn components(self, components: Vec) -> Self { + pub fn components(self, components: impl Into]>>) -> Self { Self(self.0.components(components)) } super::button_and_select_menu_convenience_methods!(self.0.components); /// Sets attachments, see [`EditAttachments`] for more details. - pub fn attachments(self, attachments: EditAttachments) -> Self { + pub fn attachments(self, attachments: EditAttachments<'a>) -> Self { Self(self.0.attachments(attachments)) } /// Adds a new attachment to the message. /// /// Resets existing attachments. See the documentation for [`EditAttachments`] for details. - pub fn new_attachment(self, attachment: CreateAttachment) -> Self { + pub fn new_attachment(self, attachment: CreateAttachment<'a>) -> Self { Self(self.0.new_attachment(attachment)) } @@ -97,13 +96,6 @@ impl EditInteractionResponse { pub fn clear_attachments(self) -> Self { Self(self.0.clear_attachments()) } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for EditInteractionResponse { - type Context<'ctx> = &'ctx str; - type Built = Message; /// Edits the initial interaction response. Does not work for ephemeral messages. /// @@ -118,15 +110,12 @@ impl Builder for EditInteractionResponse { /// Returns an [`Error::Model`] if the message content is too long. May also return an /// [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is an error in /// deserializing the API response. - async fn execute( - mut self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { + #[cfg(feature = "http")] + pub async fn execute(mut self, http: &Http, interaction_token: &str) -> Result { self.0.check_length()?; - let files = self.0.attachments.as_mut().map_or(Vec::new(), |a| a.take_files()); + let files = self.0.attachments.as_mut().map_or(Vec::new(), EditAttachments::take_files); - cache_http.http().edit_original_interaction_response(ctx, &self, files).await + http.edit_original_interaction_response(interaction_token, &self, files).await } } diff --git a/src/builder/edit_member.rs b/src/builder/edit_member.rs index 1cf965e1d29..daf50bea00b 100644 --- a/src/builder/edit_member.rs +++ b/src/builder/edit_member.rs @@ -1,7 +1,7 @@ +use std::borrow::Cow; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -14,9 +14,9 @@ use crate::model::prelude::*; #[must_use] pub struct EditMember<'a> { #[serde(skip_serializing_if = "Option::is_none")] - nick: Option, + nick: Option>, #[serde(skip_serializing_if = "Option::is_none")] - roles: Option>, + roles: Option>, #[serde(skip_serializing_if = "Option::is_none")] mute: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -24,7 +24,7 @@ pub struct EditMember<'a> { #[serde(skip_serializing_if = "Option::is_none")] channel_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] - communication_disabled_until: Option>, + communication_disabled_until: Option>, #[serde(skip_serializing_if = "Option::is_none")] flags: Option, @@ -63,7 +63,7 @@ impl<'a> EditMember<'a> { /// **Note**: Requires the [Manage Nicknames] permission. /// /// [Manage Nicknames]: Permissions::MANAGE_NICKNAMES - pub fn nickname(mut self, nickname: impl Into) -> Self { + pub fn nickname(mut self, nickname: impl Into>) -> Self { self.nick = Some(nickname.into()); self } @@ -73,8 +73,8 @@ impl<'a> EditMember<'a> { /// **Note**: Requires the [Manage Roles] permission to modify. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - pub fn roles(mut self, roles: impl IntoIterator>) -> Self { - self.roles = Some(roles.into_iter().map(Into::into).collect()); + pub fn roles(mut self, roles: impl Into>) -> Self { + self.roles = Some(roles.into()); self } @@ -83,9 +83,8 @@ impl<'a> EditMember<'a> { /// **Note**: Requires the [Move Members] permission. /// /// [Move Members]: Permissions::MOVE_MEMBERS - #[inline] - pub fn voice_channel(mut self, channel_id: impl Into) -> Self { - self.channel_id = Some(Some(channel_id.into())); + pub fn voice_channel(mut self, channel_id: ChannelId) -> Self { + self.channel_id = Some(Some(channel_id)); self } @@ -101,30 +100,18 @@ impl<'a> EditMember<'a> { /// Times the user out until `time`, an ISO8601-formatted datetime string. /// - /// `time` is considered invalid if it is not a valid ISO8601 timestamp or if it is greater + /// `time` is considered invalid if it is greater /// than 28 days from the current time. /// /// **Note**: Requires the [Moderate Members] permission. /// /// [Moderate Members]: Permissions::MODERATE_MEMBERS #[doc(alias = "timeout")] - pub fn disable_communication_until(mut self, time: String) -> Self { + pub fn disable_communication_until(mut self, time: Timestamp) -> Self { self.communication_disabled_until = Some(Some(time)); self } - /// Times the user out until `time`. - /// - /// `time` is considered invalid if it is greater than 28 days from the current time. - /// - /// **Note**: Requires the [Moderate Members] permission. - /// - /// [Moderate Members]: Permissions::MODERATE_MEMBERS - #[doc(alias = "timeout")] - pub fn disable_communication_until_datetime(self, time: Timestamp) -> Self { - self.disable_communication_until(time.to_string()) - } - /// Allow a user to communicate, removing their timeout, if there is one. /// /// **Note**: Requires the [Moderate Members] permission. @@ -146,13 +133,6 @@ impl<'a> EditMember<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditMember<'a> { - type Context<'ctx> = (GuildId, UserId); - type Built = Member; /// Edits the properties of the guild member. /// @@ -161,11 +141,8 @@ impl<'a> Builder for EditMember<'a> { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().edit_member(ctx.0, ctx.1, &self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, guild_id: GuildId, user_id: UserId) -> Result { + http.edit_member(guild_id, user_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/edit_message.rs b/src/builder/edit_message.rs index 93b181cb8ff..ae0e735f3bc 100644 --- a/src/builder/edit_message.rs +++ b/src/builder/edit_message.rs @@ -1,5 +1,5 @@ -#[cfg(feature = "http")] -use super::{check_overflow, Builder}; +use std::borrow::Cow; + use super::{ CreateActionRow, CreateAllowedMentions, @@ -8,8 +8,6 @@ use super::{ EditAttachments, }; #[cfg(feature = "http")] -use crate::constants; -#[cfg(feature = "http")] use crate::http::CacheHttp; #[cfg(feature = "http")] use crate::internal::prelude::*; @@ -25,62 +23,48 @@ use crate::model::prelude::*; /// # use serenity::builder::EditMessage; /// # use serenity::model::channel::Message; /// # use serenity::model::id::ChannelId; -/// # use serenity::http::CacheHttp; +/// # use serenity::http::Http; /// -/// # async fn example(ctx: impl CacheHttp, mut message: Message) -> Result<(), Box> { +/// # async fn example(http: &Http, mut message: Message) -> Result<(), Box> { /// let builder = EditMessage::new().content("hello"); -/// message.edit(ctx, builder).await?; +/// message.edit(http, builder).await?; /// # Ok(()) /// # } /// ``` /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#edit-message) -#[derive(Clone, Debug, Default, Serialize, PartialEq)] +#[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct EditMessage { +pub struct EditMessage<'a> { #[serde(skip_serializing_if = "Option::is_none")] - content: Option, + content: Option>, #[serde(skip_serializing_if = "Option::is_none")] - embeds: Option>, + embeds: Option]>>, #[serde(skip_serializing_if = "Option::is_none")] flags: Option, #[serde(skip_serializing_if = "Option::is_none")] - allowed_mentions: Option, + allowed_mentions: Option>, #[serde(skip_serializing_if = "Option::is_none")] - components: Option>, + components: Option]>>, #[serde(skip_serializing_if = "Option::is_none")] - attachments: Option, + attachments: Option>, } -impl EditMessage { +impl<'a> EditMessage<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() } #[cfg(feature = "http")] - fn check_length(&self) -> Result<()> { - if let Some(content) = &self.content { - check_overflow(content.chars().count(), constants::MESSAGE_CODE_LIMIT) - .map_err(|overflow| Error::Model(ModelError::MessageTooLong(overflow)))?; - } - - if let Some(embeds) = &self.embeds { - check_overflow(embeds.len(), constants::EMBED_MAX_COUNT) - .map_err(|_| Error::Model(ModelError::EmbedAmount))?; - for embed in embeds { - embed.check_length()?; - } - } - - Ok(()) + fn check_length(&self) -> Result<(), ModelError> { + super::check_lengths(self.content.as_deref(), self.embeds.as_deref(), 0) } /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. - #[inline] - pub fn content(mut self, content: impl Into) -> Self { + pub fn content(mut self, content: impl Into>) -> Self { self.content = Some(content.into()); self } @@ -89,8 +73,8 @@ impl EditMessage { /// /// **Note**: This will keep all existing embeds. Use [`Self::embed()`] to replace existing /// embeds. - pub fn add_embed(mut self, embed: CreateEmbed) -> Self { - self.embeds.get_or_insert_with(Vec::new).push(embed); + pub fn add_embed(mut self, embed: CreateEmbed<'a>) -> Self { + self.embeds.get_or_insert_with(Cow::default).to_mut().push(embed); self } @@ -98,8 +82,8 @@ impl EditMessage { /// /// **Note**: This will keep all existing embeds. Use [`Self::embeds()`] to replace existing /// embeds. - pub fn add_embeds(mut self, embeds: Vec) -> Self { - self.embeds.get_or_insert_with(Vec::new).extend(embeds); + pub fn add_embeds(mut self, embeds: impl IntoIterator>) -> Self { + self.embeds.get_or_insert_with(Cow::default).to_mut().extend(embeds); self } @@ -107,7 +91,7 @@ impl EditMessage { /// /// **Note**: This will replace all existing embeds. Use [`Self::add_embed()`] to keep existing /// embeds. - pub fn embed(self, embed: CreateEmbed) -> Self { + pub fn embed(self, embed: CreateEmbed<'a>) -> Self { self.embeds(vec![embed]) } @@ -115,8 +99,8 @@ impl EditMessage { /// /// **Note**: This will replace all existing embeds. Use [`Self::add_embeds()`] to keep existing /// embeds. - pub fn embeds(mut self, embeds: Vec) -> Self { - self.embeds = Some(embeds); + pub fn embeds(mut self, embeds: impl Into]>>) -> Self { + self.embeds = Some(embeds.into()); self } @@ -136,7 +120,7 @@ impl EditMessage { /// /// use futures::StreamExt; /// - /// let mut msg = channel_id.say(ctx, "").await?; + /// let mut msg = channel_id.say(&ctx.http, "").await?; /// /// // When the embed appears, a MessageUpdate event is sent and we suppress the embed. /// // No MessageUpdate event is sent if the message contains no embeddable link or if the link @@ -163,14 +147,14 @@ impl EditMessage { } /// Set the allowed mentions for the message. - pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions<'a>) -> Self { self.allowed_mentions = Some(allowed_mentions); self } /// Sets the components of this message. - pub fn components(mut self, components: Vec) -> Self { - self.components = Some(components); + pub fn components(mut self, components: impl Into]>>) -> Self { + self.components = Some(components.into()); self } super::button_and_select_menu_convenience_methods!(self.components); @@ -182,7 +166,7 @@ impl EditMessage { } /// Sets attachments, see [`EditAttachments`] for more details. - pub fn attachments(mut self, attachments: EditAttachments) -> Self { + pub fn attachments(mut self, attachments: EditAttachments<'a>) -> Self { self.attachments = Some(attachments); self } @@ -190,7 +174,7 @@ impl EditMessage { /// Adds a new attachment to the message. /// /// Resets existing attachments. See the documentation for [`EditAttachments`] for details. - pub fn new_attachment(mut self, attachment: CreateAttachment) -> Self { + pub fn new_attachment(mut self, attachment: CreateAttachment<'a>) -> Self { let attachments = self.attachments.get_or_insert_with(Default::default); self.attachments = Some(std::mem::take(attachments).add(attachment)); self @@ -216,13 +200,15 @@ impl EditMessage { self.attachments = Some(EditAttachments::new()); self } -} -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for EditMessage { - type Context<'ctx> = (ChannelId, MessageId, Option); - type Built = Message; + fn is_only_suppress_embeds(&self) -> bool { + self.flags == Some(MessageFlags::SUPPRESS_EMBEDS) + && self.content.is_none() + && self.embeds.is_none() + && self.allowed_mentions.is_none() + && self.components.is_none() + && self.attachments.is_none() + } /// Edits a message in the channel. /// @@ -241,37 +227,38 @@ impl Builder for EditMessage { /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the message contents are over the above limits. + /// Returns a [`ModelError::TooLarge`] if the message contents are over the above limits. /// /// Returns [`Error::Http`] if the user lacks permission, as well as if invalid data is given. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES /// [`From`]: CreateEmbed#impl-From - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( mut self, cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { + channel_id: ChannelId, + message_id: MessageId, + user_id: Option, + ) -> Result { self.check_length()?; #[cfg(feature = "cache")] - if let Some(user_id) = ctx.2 { + if let Some(user_id) = user_id { if let Some(cache) = cache_http.cache() { - let reference_builder = EditMessage::new().suppress_embeds(true); - - if user_id != cache.current_user().id && self != reference_builder { + if user_id != cache.current_user().id && !self.is_only_suppress_embeds() { return Err(Error::Model(ModelError::InvalidUser)); } } } - let files = self.attachments.as_mut().map_or(Vec::new(), |a| a.take_files()); + let files = self.attachments.as_mut().map_or(Vec::new(), EditAttachments::take_files); let http = cache_http.http(); if self.allowed_mentions.is_none() { self.allowed_mentions.clone_from(&http.default_allowed_mentions); } - http.edit_message(ctx.0, ctx.1, &self, files).await + http.edit_message(channel_id, message_id, &self, files).await } } diff --git a/src/builder/edit_profile.rs b/src/builder/edit_profile.rs index 9656181ec9c..d054ef2bbfa 100644 --- a/src/builder/edit_profile.rs +++ b/src/builder/edit_profile.rs @@ -1,8 +1,8 @@ -#[cfg(feature = "http")] -use super::Builder; +use std::borrow::Cow; + use super::CreateAttachment; #[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; #[cfg(feature = "http")] @@ -14,16 +14,16 @@ use crate::model::user::CurrentUser; /// [Discord docs](https://discord.com/developers/docs/resources/user#modify-current-user) #[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct EditProfile { +pub struct EditProfile<'a> { #[serde(skip_serializing_if = "Option::is_none")] - username: Option, + username: Option>, #[serde(skip_serializing_if = "Option::is_none")] avatar: Option>, #[serde(skip_serializing_if = "Option::is_none")] banner: Option>, } -impl EditProfile { +impl<'a> EditProfile<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() @@ -46,7 +46,7 @@ impl EditProfile { /// # Ok(()) /// # } /// ``` - pub fn avatar(mut self, avatar: &CreateAttachment) -> Self { + pub fn avatar(mut self, avatar: &CreateAttachment<'_>) -> Self { self.avatar = Some(Some(avatar.to_base64())); self } @@ -62,13 +62,13 @@ impl EditProfile { /// When modifying the username, if another user has the same _new_ username and current /// discriminator, a new unique discriminator will be assigned. If there are no available /// discriminators with the requested username, an error will occur. - pub fn username(mut self, username: impl Into) -> Self { + pub fn username(mut self, username: impl Into>) -> Self { self.username = Some(username.into()); self } /// Sets the banner of the current user. - pub fn banner(mut self, banner: &CreateAttachment) -> Self { + pub fn banner(mut self, banner: &CreateAttachment<'_>) -> Self { self.banner = Some(Some(banner.to_base64())); self } @@ -78,13 +78,6 @@ impl EditProfile { self.banner = Some(None); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for EditProfile { - type Context<'ctx> = (); - type Built = CurrentUser; /// Edit the current user's profile with the fields set. /// @@ -92,11 +85,8 @@ impl Builder for EditProfile { /// /// Returns an [`Error::Http`] if an invalid value is set. May also return an [`Error::Json`] /// if there is an error in deserializing the API response. - async fn execute( - self, - cache_http: impl CacheHttp, - _ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().edit_profile(&self).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http) -> Result { + http.edit_profile(&self).await } } diff --git a/src/builder/edit_role.rs b/src/builder/edit_role.rs index d7db7c05847..bbd8845026a 100644 --- a/src/builder/edit_role.rs +++ b/src/builder/edit_role.rs @@ -1,9 +1,8 @@ -#[cfg(feature = "http")] -use super::Builder; +use std::borrow::Cow; + use super::CreateAttachment; #[cfg(feature = "http")] -use crate::http::CacheHttp; -#[cfg(feature = "http")] +use crate::http::Http; use crate::internal::prelude::*; use crate::model::prelude::*; @@ -47,7 +46,7 @@ use crate::model::prelude::*; #[must_use] pub struct EditRole<'a> { #[serde(skip_serializing_if = "Option::is_none")] - name: Option, + name: Option>, #[serde(skip_serializing_if = "Option::is_none")] permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -56,15 +55,15 @@ pub struct EditRole<'a> { #[serde(skip_serializing_if = "Option::is_none")] hoist: Option, #[serde(skip_serializing_if = "Option::is_none")] - icon: Option>, + icon: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - unicode_emoji: Option>, + unicode_emoji: Option>>, #[serde(skip_serializing_if = "Option::is_none")] mentionable: Option, #[serde(skip)] - position: Option, + position: Option, #[serde(skip)] audit_log_reason: Option<&'a str>, } @@ -76,15 +75,15 @@ impl<'a> EditRole<'a> { } /// Creates a new builder with the values of the given [`Role`]. - pub fn from_role(role: &Role) -> Self { + pub fn from_role(role: &'a Role) -> Self { EditRole { - hoist: Some(role.hoist), - mentionable: Some(role.mentionable), - name: Some(role.name.clone()), + hoist: Some(role.hoist()), + mentionable: Some(role.mentionable()), + name: Some(Cow::Borrowed(&role.name)), permissions: Some(role.permissions.bits()), position: Some(role.position), colour: Some(role.colour), - unicode_emoji: role.unicode_emoji.as_ref().map(|v| Some(v.clone())), + unicode_emoji: role.unicode_emoji.as_ref().map(|v| Some(Cow::Borrowed(v.as_str()))), audit_log_reason: None, // TODO: Do we want to download role.icon? icon: None, @@ -111,7 +110,7 @@ impl<'a> EditRole<'a> { } /// Set the role's name. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = Some(name.into()); self } @@ -124,21 +123,21 @@ impl<'a> EditRole<'a> { /// Set the role's position in the role list. This correlates to the role's position in the /// user list. - pub fn position(mut self, position: u16) -> Self { + pub fn position(mut self, position: i16) -> Self { self.position = Some(position); self } /// Set the role icon to a unicode emoji. pub fn unicode_emoji(mut self, unicode_emoji: Option) -> Self { - self.unicode_emoji = Some(unicode_emoji); + self.unicode_emoji = Some(unicode_emoji.map(Into::into)); self.icon = Some(None); self } /// Set the role icon to a custom image. - pub fn icon(mut self, icon: Option<&CreateAttachment>) -> Self { - self.icon = Some(icon.map(CreateAttachment::to_base64)); + pub fn icon(mut self, icon: Option<&CreateAttachment<'_>>) -> Self { + self.icon = Some(icon.map(CreateAttachment::to_base64).map(Into::into)); self.unicode_emoji = Some(None); self } @@ -148,13 +147,6 @@ impl<'a> EditRole<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditRole<'a> { - type Context<'ctx> = (GuildId, Option); - type Built = Role; /// Edits the role. /// @@ -162,21 +154,16 @@ impl<'a> Builder for EditRole<'a> { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - let (guild_id, role_id) = ctx; - - #[cfg(feature = "cache")] - crate::utils::user_has_guild_perms(&cache_http, guild_id, Permissions::MANAGE_ROLES)?; - - let http = cache_http.http(); + http: &Http, + guild_id: GuildId, + role_id: Option, + ) -> Result { let role = match role_id { Some(role_id) => { http.edit_role(guild_id, role_id, &self, self.audit_log_reason).await? @@ -185,7 +172,7 @@ impl<'a> Builder for EditRole<'a> { }; if let Some(position) = self.position { - http.edit_role_position(guild_id, role.id, position, self.audit_log_reason).await?; + guild_id.edit_role_position(http, role.id, position, self.audit_log_reason).await?; } Ok(role) } diff --git a/src/builder/edit_scheduled_event.rs b/src/builder/edit_scheduled_event.rs index 77b94fd0921..f7af199893d 100644 --- a/src/builder/edit_scheduled_event.rs +++ b/src/builder/edit_scheduled_event.rs @@ -1,8 +1,8 @@ +use std::borrow::Cow; + +use super::{CreateAttachment, CreateScheduledEventMetadata}; #[cfg(feature = "http")] -use super::Builder; -use super::CreateAttachment; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -14,17 +14,17 @@ pub struct EditScheduledEvent<'a> { #[serde(skip_serializing_if = "Option::is_none")] channel_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] - entity_metadata: Option>, + entity_metadata: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - name: Option, + name: Option>, #[serde(skip_serializing_if = "Option::is_none")] privacy_level: Option, #[serde(skip_serializing_if = "Option::is_none")] - scheduled_start_time: Option, + scheduled_start_time: Option, #[serde(skip_serializing_if = "Option::is_none")] - scheduled_end_time: Option, + scheduled_end_time: Option, #[serde(skip_serializing_if = "Option::is_none")] - description: Option, + description: Option>, #[serde(skip_serializing_if = "Option::is_none")] entity_type: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -48,13 +48,13 @@ impl<'a> EditScheduledEvent<'a> { /// [`kind`]: EditScheduledEvent::kind /// [`Voice`]: ScheduledEventType::Voice /// [`External`]: ScheduledEventType::External - pub fn channel_id(mut self, channel_id: impl Into) -> Self { - self.channel_id = Some(Some(channel_id.into())); + pub fn channel_id(mut self, channel_id: ChannelId) -> Self { + self.channel_id = Some(Some(channel_id)); self } /// Sets the name of the scheduled event. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = Some(name.into()); self } @@ -66,15 +66,14 @@ impl<'a> EditScheduledEvent<'a> { } /// Sets the description of the scheduled event. - pub fn description(mut self, description: impl Into) -> Self { + pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } /// Sets the start time of the scheduled event. - #[inline] pub fn start_time(mut self, timestamp: impl Into) -> Self { - self.scheduled_start_time = Some(timestamp.into().to_string()); + self.scheduled_start_time = Some(timestamp.into()); self } @@ -84,9 +83,8 @@ impl<'a> EditScheduledEvent<'a> { /// /// [`kind`]: EditScheduledEvent::kind /// [`External`]: ScheduledEventType::External - #[inline] pub fn end_time(mut self, timestamp: impl Into) -> Self { - self.scheduled_end_time = Some(timestamp.into().to_string()); + self.scheduled_end_time = Some(timestamp.into()); self } @@ -146,15 +144,15 @@ impl<'a> EditScheduledEvent<'a> { /// /// [`kind`]: EditScheduledEvent::kind /// [`External`]: ScheduledEventType::External - pub fn location(mut self, location: impl Into) -> Self { - self.entity_metadata = Some(Some(ScheduledEventMetadata { + pub fn location(mut self, location: impl Into>) -> Self { + self.entity_metadata = Some(Some(CreateScheduledEventMetadata { location: Some(location.into()), })); self } /// Sets the cover image for the scheduled event. - pub fn image(mut self, image: &CreateAttachment) -> Self { + pub fn image(mut self, image: &CreateAttachment<'_>) -> Self { self.image = Some(image.to_base64()); self } @@ -164,13 +162,6 @@ impl<'a> EditScheduledEvent<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditScheduledEvent<'a> { - type Context<'ctx> = (GuildId, ScheduledEventId); - type Built = ScheduledEvent; /// Modifies a scheduled event in the guild with the data set, if any. /// @@ -179,16 +170,17 @@ impl<'a> Builder for EditScheduledEvent<'a> { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Create Events]: Permissions::CREATE_EVENTS /// [Manage Events]: Permissions::MANAGE_EVENTS - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().edit_scheduled_event(ctx.0, ctx.1, &self, self.audit_log_reason).await + http: &Http, + guild_id: GuildId, + event_id: ScheduledEventId, + ) -> Result { + http.edit_scheduled_event(guild_id, event_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/edit_stage_instance.rs b/src/builder/edit_stage_instance.rs index d69812e4cf8..ce488691084 100644 --- a/src/builder/edit_stage_instance.rs +++ b/src/builder/edit_stage_instance.rs @@ -1,7 +1,7 @@ +use std::borrow::Cow; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -13,7 +13,7 @@ use crate::model::prelude::*; #[must_use] pub struct EditStageInstance<'a> { #[serde(skip_serializing_if = "Option::is_none")] - topic: Option, + topic: Option>, #[serde(skip_serializing_if = "Option::is_none")] privacy_level: Option, @@ -28,7 +28,7 @@ impl<'a> EditStageInstance<'a> { } /// Sets the topic of the stage channel instance. - pub fn topic(mut self, topic: impl Into) -> Self { + pub fn topic(mut self, topic: impl Into>) -> Self { self.topic = Some(topic.into()); self } @@ -44,13 +44,6 @@ impl<'a> EditStageInstance<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditStageInstance<'a> { - type Context<'ctx> = ChannelId; - type Built = StageInstance; /// Edits the stage instance /// @@ -58,11 +51,8 @@ impl<'a> Builder for EditStageInstance<'a> { /// /// Returns [`Error::Http`] if the channel is not a stage channel, or there is no stage /// instance currently. - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().edit_stage_instance(ctx, &self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, channel_id: ChannelId) -> Result { + http.edit_stage_instance(channel_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/edit_sticker.rs b/src/builder/edit_sticker.rs index 2b8af762646..088cd265ea8 100644 --- a/src/builder/edit_sticker.rs +++ b/src/builder/edit_sticker.rs @@ -1,7 +1,7 @@ +use std::borrow::Cow; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; #[cfg(any(feature = "http", doc))] @@ -21,11 +21,11 @@ use crate::model::prelude::*; #[must_use] pub struct EditSticker<'a> { #[serde(skip_serializing_if = "Option::is_none")] - name: Option, + name: Option>, #[serde(skip_serializing_if = "Option::is_none")] - description: Option, + description: Option>, #[serde(skip_serializing_if = "Option::is_none")] - tags: Option, + tags: Option>, #[serde(skip)] audit_log_reason: Option<&'a str>, @@ -40,7 +40,7 @@ impl<'a> EditSticker<'a> { /// The name of the sticker to set. /// /// **Note**: Must be between 2 and 30 characters long. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = Some(name.into()); self } @@ -48,7 +48,7 @@ impl<'a> EditSticker<'a> { /// The description of the sticker. /// /// **Note**: If not empty, must be between 2 and 100 characters long. - pub fn description(mut self, description: impl Into) -> Self { + pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(description.into()); self } @@ -56,7 +56,7 @@ impl<'a> EditSticker<'a> { /// The Discord name of a unicode emoji representing the sticker's expression. /// /// **Note**: Must be between 2 and 200 characters long. - pub fn tags(mut self, tags: impl Into) -> Self { + pub fn tags(mut self, tags: impl Into>) -> Self { self.tags = Some(tags.into()); self } @@ -66,13 +66,6 @@ impl<'a> EditSticker<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditSticker<'a> { - type Context<'ctx> = (GuildId, StickerId); - type Built = Sticker; /// Edits the sticker. /// @@ -86,11 +79,13 @@ impl<'a> Builder for EditSticker<'a> { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().edit_sticker(ctx.0, ctx.1, &self, self.audit_log_reason).await + http: &Http, + guild_id: GuildId, + sticker_id: StickerId, + ) -> Result { + http.edit_sticker(guild_id, sticker_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/edit_thread.rs b/src/builder/edit_thread.rs index ca3a5214148..4175226f31a 100644 --- a/src/builder/edit_thread.rs +++ b/src/builder/edit_thread.rs @@ -1,7 +1,9 @@ +use std::borrow::Cow; + +use nonmax::NonMaxU16; + #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -11,7 +13,7 @@ use crate::model::prelude::*; #[must_use] pub struct EditThread<'a> { #[serde(skip_serializing_if = "Option::is_none")] - name: Option, + name: Option>, #[serde(skip_serializing_if = "Option::is_none")] archived: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -21,11 +23,11 @@ pub struct EditThread<'a> { #[serde(skip_serializing_if = "Option::is_none")] invitable: Option, #[serde(skip_serializing_if = "Option::is_none")] - rate_limit_per_user: Option, + rate_limit_per_user: Option, #[serde(skip_serializing_if = "Option::is_none")] flags: Option, #[serde(skip_serializing_if = "Option::is_none")] - applied_tags: Option>, + applied_tags: Option>, #[serde(skip)] audit_log_reason: Option<&'a str>, @@ -40,7 +42,7 @@ impl<'a> EditThread<'a> { /// The name of the thread. /// /// **Note**: Must be between 2 and 100 characters long. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = Some(name.into()); self } @@ -77,7 +79,7 @@ impl<'a> EditThread<'a> { /// Amount of seconds a user has to wait before sending another message (0-21600); bots, as well /// as users with the permission manage_messages, manage_thread, or manage_channel, are /// unaffected - pub fn rate_limit_per_user(mut self, rate_limit_per_user: u16) -> Self { + pub fn rate_limit_per_user(mut self, rate_limit_per_user: NonMaxU16) -> Self { self.rate_limit_per_user = Some(rate_limit_per_user); self } @@ -90,8 +92,8 @@ impl<'a> EditThread<'a> { } /// If this is a forum post, edits the assigned tags of this forum post. - pub fn applied_tags(mut self, applied_tags: impl IntoIterator) -> Self { - self.applied_tags = Some(applied_tags.into_iter().collect()); + pub fn applied_tags(mut self, applied_tags: impl Into>) -> Self { + self.applied_tags = Some(applied_tags.into()); self } @@ -100,24 +102,14 @@ impl<'a> EditThread<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditThread<'a> { - type Context<'ctx> = ChannelId; - type Built = GuildChannel; /// Edits the thread. /// /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission. - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().edit_thread(ctx, &self, self.audit_log_reason).await + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, channel_id: ChannelId) -> Result { + http.edit_thread(channel_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/edit_voice_state.rs b/src/builder/edit_voice_state.rs index 751c3c3e7f7..0436733167e 100644 --- a/src/builder/edit_voice_state.rs +++ b/src/builder/edit_voice_state.rs @@ -1,7 +1,5 @@ #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -60,13 +58,6 @@ impl EditVoiceState { self.request_to_speak_timestamp = Some(Some(timestamp.into())); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for EditVoiceState { - type Context<'ctx> = (GuildId, ChannelId, Option); - type Built = (); /// Edits the given user's voice state in a stage channel. Providing a [`UserId`] will edit /// that user's voice state, otherwise the current user's voice state will be edited. @@ -81,18 +72,19 @@ impl Builder for EditVoiceState { /// /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK /// [Mute Members]: Permissions::MUTE_MEMBERS - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( mut self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - let (guild_id, channel_id, user_id) = ctx; - + http: &Http, + guild_id: GuildId, + channel_id: ChannelId, + user_id: Option, + ) -> Result<()> { self.channel_id = Some(channel_id); if let Some(user_id) = user_id { - cache_http.http().edit_voice_state(guild_id, user_id, &self).await + http.edit_voice_state(guild_id, user_id, &self).await } else { - cache_http.http().edit_voice_state_me(guild_id, &self).await + http.edit_voice_state_me(guild_id, &self).await } } } diff --git a/src/builder/edit_webhook.rs b/src/builder/edit_webhook.rs index 9b2bc429d84..e50773eeed4 100644 --- a/src/builder/edit_webhook.rs +++ b/src/builder/edit_webhook.rs @@ -1,8 +1,8 @@ -#[cfg(feature = "http")] -use super::Builder; +use std::borrow::Cow; + use super::CreateAttachment; #[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -12,7 +12,7 @@ use crate::model::prelude::*; #[must_use] pub struct EditWebhook<'a> { #[serde(skip_serializing_if = "Option::is_none")] - name: Option, + name: Option>, #[serde(skip_serializing_if = "Option::is_none")] avatar: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -31,19 +31,19 @@ impl<'a> EditWebhook<'a> { /// Set the webhook's name. /// /// This must be between 1-80 characters. - pub fn name(mut self, name: impl Into) -> Self { + pub fn name(mut self, name: impl Into>) -> Self { self.name = Some(name.into()); self } /// Set the channel to move the webhook to. - pub fn channel_id(mut self, channel_id: impl Into) -> Self { - self.channel_id = Some(channel_id.into()); + pub fn channel_id(mut self, channel_id: ChannelId) -> Self { + self.channel_id = Some(channel_id); self } /// Set the webhook's default avatar. - pub fn avatar(mut self, avatar: &CreateAttachment) -> Self { + pub fn avatar(mut self, avatar: &CreateAttachment<'_>) -> Self { self.avatar = Some(Some(avatar.to_base64())); self } @@ -59,13 +59,6 @@ impl<'a> EditWebhook<'a> { self.audit_log_reason = Some(reason); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl<'a> Builder for EditWebhook<'a> { - type Context<'ctx> = (WebhookId, Option<&'ctx str>); - type Built = Webhook; /// Edits the webhook corresponding to the provided [`WebhookId`] and token, and returns the /// resulting new [`Webhook`]. @@ -75,19 +68,18 @@ impl<'a> Builder for EditWebhook<'a> { /// Returns [`Error::Http`] if the content is malformed, or if the token is invalid. /// /// Returns [`Error::Json`] if there is an error in deserialising Discord's response. - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - match ctx.1 { + http: &Http, + webhook_id: WebhookId, + webhook_token: Option<&str>, + ) -> Result { + match webhook_token { Some(token) => { - cache_http - .http() - .edit_webhook_with_token(ctx.0, token, &self, self.audit_log_reason) - .await + http.edit_webhook_with_token(webhook_id, token, &self, self.audit_log_reason).await }, - None => cache_http.http().edit_webhook(ctx.0, &self, self.audit_log_reason).await, + None => http.edit_webhook(webhook_id, &self, self.audit_log_reason).await, } } } diff --git a/src/builder/edit_webhook_message.rs b/src/builder/edit_webhook_message.rs index 6053dac9248..bbed86fe17d 100644 --- a/src/builder/edit_webhook_message.rs +++ b/src/builder/edit_webhook_message.rs @@ -1,5 +1,5 @@ -#[cfg(feature = "http")] -use super::{check_overflow, Builder}; +use std::borrow::Cow; + use super::{ CreateActionRow, CreateAllowedMentions, @@ -8,9 +8,7 @@ use super::{ EditAttachments, }; #[cfg(feature = "http")] -use crate::constants; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -20,76 +18,61 @@ use crate::model::prelude::*; /// [Discord docs](https://discord.com/developers/docs/resources/webhook#edit-webhook-message) #[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct EditWebhookMessage { +pub struct EditWebhookMessage<'a> { #[serde(skip_serializing_if = "Option::is_none")] - content: Option, + content: Option>, #[serde(skip_serializing_if = "Option::is_none")] - embeds: Option>, + embeds: Option]>>, #[serde(skip_serializing_if = "Option::is_none")] - allowed_mentions: Option, + allowed_mentions: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) components: Option>, + pub(crate) components: Option]>>, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) attachments: Option, + pub(crate) attachments: Option>, #[serde(skip)] thread_id: Option, } -impl EditWebhookMessage { +impl<'a> EditWebhookMessage<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() } #[cfg(feature = "http")] - pub(crate) fn check_length(&self) -> Result<()> { - if let Some(content) = &self.content { - check_overflow(content.chars().count(), constants::MESSAGE_CODE_LIMIT) - .map_err(|overflow| Error::Model(ModelError::MessageTooLong(overflow)))?; - } - - if let Some(embeds) = &self.embeds { - check_overflow(embeds.len(), constants::EMBED_MAX_COUNT) - .map_err(|_| Error::Model(ModelError::EmbedAmount))?; - for embed in embeds { - embed.check_length()?; - } - } - - Ok(()) + pub(crate) fn check_length(&self) -> Result<(), ModelError> { + super::check_lengths(self.content.as_deref(), self.embeds.as_deref(), 0) } /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. - #[inline] - pub fn content(mut self, content: impl Into) -> Self { + pub fn content(mut self, content: impl Into>) -> Self { self.content = Some(content.into()); self } /// Edits a message within a given thread. If the provided thread Id doesn't belong to the /// current webhook, the API will return an error. - #[inline] - pub fn in_thread(mut self, thread_id: impl Into) -> Self { - self.thread_id = Some(thread_id.into()); + pub fn in_thread(mut self, thread_id: ChannelId) -> Self { + self.thread_id = Some(thread_id); self } /// Adds an embed for the message. /// /// Embeds from the original message are reset when adding new embeds and must be re-added. - pub fn add_embed(mut self, embed: CreateEmbed) -> Self { - self.embeds.get_or_insert(Vec::new()).push(embed); + pub fn add_embed(mut self, embed: CreateEmbed<'a>) -> Self { + self.embeds.get_or_insert_with(Cow::default).to_mut().push(embed); self } /// Adds multiple embeds to the message. /// /// Embeds from the original message are reset when adding new embeds and must be re-added. - pub fn add_embeds(mut self, embeds: Vec) -> Self { - self.embeds.get_or_insert(Vec::new()).extend(embeds); + pub fn add_embeds(mut self, embeds: impl IntoIterator>) -> Self { + self.embeds.get_or_insert_with(Cow::default).to_mut().extend(embeds); self } @@ -97,9 +80,8 @@ impl EditWebhookMessage { /// /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embed`] /// instead. - pub fn embed(mut self, embed: CreateEmbed) -> Self { - self.embeds = Some(vec![embed]); - self + pub fn embed(self, embed: CreateEmbed<'a>) -> Self { + self.embeds(vec![embed]) } /// Sets the embeds for the message. @@ -108,13 +90,13 @@ impl EditWebhookMessage { /// /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embeds`] /// instead. - pub fn embeds(mut self, embeds: Vec) -> Self { - self.embeds = Some(embeds); + pub fn embeds(mut self, embeds: impl Into]>>) -> Self { + self.embeds = Some(embeds.into()); self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions<'a>) -> Self { self.allowed_mentions = Some(allowed_mentions); self } @@ -125,14 +107,14 @@ impl EditWebhookMessage { /// /// [`WebhookType::Application`]: crate::model::webhook::WebhookType /// [`WebhookType::Incoming`]: crate::model::webhook::WebhookType - pub fn components(mut self, components: Vec) -> Self { - self.components = Some(components); + pub fn components(mut self, components: impl Into]>>) -> Self { + self.components = Some(components.into()); self } super::button_and_select_menu_convenience_methods!(self.components); /// Sets attachments, see [`EditAttachments`] for more details. - pub fn attachments(mut self, attachments: EditAttachments) -> Self { + pub fn attachments(mut self, attachments: EditAttachments<'a>) -> Self { self.attachments = Some(attachments); self } @@ -140,7 +122,7 @@ impl EditWebhookMessage { /// Adds a new attachment to the message. /// /// Resets existing attachments. See the documentation for [`EditAttachments`] for details. - pub fn new_attachment(mut self, attachment: CreateAttachment) -> Self { + pub fn new_attachment(mut self, attachment: CreateAttachment<'a>) -> Self { let attachments = self.attachments.get_or_insert_with(Default::default); self.attachments = Some(std::mem::take(attachments).add(attachment)); self @@ -158,13 +140,6 @@ impl EditWebhookMessage { self.attachments = Some(EditAttachments::new()); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for EditWebhookMessage { - type Context<'ctx> = (WebhookId, &'ctx str, MessageId); - type Built = Message; /// Edits the webhook's message. /// @@ -179,20 +154,30 @@ impl Builder for EditWebhookMessage { /// invalid, or the given message Id does not belong to the webhook. /// /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response. - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( mut self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { + http: &Http, + webhook_id: WebhookId, + webhook_token: &str, + message_id: MessageId, + ) -> Result { self.check_length()?; - let files = self.attachments.as_mut().map_or(Vec::new(), |a| a.take_files()); + let files = self.attachments.as_mut().map_or(Vec::new(), EditAttachments::take_files); - let http = cache_http.http(); if self.allowed_mentions.is_none() { self.allowed_mentions.clone_from(&http.default_allowed_mentions); } - http.edit_webhook_message(ctx.0, self.thread_id, ctx.1, ctx.2, &self, files).await + http.edit_webhook_message( + webhook_id, + self.thread_id, + webhook_token, + message_id, + &self, + files, + ) + .await } } diff --git a/src/builder/execute_webhook.rs b/src/builder/execute_webhook.rs index 4265e9c38b8..02259b8d95d 100644 --- a/src/builder/execute_webhook.rs +++ b/src/builder/execute_webhook.rs @@ -1,5 +1,5 @@ -#[cfg(feature = "http")] -use super::{check_overflow, Builder}; +use std::borrow::Cow; + use super::{ CreateActionRow, CreateAllowedMentions, @@ -8,9 +8,7 @@ use super::{ EditAttachments, }; #[cfg(feature = "http")] -use crate::constants; -#[cfg(feature = "http")] -use crate::http::CacheHttp; +use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -58,49 +56,38 @@ use crate::model::prelude::*; /// [Discord docs](https://discord.com/developers/docs/resources/webhook#execute-webhook) #[derive(Clone, Debug, Default, Serialize)] #[must_use] -pub struct ExecuteWebhook { +pub struct ExecuteWebhook<'a> { #[serde(skip_serializing_if = "Option::is_none")] - content: Option, + content: Option>, #[serde(skip_serializing_if = "Option::is_none")] - username: Option, + username: Option>, #[serde(skip_serializing_if = "Option::is_none")] - avatar_url: Option, + avatar_url: Option>, tts: bool, - embeds: Vec, + embeds: Cow<'a, [CreateEmbed<'a>]>, #[serde(skip_serializing_if = "Option::is_none")] - allowed_mentions: Option, + allowed_mentions: Option>, #[serde(skip_serializing_if = "Option::is_none")] - components: Option>, + components: Option]>>, #[serde(skip_serializing_if = "Option::is_none")] flags: Option, #[serde(skip_serializing_if = "Option::is_none")] - thread_name: Option, - attachments: EditAttachments, + thread_name: Option>, + attachments: EditAttachments<'a>, #[serde(skip)] thread_id: Option, } -impl ExecuteWebhook { +impl<'a> ExecuteWebhook<'a> { /// Equivalent to [`Self::default`]. pub fn new() -> Self { Self::default() } #[cfg(feature = "http")] - fn check_length(&self) -> Result<()> { - if let Some(content) = &self.content { - check_overflow(content.chars().count(), constants::MESSAGE_CODE_LIMIT) - .map_err(|overflow| Error::Model(ModelError::MessageTooLong(overflow)))?; - } - - check_overflow(self.embeds.len(), constants::EMBED_MAX_COUNT) - .map_err(|_| Error::Model(ModelError::EmbedAmount))?; - for embed in &self.embeds { - embed.check_length()?; - } - - Ok(()) + fn check_length(&self) -> Result<(), ModelError> { + super::check_lengths(self.content.as_deref(), Some(&self.embeds), 0) } /// Override the default avatar of the webhook with an image URL. @@ -124,7 +111,7 @@ impl ExecuteWebhook { /// # Ok(()) /// # } /// ``` - pub fn avatar_url(mut self, avatar_url: impl Into) -> Self { + pub fn avatar_url(mut self, avatar_url: impl Into>) -> Self { self.avatar_url = Some(avatar_url.into()); self } @@ -155,7 +142,7 @@ impl ExecuteWebhook { /// # Ok(()) /// # } /// ``` - pub fn content(mut self, content: impl Into) -> Self { + pub fn content(mut self, content: impl Into>) -> Self { self.content = Some(content.into()); self } @@ -172,31 +159,31 @@ impl ExecuteWebhook { /// ```rust,no_run /// # use serenity::builder::ExecuteWebhook; /// # use serenity::http::Http; - /// # use serenity::model::webhook::Webhook; + /// # use serenity::model::{id::ChannelId, webhook::Webhook}; /// # /// # async fn run() -> Result<(), Box> { /// # let http: Http = unimplemented!(); /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// let mut webhook = Webhook::from_url(&http, url).await?; /// - /// let builder = ExecuteWebhook::new().in_thread(12345678).content("test"); + /// let builder = ExecuteWebhook::new().in_thread(ChannelId::new(12345678)).content("test"); /// webhook.execute(&http, false, builder).await?; /// # Ok(()) /// # } /// ``` - pub fn in_thread(mut self, thread_id: impl Into) -> Self { - self.thread_id = Some(thread_id.into()); + pub fn in_thread(mut self, thread_id: ChannelId) -> Self { + self.thread_id = Some(thread_id); self } /// Appends a file to the webhook message. - pub fn add_file(mut self, file: CreateAttachment) -> Self { + pub fn add_file(mut self, file: CreateAttachment<'a>) -> Self { self.attachments = self.attachments.add(file); self } /// Appends a list of files to the webhook message. - pub fn add_files(mut self, files: impl IntoIterator) -> Self { + pub fn add_files(mut self, files: impl IntoIterator>) -> Self { for file in files { self.attachments = self.attachments.add(file); } @@ -207,13 +194,13 @@ impl ExecuteWebhook { /// /// Calling this multiple times will overwrite the file list. To append files, call /// [`Self::add_file`] or [`Self::add_files`] instead. - pub fn files(mut self, files: impl IntoIterator) -> Self { + pub fn files(mut self, files: impl IntoIterator>) -> Self { self.attachments = EditAttachments::new(); self.add_files(files) } /// Set the allowed mentions for the message. - pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions<'a>) -> Self { self.allowed_mentions = Some(allowed_mentions); self } @@ -224,8 +211,8 @@ impl ExecuteWebhook { /// /// [`WebhookType::Application`]: crate::model::webhook::WebhookType /// [`WebhookType::Incoming`]: crate::model::webhook::WebhookType - pub fn components(mut self, components: Vec) -> Self { - self.components = Some(components); + pub fn components(mut self, components: impl Into]>>) -> Self { + self.components = Some(components.into()); self } super::button_and_select_menu_convenience_methods!(self.components); @@ -235,13 +222,13 @@ impl ExecuteWebhook { /// Refer to the [struct-level documentation] for an example on how to use embeds. /// /// [struct-level documentation]: #examples - pub fn embed(self, embed: CreateEmbed) -> Self { + pub fn embed(self, embed: CreateEmbed<'a>) -> Self { self.embeds(vec![embed]) } /// Set multiple embeds for the message. - pub fn embeds(mut self, embeds: Vec) -> Self { - self.embeds = embeds; + pub fn embeds(mut self, embeds: impl Into]>>) -> Self { + self.embeds = embeds.into(); self } @@ -296,7 +283,7 @@ impl ExecuteWebhook { /// # Ok(()) /// # } /// ``` - pub fn username(mut self, username: impl Into) -> Self { + pub fn username(mut self, username: impl Into>) -> Self { self.username = Some(username.into()); self } @@ -333,18 +320,10 @@ impl ExecuteWebhook { } /// Name of thread to create (requires the webhook channel to be a forum channel) - pub fn thread_name(mut self, thread_name: String) -> Self { + pub fn thread_name(mut self, thread_name: Cow<'a, str>) -> Self { self.thread_name = Some(thread_name); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for ExecuteWebhook { - type Context<'ctx> = (WebhookId, &'ctx str, bool); - type Built = Option; - /// Executes the webhook with the given content. /// /// # Errors @@ -353,20 +332,22 @@ impl Builder for ExecuteWebhook { /// execution is attempted in a thread not belonging to the webhook's [`Channel`]. /// /// Returns [`Error::Json`] if there is an error in deserialising Discord's response. - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( mut self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { + http: &Http, + webhook_id: WebhookId, + webhook_token: &str, + wait: bool, + ) -> Result> { self.check_length()?; let files = self.attachments.take_files(); - let http = cache_http.http(); if self.allowed_mentions.is_none() { self.allowed_mentions.clone_from(&http.default_allowed_mentions); } - http.execute_webhook(ctx.0, self.thread_id, ctx.1, ctx.2, files, &self).await + http.execute_webhook(webhook_id, self.thread_id, webhook_token, wait, files, &self).await } } diff --git a/src/builder/get_messages.rs b/src/builder/get_messages.rs index 9b40fac9449..89b6c07ae61 100644 --- a/src/builder/get_messages.rs +++ b/src/builder/get_messages.rs @@ -1,5 +1,5 @@ -#[cfg(feature = "http")] -use super::Builder; +use nonmax::NonMaxU8; + #[cfg(feature = "http")] use crate::http::{CacheHttp, MessagePagination}; #[cfg(feature = "http")] @@ -50,7 +50,7 @@ use crate::model::prelude::*; #[must_use] pub struct GetMessages { search_filter: Option, - limit: Option, + limit: Option, } impl GetMessages { @@ -60,21 +60,21 @@ impl GetMessages { } /// Indicates to retrieve the messages after a specific message, given its Id. - pub fn after(mut self, message_id: impl Into) -> Self { - self.search_filter = Some(SearchFilter::After(message_id.into())); + pub fn after(mut self, message_id: MessageId) -> Self { + self.search_filter = Some(SearchFilter::After(message_id)); self } /// Indicates to retrieve the messages _around_ a specific message, in other words in either /// direction from the message in time. - pub fn around(mut self, message_id: impl Into) -> Self { - self.search_filter = Some(SearchFilter::Around(message_id.into())); + pub fn around(mut self, message_id: MessageId) -> Self { + self.search_filter = Some(SearchFilter::Around(message_id)); self } /// Indicates to retrieve the messages before a specific message, given its Id. - pub fn before(mut self, message_id: impl Into) -> Self { - self.search_filter = Some(SearchFilter::Before(message_id.into())); + pub fn before(mut self, message_id: MessageId) -> Self { + self.search_filter = Some(SearchFilter::Before(message_id)); self } @@ -85,19 +85,15 @@ impl GetMessages { /// **Note**: This field is capped to 100 messages due to a Discord limitation. If an amount /// larger than 100 is supplied, it will be truncated. pub fn limit(mut self, limit: u8) -> Self { - self.limit = Some(limit.min(100)); + self.limit = NonMaxU8::new(limit.min(100)); self } -} - -#[cfg(feature = "http")] -#[async_trait::async_trait] -impl Builder for GetMessages { - type Context<'ctx> = ChannelId; - type Built = Vec; /// Gets messages from the channel. /// + /// If the cache is enabled, this method will fill up the message cache for the channel, if the + /// messages returned are newer than the existing cached messages or the cache is not full yet. + /// /// **Note**: If the user does not have the [Read Message History] permission, returns an empty /// [`Vec`]. /// @@ -106,12 +102,22 @@ impl Builder for GetMessages { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - async fn execute( + #[cfg(feature = "http")] + pub async fn execute( self, cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - cache_http.http().get_messages(ctx, self.search_filter.map(Into::into), self.limit).await + channel_id: ChannelId, + ) -> Result> { + let http = cache_http.http(); + let search_filter = self.search_filter.map(Into::into); + let messages = http.get_messages(channel_id, search_filter, self.limit).await?; + + #[cfg(feature = "cache")] + if let Some(cache) = cache_http.cache() { + cache.fill_message_cache(channel_id, messages.iter().cloned()); + } + + Ok(messages) } } diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 52b89e43fde..149e012548a 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -7,33 +7,32 @@ // #[serde(skip_serializing_if = "Option::is_none")] #![allow(clippy::option_option)] -#[cfg(feature = "http")] -use crate::http::CacheHttp; #[cfg(feature = "http")] use crate::internal::prelude::*; - -/// Common trait for all HTTP request builders in this module. #[cfg(feature = "http")] -#[async_trait::async_trait] -pub trait Builder { - /// Additional data that's only required when sending a request off to the API. - type Context<'ctx>; - type Built; - /// Serializes a builder's fields and sends the request off the API, returning the response. - async fn execute( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result; -} +use crate::model::ModelError; #[cfg(feature = "http")] -pub(crate) fn check_overflow(len: usize, max: usize) -> StdResult<(), usize> { - if len > max { - Err(len - max) - } else { - Ok(()) +pub(crate) fn check_lengths( + content: Option<&str>, + embeds: Option<&[CreateEmbed<'_>]>, + stickers: usize, +) -> StdResult<(), ModelError> { + use crate::model::error::Maximum; + + if let Some(content) = content { + Maximum::MessageLength.check_overflow(content.chars().count())?; } + + if let Some(embeds) = embeds { + Maximum::EmbedCount.check_overflow(embeds.len())?; + + for embed in embeds { + Maximum::EmbedLength.check_overflow(embed.get_length())?; + } + } + + Maximum::StickerCount.check_overflow(stickers) } mod add_member; @@ -124,8 +123,8 @@ macro_rules! button_and_select_menu_convenience_methods { /// /// Convenience method that wraps [`Self::components`]. Arranges buttons in action rows /// automatically. - pub fn button(mut $self, button: super::CreateButton) -> Self { - let rows = $self$(.$components_path)+.get_or_insert_with(Vec::new); + pub fn button(mut $self, button: super::CreateButton<'a>) -> Self { + let rows = $self$(.$components_path)+.get_or_insert_with(Cow::default).to_mut(); let row_with_space_left = rows.last_mut().and_then(|row| match row { super::CreateActionRow::Buttons(buttons) if buttons.len() < 5 => Some(buttons), _ => None, @@ -140,9 +139,10 @@ macro_rules! button_and_select_menu_convenience_methods { /// Adds an interactive select menu to this message. /// /// Convenience method that wraps [`Self::components`]. - pub fn select_menu(mut $self, select_menu: super::CreateSelectMenu) -> Self { + pub fn select_menu(mut $self, select_menu: super::CreateSelectMenu<'a>) -> Self { $self$(.$components_path)+ - .get_or_insert_with(Vec::new) + .get_or_insert_with(Cow::default) + .to_mut() .push(super::CreateActionRow::SelectMenu(select_menu)); $self } diff --git a/src/cache/cache_update.rs b/src/cache/cache_update.rs index c05faf4efd5..7945649ab7b 100644 --- a/src/cache/cache_update.rs +++ b/src/cache/cache_update.rs @@ -13,7 +13,7 @@ use super::Cache; /// ```rust,ignore /// use std::collections::hash_map::Entry; /// -/// use serenity::json::json; +/// use serde_json::json; /// use serenity::cache::{Cache, CacheUpdate}; /// use serenity::model::id::UserId; /// use serenity::model::user::User; @@ -56,7 +56,7 @@ use super::Cache; /// Some(old_user) /// }, /// Entry::Vacant(entry) => { -/// // We can convert a [`json::Value`] to a User for test +/// // We can convert a [`Value`] to a User for test /// // purposes. /// let user = from_value::(json!({ /// "id": self.user_id, diff --git a/src/cache/event.rs b/src/cache/event.rs index ff57e00ed35..62675f0a8f1 100644 --- a/src/cache/event.rs +++ b/src/cache/event.rs @@ -1,6 +1,8 @@ -use std::collections::HashSet; +use std::collections::{HashSet, VecDeque}; +use std::num::NonZeroU16; use super::{Cache, CacheUpdate}; +use crate::internal::prelude::*; use crate::model::channel::{GuildChannel, Message}; use crate::model::event::{ ChannelCreateEvent, @@ -30,8 +32,8 @@ use crate::model::event::{ VoiceChannelStatusUpdateEvent, VoiceStateUpdateEvent, }; -use crate::model::gateway::ShardInfo; -use crate::model::guild::{Guild, GuildMemberFlags, Member, Role}; +use crate::model::gateway::{Presence, ShardInfo}; +use crate::model::guild::{Guild, GuildMemberFlags, Member, MemberGeneratedFlags, Role}; use crate::model::id::ShardId; use crate::model::user::{CurrentUser, OnlineStatus}; use crate::model::voice::VoiceState; @@ -43,24 +45,22 @@ impl CacheUpdate for ChannelCreateEvent { let old_channel = cache .guilds .get_mut(&self.channel.guild_id) - .and_then(|mut g| g.channels.insert(self.channel.id, self.channel.clone())); + .and_then(|mut g| g.channels.insert(self.channel.clone())); - cache.channels.insert(self.channel.id, self.channel.guild_id); old_channel } } impl CacheUpdate for ChannelDeleteEvent { - type Output = Vec; + type Output = VecDeque; - fn update(&mut self, cache: &Cache) -> Option> { + fn update(&mut self, cache: &Cache) -> Option> { let (channel_id, guild_id) = (self.channel.id, self.channel.guild_id); - cache.channels.remove(&channel_id); cache.guilds.get_mut(&guild_id).map(|mut g| g.channels.remove(&channel_id)); // Remove the cached messages for the channel. - cache.messages.remove(&channel_id).map(|(_, messages)| messages.into_values().collect()) + cache.messages.remove(&channel_id).map(|(_, messages)| messages) } } @@ -68,12 +68,10 @@ impl CacheUpdate for ChannelUpdateEvent { type Output = GuildChannel; fn update(&mut self, cache: &Cache) -> Option { - cache.channels.insert(self.channel.id, self.channel.guild_id); - cache .guilds .get_mut(&self.channel.guild_id) - .and_then(|mut g| g.channels.insert(self.channel.id, self.channel.clone())) + .and_then(|mut g| g.channels.insert(self.channel.clone())) } } @@ -83,7 +81,7 @@ impl CacheUpdate for ChannelPinsUpdateEvent { fn update(&mut self, cache: &Cache) -> Option<()> { if let Some(guild_id) = self.guild_id { if let Some(mut guild) = cache.guilds.get_mut(&guild_id) { - if let Some(channel) = guild.channels.get_mut(&self.channel_id) { + if let Some(mut channel) = guild.channels.get_mut(&self.channel_id) { channel.last_pin_timestamp = self.last_pin_timestamp; } } @@ -98,19 +96,9 @@ impl CacheUpdate for GuildCreateEvent { fn update(&mut self, cache: &Cache) -> Option<()> { cache.unavailable_guilds.remove(&self.guild.id); - let mut guild = self.guild.clone(); - - for (user_id, member) in &mut guild.members { - cache.update_user_entry(&member.user); - if let Some(u) = cache.user(user_id) { - member.user = u.clone(); - } - } + let guild = self.guild.clone(); cache.guilds.insert(self.guild.id, guild); - for channel_id in self.guild.channels.keys() { - cache.channels.insert(*channel_id, self.guild.id); - } None } @@ -129,12 +117,9 @@ impl CacheUpdate for GuildDeleteEvent { match cache.guilds.remove(&self.guild.id) { Some(guild) => { - for channel_id in guild.1.channels.keys() { - // Remove the channel from the cache. - cache.channels.remove(channel_id); - + for channel in &guild.1.channels { // Remove the channel's cached messages. - cache.messages.remove(channel_id); + cache.messages.remove(&channel.id); } Some(guild.1) @@ -160,15 +145,9 @@ impl CacheUpdate for GuildMemberAddEvent { type Output = (); fn update(&mut self, cache: &Cache) -> Option<()> { - let user_id = self.member.user.id; - cache.update_user_entry(&self.member.user); - if let Some(u) = cache.user(user_id) { - self.member.user = u.clone(); - } - if let Some(mut guild) = cache.guilds.get_mut(&self.member.guild_id) { guild.member_count += 1; - guild.members.insert(user_id, self.member.clone()); + guild.members.insert(self.member.clone()); } None @@ -192,23 +171,21 @@ impl CacheUpdate for GuildMemberUpdateEvent { type Output = Member; fn update(&mut self, cache: &Cache) -> Option { - cache.update_user_entry(&self.user); - if let Some(mut guild) = cache.guilds.get_mut(&self.guild_id) { - let item = if let Some(member) = guild.members.get_mut(&self.user.id) { + let item = if let Some(mut member) = guild.members.get_mut(&self.user.id) { let item = Some(member.clone()); member.joined_at.clone_from(&Some(self.joined_at)); member.nick.clone_from(&self.nick); member.roles.clone_from(&self.roles); member.user.clone_from(&self.user); - member.pending.clone_from(&self.pending); member.premium_since.clone_from(&self.premium_since); - member.deaf.clone_from(&self.deaf); - member.mute.clone_from(&self.mute); member.avatar.clone_from(&self.avatar); member.communication_disabled_until.clone_from(&self.communication_disabled_until); member.unusual_dm_activity_until.clone_from(&self.unusual_dm_activity_until); + member.set_pending(self.pending()); + member.set_deaf(self.deaf()); + member.set_mute(self.mute()); item } else { @@ -216,22 +193,26 @@ impl CacheUpdate for GuildMemberUpdateEvent { }; if item.is_none() { - guild.members.insert(self.user.id, Member { - deaf: false, + let mut new_member = Member { + __generated_flags: MemberGeneratedFlags::empty(), guild_id: self.guild_id, joined_at: Some(self.joined_at), - mute: false, nick: self.nick.clone(), roles: self.roles.clone(), user: self.user.clone(), - pending: self.pending, premium_since: self.premium_since, permissions: None, avatar: self.avatar, communication_disabled_until: self.communication_disabled_until, flags: GuildMemberFlags::default(), unusual_dm_activity_until: self.unusual_dm_activity_until, - }); + }; + + new_member.set_pending(self.pending()); + new_member.set_deaf(self.deaf()); + new_member.set_mute(self.mute()); + + guild.members.insert(new_member); } item @@ -245,10 +226,6 @@ impl CacheUpdate for GuildMembersChunkEvent { type Output = (); fn update(&mut self, cache: &Cache) -> Option<()> { - for member in self.members.values() { - cache.update_user_entry(&member.user); - } - if let Some(mut g) = cache.guilds.get_mut(&self.guild_id) { g.members.extend(self.members.clone()); } @@ -261,11 +238,7 @@ impl CacheUpdate for GuildRoleCreateEvent { type Output = (); fn update(&mut self, cache: &Cache) -> Option<()> { - cache - .guilds - .get_mut(&self.role.guild_id) - .map(|mut g| g.roles.insert(self.role.id, self.role.clone())); - + cache.guilds.get_mut(&self.role.guild_id).map(|mut g| g.roles.insert(self.role.clone())); None } } @@ -283,8 +256,8 @@ impl CacheUpdate for GuildRoleUpdateEvent { fn update(&mut self, cache: &Cache) -> Option { if let Some(mut guild) = cache.guilds.get_mut(&self.role.guild_id) { - if let Some(role) = guild.roles.get_mut(&self.role.id) { - return Some(std::mem::replace(role, self.role.clone())); + if let Some(mut role) = guild.roles.get_mut(&self.role.id) { + return Some(std::mem::replace(&mut *role, self.role.clone())); } } @@ -334,7 +307,7 @@ impl CacheUpdate for GuildUpdateEvent { guild.system_channel_id = self.guild.system_channel_id; guild.verification_level = self.guild.verification_level; guild.widget_channel_id = self.guild.widget_channel_id; - guild.widget_enabled = self.guild.widget_enabled; + guild.set_widget_enabled(self.guild.widget_enabled()); } None @@ -350,9 +323,14 @@ impl CacheUpdate for MessageCreateEvent { let guild = self.message.guild_id.and_then(|g_id| cache.guilds.get_mut(&g_id)); if let Some(mut guild) = guild { - if let Some(channel) = guild.channels.get_mut(&self.message.channel_id) { - update_channel_last_message_id(&self.message, channel, cache); - } else { + let mut found_channel = false; + if let Some(mut channel) = guild.channels.get_mut(&self.message.channel_id) { + update_channel_last_message_id(&self.message, &mut channel, cache); + found_channel = true; + } + + // found_channel is to avoid limitations of the NLL borrow checker. + if !found_channel { // This may be a thread. let thread = guild.threads.iter_mut().find(|thread| thread.id == self.message.channel_id); @@ -370,18 +348,15 @@ impl CacheUpdate for MessageCreateEvent { } let mut messages = cache.messages.entry(self.message.channel_id).or_default(); - let mut queue = cache.message_queue.entry(self.message.channel_id).or_default(); let mut removed_msg = None; - if messages.len() == max { - if let Some(id) = queue.pop_front() { - removed_msg = messages.remove(&id); - } + removed_msg = messages.pop_front(); } - queue.push_back(self.message.id); - messages.insert(self.message.id, self.message.clone()); + if !messages.iter().any(|m| m.id == self.message.id) { + messages.push_back(self.message.clone()); + } removed_msg } @@ -406,56 +381,54 @@ impl CacheUpdate for MessageUpdateEvent { type Output = Message; fn update(&mut self, cache: &Cache) -> Option { - let mut messages = cache.messages.get_mut(&self.channel_id)?; - let message = messages.get_mut(&self.id)?; - let old_message = message.clone(); - - self.apply_to_message(message); + for message in cache.messages.get_mut(&self.channel_id)?.iter_mut() { + if message.id == self.id { + let old_message = message.clone(); + self.apply_to_message(message); + return Some(old_message); + } + } - Some(old_message) + None } } impl CacheUpdate for PresenceUpdateEvent { - type Output = (); - - fn update(&mut self, cache: &Cache) -> Option<()> { - if let Some(user) = self.presence.user.to_user() { - cache.update_user_entry(&user); - } - - if let Some(user) = cache.user(self.presence.user.id) { - self.presence.user.update_with_user(&user); - } + type Output = Presence; + fn update(&mut self, cache: &Cache) -> Option { if let Some(guild_id) = self.presence.guild_id { if let Some(mut guild) = cache.guilds.get_mut(&guild_id) { + let old = guild.presences.get(&self.presence.user.id).cloned(); + // If the member went offline, remove them from the presence list. if self.presence.status == OnlineStatus::Offline { guild.presences.remove(&self.presence.user.id); } else { - guild.presences.insert(self.presence.user.id, self.presence.clone()); + guild.presences.insert(self.presence.clone()); } // Create a partial member instance out of the presence update data. if let Some(user) = self.presence.user.to_user() { - guild.members.entry(self.presence.user.id).or_insert_with(|| Member { - deaf: false, - guild_id, - joined_at: None, - mute: false, - nick: None, - user, - roles: vec![], - pending: false, - premium_since: None, - permissions: None, - avatar: None, - communication_disabled_until: None, - flags: GuildMemberFlags::default(), - unusual_dm_activity_until: None, - }); + if !guild.members.contains_key(&self.presence.user.id) { + guild.members.insert(Member { + guild_id, + joined_at: None, + nick: None, + user, + roles: FixedArray::default(), + premium_since: None, + permissions: None, + avatar: None, + communication_disabled_until: None, + flags: GuildMemberFlags::default(), + unusual_dm_activity_until: None, + __generated_flags: MemberGeneratedFlags::empty(), + }); + } } + + return old; } } @@ -467,9 +440,7 @@ impl CacheUpdate for ReadyEvent { type Output = (); fn update(&mut self, cache: &Cache) -> Option<()> { - let ready = self.ready.clone(); - - for unavailable in ready.guilds { + for unavailable in &self.ready.guilds { cache.guilds.remove(&unavailable.id); cache.unavailable_guilds.insert(unavailable.id, ()); } @@ -478,7 +449,8 @@ impl CacheUpdate for ReadyEvent { let mut guilds_to_remove = vec![]; let ready_guilds_hashset = self.ready.guilds.iter().map(|status| status.id).collect::>(); - let shard_data = self.ready.shard.unwrap_or_else(|| ShardInfo::new(ShardId(1), 1)); + let shard_data = + self.ready.shard.unwrap_or_else(|| ShardInfo::new(ShardId(1), NonZeroU16::MIN)); for guild_entry in cache.guilds.iter() { let guild = guild_entry.key(); @@ -500,7 +472,7 @@ impl CacheUpdate for ReadyEvent { cached_shard_data.total = shard_data.total; cached_shard_data.connected.insert(shard_data.id); } - *cache.user.write() = ready.user; + cache.user.write().clone_from(&self.ready.user); None } @@ -514,9 +486,15 @@ impl CacheUpdate for ThreadCreateEvent { cache.guilds.get_mut(&guild_id).and_then(|mut g| { if let Some(i) = g.threads.iter().position(|e| e.id == thread_id) { - Some(std::mem::replace(&mut g.threads[i], self.thread.clone())) + Some(std::mem::replace(&mut g.threads[i as u32], self.thread.clone())) } else { - g.threads.push(self.thread.clone()); + // This is a rare enough occurence to realloc. + let mut threads = std::mem::take(&mut g.threads).into_vec(); + threads.push(self.thread.clone()); + + g.threads = FixedArray::try_from(threads.into_boxed_slice()) + .expect("A guild should not have 4 billion threads"); + None } }) @@ -531,9 +509,15 @@ impl CacheUpdate for ThreadUpdateEvent { cache.guilds.get_mut(&guild_id).and_then(|mut g| { if let Some(i) = g.threads.iter().position(|e| e.id == thread_id) { - Some(std::mem::replace(&mut g.threads[i], self.thread.clone())) + Some(std::mem::replace(&mut g.threads[i as u32], self.thread.clone())) } else { - g.threads.push(self.thread.clone()); + // This is a rare enough occurence to realloc. + let mut threads = std::mem::take(&mut g.threads).into_vec(); + threads.push(self.thread.clone()); + + g.threads = FixedArray::try_from(threads.into_boxed_slice()) + .expect("A guild should not have 4 billion threads"); + None } }) @@ -547,7 +531,15 @@ impl CacheUpdate for ThreadDeleteEvent { let (guild_id, thread_id) = (self.thread.guild_id, self.thread.id); cache.guilds.get_mut(&guild_id).and_then(|mut g| { - g.threads.iter().position(|e| e.id == thread_id).map(|i| g.threads.remove(i)) + g.threads.iter().position(|e| e.id == thread_id).map(|i| { + let mut threads = std::mem::take(&mut g.threads).into_vec(); + let thread = threads.remove(i); + + g.threads = FixedArray::try_from(threads.into_boxed_slice()) + .expect("A guild should not have 4 billion threads"); + + thread + }) }) } } @@ -568,12 +560,14 @@ impl CacheUpdate for VoiceStateUpdateEvent { if let Some(guild_id) = self.voice_state.guild_id { if let Some(mut guild) = cache.guilds.get_mut(&guild_id) { if let Some(member) = &self.voice_state.member { - guild.members.insert(member.user.id, member.clone()); + guild.members.insert(member.clone()); } if self.voice_state.channel_id.is_some() { // Update or add to the voice state list - guild.voice_states.insert(self.voice_state.user_id, self.voice_state.clone()) + let old_state = guild.voice_states.remove(&self.voice_state.user_id); + guild.voice_states.insert(self.voice_state.clone()); + old_state } else { // Remove the user from the voice state list guild.voice_states.remove(&self.voice_state.user_id) @@ -588,11 +582,11 @@ impl CacheUpdate for VoiceStateUpdateEvent { } impl CacheUpdate for VoiceChannelStatusUpdateEvent { - type Output = String; + type Output = FixedString; fn update(&mut self, cache: &Cache) -> Option { let mut guild = cache.guilds.get_mut(&self.guild_id)?; - let channel = guild.channels.get_mut(&self.id)?; + let mut channel = guild.channels.get_mut(&self.id)?; let old = channel.status.clone(); channel.status.clone_from(&self.status); diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 95a978d5506..0b591070691 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -26,18 +26,17 @@ use std::collections::{HashSet, VecDeque}; use std::hash::Hash; +use std::num::NonZeroU16; #[cfg(feature = "temp_cache")] use std::sync::Arc; #[cfg(feature = "temp_cache")] use std::time::Duration; -use dashmap::mapref::entry::Entry; use dashmap::mapref::one::{MappedRef, Ref}; use dashmap::DashMap; #[cfg(feature = "temp_cache")] use mini_moka::sync::Cache as MokaCache; use parking_lot::RwLock; -use tracing::instrument; pub use self::cache_update::CacheUpdate; pub use self::settings::Settings; @@ -52,8 +51,6 @@ mod wrappers; pub(crate) use wrappers::MaybeOwnedArc; use wrappers::{BuildHasher, MaybeMap, ReadOnlyMapRef}; -type MessageCache = DashMap, BuildHasher>; - struct NotSend; enum CacheRefInner<'a, K, V, T> { @@ -109,24 +106,17 @@ impl std::ops::Deref for CacheRef<'_, K, V, T> { } } -type MappedGuildRef<'a, T> = CacheRef<'a, GuildId, T, Guild>; - pub type UserRef<'a> = CacheRef<'a, UserId, User>; -pub type MemberRef<'a> = MappedGuildRef<'a, Member>; pub type GuildRef<'a> = CacheRef<'a, GuildId, Guild>; -pub type GuildRoleRef<'a> = MappedGuildRef<'a, Role>; pub type SettingsRef<'a> = CacheRef<'a, (), Settings>; pub type CurrentUserRef<'a> = CacheRef<'a, (), CurrentUser>; -pub type GuildChannelRef<'a> = MappedGuildRef<'a, GuildChannel>; -pub type GuildRolesRef<'a> = MappedGuildRef<'a, HashMap>; -pub type GuildChannelsRef<'a> = MappedGuildRef<'a, HashMap>; -pub type ChannelMessagesRef<'a> = CacheRef<'a, ChannelId, HashMap>; -pub type MessageRef<'a> = CacheRef<'a, ChannelId, Message, HashMap>; +pub type ChannelMessagesRef<'a> = CacheRef<'a, ChannelId, VecDeque>; +pub type MessageRef<'a> = CacheRef<'a, ChannelId, Message, VecDeque>; #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Debug)] pub(crate) struct CachedShardData { - pub total: u32, + pub total: NonZeroU16, pub connected: HashSet, pub has_sent_shards_ready: bool, } @@ -176,10 +166,6 @@ pub struct Cache { #[cfg(feature = "temp_cache")] pub(crate) temp_users: MokaCache, BuildHasher>, - // Channels cache: - /// A map of channel ids to the guilds in which the channel data is stored. - pub(crate) channels: MaybeMap, - // Guilds cache: // --- /// A map of guilds with full data available. This includes data like [`Role`]s and [`Emoji`]s @@ -191,32 +177,9 @@ pub struct Cache { /// are "sent in" over time through the receiving of [`Event::GuildCreate`]s. pub(crate) unavailable_guilds: MaybeMap, - // Users cache: - // --- - /// A map of users that the current user sees. - /// - /// Users are added to - and updated from - this map via the following received events: - /// - /// - [`GuildMemberAdd`][`GuildMemberAddEvent`] - /// - [`GuildMemberRemove`][`GuildMemberRemoveEvent`] - /// - [`GuildMembersChunk`][`GuildMembersChunkEvent`] - /// - [`PresenceUpdate`][`PresenceUpdateEvent`] - /// - [`Ready`][`ReadyEvent`] - /// - /// Note, however, that users are _not_ removed from the map on removal events such as - /// [`GuildMemberRemove`][`GuildMemberRemoveEvent`], as other structs such as members or - /// recipients may still exist. - pub(crate) users: MaybeMap, - // Messages cache: // --- - pub(crate) messages: MessageCache, - /// Queue of message IDs for each channel. - /// - /// This is simply a vecdeque so we can keep track of the order of messages inserted into the - /// cache. When a maximum number of messages are in a channel's cache, we can pop the front and - /// remove that ID from the cache. - pub(crate) message_queue: DashMap, BuildHasher>, + pub(crate) messages: DashMap, BuildHasher>, // Miscellanous fixed-size data // --- @@ -235,7 +198,6 @@ pub struct Cache { impl Cache { /// Creates a new cache. - #[inline] #[must_use] pub fn new() -> Self { Self::default() @@ -253,7 +215,7 @@ impl Cache { /// /// let cache = Cache::new_with_settings(settings); /// ``` - #[instrument] + #[cfg_attr(feature = "tracing_instrument", instrument)] pub fn new_with_settings(settings: Settings) -> Self { #[cfg(feature = "temp_cache")] fn temp_cache(ttl: Duration) -> MokaCache @@ -274,18 +236,13 @@ impl Cache { #[cfg(feature = "temp_cache")] temp_users: temp_cache(settings.time_to_live), - channels: MaybeMap(settings.cache_channels.then(DashMap::default)), - guilds: MaybeMap(settings.cache_guilds.then(DashMap::default)), unavailable_guilds: MaybeMap(settings.cache_guilds.then(DashMap::default)), - users: MaybeMap(settings.cache_users.then(DashMap::default)), - messages: DashMap::default(), - message_queue: DashMap::default(), shard_data: RwLock::new(CachedShardData { - total: 1, + total: NonZeroU16::MIN, connected: HashSet::new(), has_sent_shards_ready: false, }), @@ -370,31 +327,6 @@ impl Cache { self.guilds.iter().map(|i| *i.key()).chain(unavailable_guild_ids).collect() } - /// Retrieves a [`GuildChannel`] from the cache based on the given Id. - #[inline] - #[deprecated = "Use Cache::guild and Guild::channels instead"] - pub fn channel>(&self, id: C) -> Option> { - self._channel(id.into()) - } - - fn _channel(&self, id: ChannelId) -> Option> { - let guild_id = *self.channels.get(&id)?; - let guild_ref = self.guilds.get(&guild_id)?; - let channel = guild_ref.try_map(|g| g.channels.get(&id)).ok(); - if let Some(channel) = channel { - return Some(CacheRef::from_mapped_ref(channel)); - } - - #[cfg(feature = "temp_cache")] - { - if let Some(channel) = self.temp_channels.get(&id) { - return Some(CacheRef::from_arc(channel)); - } - } - - None - } - /// Get a reference to the cached messages for a channel based on the given `Id`. /// /// # Examples @@ -402,17 +334,16 @@ impl Cache { /// Find all messages by user ID 8 in channel ID 7: /// /// ```rust,no_run + /// # use serenity::model::id::ChannelId; + /// # /// # let cache: serenity::cache::Cache = todo!(); - /// let messages_in_channel = cache.channel_messages(7); - /// let messages_by_user = messages_in_channel - /// .as_ref() - /// .map(|msgs| msgs.values().filter(|m| m.author.id == 8).collect::>()); + /// if let Some(messages_in_channel) = cache.channel_messages(ChannelId::new(7)) { + /// let messages_by_user: Vec<_> = + /// messages_in_channel.iter().filter(|m| m.author.id == 8).collect(); + /// } /// ``` - pub fn channel_messages( - &self, - channel_id: impl Into, - ) -> Option> { - self.messages.get(&channel_id.into()).map(CacheRef::from_ref) + pub fn channel_messages(&self, channel_id: ChannelId) -> Option> { + self.messages.get(&channel_id).map(CacheRef::from_ref) } /// Gets a reference to a guild from the cache based on the given `id`. @@ -423,19 +354,15 @@ impl Cache { /// /// ```rust,no_run /// # use serenity::cache::Cache; + /// # use serenity::model::id::GuildId; /// # /// # let cache = Cache::default(); /// // assuming the cache is in scope, e.g. via `Context` - /// if let Some(guild) = cache.guild(7) { + /// if let Some(guild) = cache.guild(GuildId::new(7)) { /// println!("Guild name: {}", guild.name); /// }; /// ``` - #[inline] - pub fn guild>(&self, id: G) -> Option> { - self._guild(id.into()) - } - - fn _guild(&self, id: GuildId) -> Option> { + pub fn guild(&self, id: GuildId) -> Option> { self.guilds.get(&id).map(CacheRef::from_ref) } @@ -444,101 +371,13 @@ impl Cache { self.guilds.len() } - /// Retrieves a [`Guild`]'s member from the cache based on the guild's and user's given Ids. - /// - /// # Examples - /// - /// Retrieving the member object of the user that posted a message, in a - /// [`EventHandler::message`] context: - /// - /// ```rust,no_run - /// # use serenity::cache::Cache; - /// # use serenity::http::Http; - /// # use serenity::model::channel::Message; - /// # - /// # async fn run(http: Http, cache: Cache, message: Message) { - /// # - /// let roles_len = { - /// let channel = match cache.channel(message.channel_id) { - /// Some(channel) => channel, - /// None => { - /// if let Err(why) = message.channel_id.say(http, "Error finding channel data").await { - /// println!("Error sending message: {:?}", why); - /// } - /// return; - /// }, - /// }; - /// - /// cache.member(channel.guild_id, message.author.id).map(|m| m.roles.len()) - /// }; - /// - /// let message_res = if let Some(roles_len) = roles_len { - /// let msg = format!("You have {} roles", roles_len); - /// message.channel_id.say(&http, &msg).await - /// } else { - /// message.channel_id.say(&http, "Error finding member data").await - /// }; - /// - /// if let Err(why) = message_res { - /// println!("Error sending message: {:?}", why); - /// } - /// # } - /// ``` - /// - /// [`EventHandler::message`]: crate::client::EventHandler::message - /// [`members`]: crate::model::guild::Guild::members - #[inline] - #[deprecated = "Use Cache::guild and Guild::members instead"] - pub fn member( - &self, - guild_id: impl Into, - user_id: impl Into, - ) -> Option> { - self._member(guild_id.into(), user_id.into()) - } - - fn _member(&self, guild_id: GuildId, user_id: UserId) -> Option> { - let member = self.guilds.get(&guild_id)?.try_map(|g| g.members.get(&user_id)).ok()?; - Some(CacheRef::from_mapped_ref(member)) - } - - #[inline] - #[deprecated = "Use Cache::guild and Guild::roles instead"] - pub fn guild_roles(&self, guild_id: impl Into) -> Option> { - self._guild_roles(guild_id.into()) - } - - fn _guild_roles(&self, guild_id: GuildId) -> Option> { - let roles = self.guilds.get(&guild_id)?.map(|g| &g.roles); - Some(CacheRef::from_mapped_ref(roles)) - } - /// This method clones and returns all unavailable guilds. - #[inline] pub fn unavailable_guilds(&self) -> ReadOnlyMapRef<'_, GuildId, ()> { self.unavailable_guilds.as_read_only() } - /// This method returns all channels from a guild of with the given `guild_id`. - #[inline] - #[deprecated = "Use Cache::guild and Guild::channels instead"] - pub fn guild_channels(&self, guild_id: impl Into) -> Option> { - self._guild_channels(guild_id.into()) - } - - fn _guild_channels(&self, guild_id: GuildId) -> Option> { - let channels = self.guilds.get(&guild_id)?.map(|g| &g.channels); - Some(CacheRef::from_mapped_ref(channels)) - } - - /// Returns the number of guild channels in the cache. - pub fn guild_channel_count(&self) -> usize { - self.channels.len() - } - /// Returns the number of shards. - #[inline] - pub fn shard_count(&self) -> u32 { + pub fn shard_count(&self) -> NonZeroU16 { self.shard_data.read().total } @@ -565,62 +404,18 @@ impl Cache { /// ``` /// /// [`EventHandler::message`]: crate::client::EventHandler::message - #[inline] - pub fn message(&self, channel_id: C, message_id: M) -> Option> - where - C: Into, - M: Into, - { - self._message(channel_id.into(), message_id.into()) - } - - fn _message(&self, channel_id: ChannelId, message_id: MessageId) -> Option> { + pub fn message(&self, channel_id: ChannelId, message_id: MessageId) -> Option> { #[cfg(feature = "temp_cache")] if let Some(message) = self.temp_messages.get(&message_id) { return Some(CacheRef::from_arc(message)); } - let channel_messages = self.messages.get(&channel_id)?; - let message = channel_messages.try_map(|messages| messages.get(&message_id)).ok()?; + let messages = self.messages.get(&channel_id)?; + let message = + messages.try_map(|messages| messages.iter().find(|m| m.id == message_id)).ok()?; Some(CacheRef::from_mapped_ref(message)) } - /// Retrieves a [`Guild`]'s role by their Ids. - /// - /// **Note**: This will clone the entire role. Instead, retrieve the guild and retrieve from - /// the guild's [`roles`] map to avoid this. - /// - /// # Examples - /// - /// Retrieve a role from the cache and print its name: - /// - /// ```rust,no_run - /// # use serenity::cache::Cache; - /// # - /// # let cache = Cache::default(); - /// // assuming the cache is in scope, e.g. via `Context` - /// if let Some(role) = cache.role(7, 77) { - /// println!("Role with Id 77 is called {}", role.name); - /// }; - /// ``` - /// - /// [`Guild`]: crate::model::guild::Guild - /// [`roles`]: crate::model::guild::Guild::roles - #[inline] - #[deprecated = "Use Cache::guild and Guild::roles instead"] - pub fn role(&self, guild_id: G, role_id: R) -> Option> - where - G: Into, - R: Into, - { - self._role(guild_id.into(), role_id.into()) - } - - fn _role(&self, guild_id: GuildId, role_id: RoleId) -> Option> { - let role = self.guilds.get(&guild_id)?.try_map(|g| g.roles.get(&role_id)).ok()?; - Some(CacheRef::from_mapped_ref(role)) - } - /// Returns the settings. /// /// # Examples @@ -646,92 +441,46 @@ impl Cache { self.settings.write().max_messages = max; } - /// Retrieves a [`User`] from the cache's [`Self::users`] map, if it exists. - /// - /// The only advantage of this method is that you can pass in anything that is indirectly a - /// [`UserId`]. - /// - /// # Examples - /// - /// Retrieve a user from the cache and print their name: - /// - /// ```rust,no_run - /// # use serenity::client::Context; - /// # - /// # async fn test(context: &Context) -> Result<(), Box> { - /// if let Some(user) = context.cache.user(7) { - /// println!("User with Id 7 is currently named {}", user.name); - /// } - /// # Ok(()) - /// # } - /// ``` - #[inline] - pub fn user>(&self, user_id: U) -> Option> { - self._user(user_id.into()) - } - - #[cfg(feature = "temp_cache")] - fn _user(&self, user_id: UserId) -> Option> { - if let Some(user) = self.users.get(&user_id) { - Some(CacheRef::from_ref(user)) - } else { - self.temp_users.get(&user_id).map(CacheRef::from_arc) - } - } - - #[cfg(not(feature = "temp_cache"))] - fn _user(&self, user_id: UserId) -> Option> { - self.users.get(&user_id).map(CacheRef::from_ref) - } - - /// Clones all users and returns them. - #[inline] - pub fn users(&self) -> ReadOnlyMapRef<'_, UserId, User> { - self.users.as_read_only() - } - - /// Returns the amount of cached users. - #[inline] - pub fn user_count(&self) -> usize { - self.users.len() - } - /// This method provides a reference to the user used by the bot. - #[inline] pub fn current_user(&self) -> CurrentUserRef<'_> { CacheRef::from_guard(self.user.read()) } - /// Returns a channel category matching the given ID - #[deprecated = "Use Cache::guild, Guild::channels, and GuildChannel::kind"] - pub fn category(&self, channel_id: ChannelId) -> Option> { - #[allow(deprecated)] - let channel = self.channel(channel_id)?; - if channel.kind == ChannelType::Category { - Some(channel) - } else { - None - } - } + /// Clones all channel categories in the given guild and returns them. + pub fn guild_categories( + &self, + guild_id: GuildId, + ) -> Option> { + let guild = self.guilds.get(&guild_id)?; - /// Returns the parent category of the given channel ID. - #[deprecated = "Use Cache::guild, Guild::channels, and GuildChannel::parent_id"] - pub fn channel_category_id(&self, channel_id: ChannelId) -> Option { - #[allow(deprecated)] - self.channel(channel_id)?.parent_id + let filter = |channel: &&GuildChannel| channel.kind == ChannelType::Category; + Some(guild.channels.iter().filter(filter).cloned().collect()) } - /// Clones all channel categories in the given guild and returns them. - pub fn guild_categories(&self, guild_id: GuildId) -> Option> { - let guild = self.guilds.get(&guild_id)?; - Some( - guild - .channels - .iter() - .filter(|(_id, channel)| channel.kind == ChannelType::Category) - .map(|(id, channel)| (*id, channel.clone())) - .collect(), - ) + /// Inserts new messages into the message cache for a channel manually. + /// + /// This will keep the ordering of the message cache consistent, even if the message iterator + /// contains randomly ordered messages, and respects the [`Settings::max_messages`] setting. + pub(crate) fn fill_message_cache( + &self, + channel_id: ChannelId, + new_messages: impl Iterator, + ) { + let max_messages = self.settings().max_messages; + if max_messages == 0 { + // Early exit for common case of message cache being disabled. + return; + } + + let mut channel_messages = self.messages.entry(channel_id).or_default(); + + // Fill up the existing cache + channel_messages.extend(new_messages.take(max_messages)); + // Make sure the cache stays sorted to messages + channel_messages.make_contiguous().sort_unstable_by_key(|m| m.id); + // Get rid of the overflow at the front of the queue. + let truncate_end_index = channel_messages.len().saturating_sub(max_messages); + channel_messages.drain(..truncate_end_index); } /// Updates the cache with the update implementation for an event or other custom update @@ -744,23 +493,10 @@ impl Cache { /// Refer to the [`CacheUpdate` examples]. /// /// [`CacheUpdate` examples]: CacheUpdate#examples - #[instrument(skip(self, e))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self, e)))] pub fn update(&self, e: &mut E) -> Option { e.update(self) } - - pub(crate) fn update_user_entry(&self, user: &User) { - if let Some(users) = &self.users.0 { - match users.entry(user.id) { - Entry::Vacant(e) => { - e.insert(user.clone()); - }, - Entry::Occupied(mut e) => { - e.get_mut().clone_from(user); - }, - } - } - } } impl Default for Cache { @@ -771,7 +507,6 @@ impl Default for Cache { #[cfg(test)] mod test { - use crate::cache::{Cache, CacheUpdate, Settings}; use crate::model::prelude::*; @@ -815,7 +550,7 @@ mod test { assert_eq!(channel.len(), 2); // Check that the first message is now removed. - assert!(!channel.contains_key(&MessageId::new(3))); + assert!(!channel.iter().any(|m| m.id == MessageId::new(3))); } let channel = GuildChannel { @@ -836,7 +571,7 @@ mod test { let mut guild_create = GuildCreateEvent { guild: Guild { id: GuildId::new(1), - channels: HashMap::from([(ChannelId::new(2), channel)]), + channels: ExtractMap::from_iter([channel]), ..Default::default() }, }; diff --git a/src/cache/settings.rs b/src/cache/settings.rs index 071499a6e1e..00de17f47d8 100644 --- a/src/cache/settings.rs +++ b/src/cache/settings.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - /// Settings for the cache. /// /// # Examples @@ -16,14 +14,15 @@ use std::time::Duration; #[derive(Clone, Debug)] #[non_exhaustive] pub struct Settings { + /// How long temporarily-cached data should be stored before being thrown out. + /// + /// Defaults to one hour. + #[cfg(feature = "temp_cache")] + pub time_to_live: std::time::Duration, /// The maximum number of messages to store in a channel's message cache. /// /// Defaults to 0. pub max_messages: usize, - /// How long temporarily-cached data should be stored before being thrown out. - /// - /// Defaults to one hour. - pub time_to_live: Duration, /// Whether to cache guild data received from gateway. /// /// Defaults to true. @@ -41,8 +40,9 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { Self { + #[cfg(feature = "temp_cache")] + time_to_live: std::time::Duration::from_secs(60 * 60), max_messages: 0, - time_to_live: Duration::from_secs(60 * 60), cache_guilds: true, cache_channels: true, cache_users: true, diff --git a/src/cache/wrappers.rs b/src/cache/wrappers.rs index 0b8e98c7ae9..e4d8d6ce604 100644 --- a/src/cache/wrappers.rs +++ b/src/cache/wrappers.rs @@ -39,7 +39,7 @@ impl MaybeMap { } pub fn len(&self) -> usize { - self.0.as_ref().map_or(0, |map| map.len()) + self.0.as_ref().map_or(0, DashMap::len) } pub fn shrink_to_fit(&self) { diff --git a/src/client/context.rs b/src/client/context.rs index 40a84683e91..1bf2f2a2c03 100644 --- a/src/client/context.rs +++ b/src/client/context.rs @@ -1,9 +1,6 @@ use std::fmt; use std::sync::Arc; -use tokio::sync::RwLock; -use typemap_rev::TypeMap; - #[cfg(feature = "cache")] pub use crate::cache::Cache; use crate::gateway::ActivityData; @@ -31,7 +28,7 @@ pub struct Context { /// A clone of [`Client::data`]. Refer to its documentation for more information. /// /// [`Client::data`]: super::Client::data - pub data: Arc>, + data: Arc, /// The messenger to communicate with the shard runner. pub shard: ShardMessenger, /// The ID of the shard this context is related to. @@ -41,7 +38,8 @@ pub struct Context { pub cache: Arc, } -// Used by the #[instrument] macro on client::dispatch::handle_event +// Used by the #[cfg_attr(feature = "tracing_instrument", instrument)] macro on +// client::dispatch::handle_event impl fmt::Debug for Context { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Context") @@ -55,7 +53,7 @@ impl Context { /// Create a new Context to be passed to an event handler. #[cfg(feature = "gateway")] pub(crate) fn new( - data: Arc>, + data: Arc, runner: &ShardRunner, shard_id: ShardId, http: Arc, @@ -72,7 +70,11 @@ impl Context { } #[cfg(all(not(feature = "cache"), not(feature = "gateway")))] - pub fn easy(data: Arc>, shard_id: u32, http: Arc) -> Context { + pub fn easy( + data: Arc, + shard_id: ShardId, + http: Arc, + ) -> Context { Context { shard_id, data, @@ -80,6 +82,24 @@ impl Context { } } + /// A container for a data type that can be used across contexts. + /// + /// The purpose of the data field is to be accessible and persistent across contexts; that is, + /// data can be modified by one context, and will persist through the future and be accessible + /// through other contexts. This is useful for anything that should "live" through the program: + /// counters, database connections, custom user caches, etc. + /// + /// # Panics + /// Panics if the generic provided is not equal to the type provided in [`ClientBuilder::data`]. + /// + /// [`ClientBuilder::data`]: super::ClientBuilder::data + #[must_use] + pub fn data(&self) -> Arc { + Arc::clone(&self.data) + .downcast() + .expect("Type provided to Context should be the same as ClientBuilder::data.") + } + /// Sets the current user as being [`Online`]. This maintains the current activity. /// /// # Examples @@ -104,7 +124,6 @@ impl Context { /// /// [`Online`]: OnlineStatus::Online #[cfg(feature = "gateway")] - #[inline] pub fn online(&self) { self.shard.set_status(OnlineStatus::Online); } @@ -133,7 +152,6 @@ impl Context { /// /// [`Idle`]: OnlineStatus::Idle #[cfg(feature = "gateway")] - #[inline] pub fn idle(&self) { self.shard.set_status(OnlineStatus::Idle); } @@ -162,7 +180,6 @@ impl Context { /// /// [`DoNotDisturb`]: OnlineStatus::DoNotDisturb #[cfg(feature = "gateway")] - #[inline] pub fn dnd(&self) { self.shard.set_status(OnlineStatus::DoNotDisturb); } @@ -191,7 +208,6 @@ impl Context { /// /// [`Invisible`]: OnlineStatus::Invisible #[cfg(feature = "gateway")] - #[inline] pub fn invisible(&self) { self.shard.set_status(OnlineStatus::Invisible); } @@ -224,7 +240,6 @@ impl Context { /// [`Event::Resumed`]: crate::model::event::Event::Resumed /// [`Online`]: OnlineStatus::Online #[cfg(feature = "gateway")] - #[inline] pub fn reset_presence(&self) { self.shard.set_presence(None, OnlineStatus::Online); } @@ -254,7 +269,6 @@ impl Context { /// } /// ``` #[cfg(feature = "gateway")] - #[inline] pub fn set_activity(&self, activity: Option) { self.shard.set_activity(activity); } @@ -304,61 +318,7 @@ impl Context { /// [`DoNotDisturb`]: OnlineStatus::DoNotDisturb /// [`Idle`]: OnlineStatus::Idle #[cfg(feature = "gateway")] - #[inline] pub fn set_presence(&self, activity: Option, status: OnlineStatus) { self.shard.set_presence(activity, status); } } - -impl AsRef for Context { - fn as_ref(&self) -> &Http { - &self.http - } -} - -impl AsRef for Arc { - fn as_ref(&self) -> &Http { - &self.http - } -} - -impl AsRef> for Context { - fn as_ref(&self) -> &Arc { - &self.http - } -} - -#[cfg(feature = "cache")] -impl AsRef for Context { - fn as_ref(&self) -> &Cache { - &self.cache - } -} - -#[cfg(feature = "cache")] -impl AsRef for Arc { - fn as_ref(&self) -> &Cache { - &self.cache - } -} - -#[cfg(feature = "cache")] -impl AsRef> for Context { - fn as_ref(&self) -> &Arc { - &self.cache - } -} - -#[cfg(feature = "cache")] -impl AsRef for Cache { - fn as_ref(&self) -> &Cache { - self - } -} - -#[cfg(feature = "gateway")] -impl AsRef for Context { - fn as_ref(&self) -> &ShardMessenger { - &self.shard - } -} diff --git a/src/client/dispatch.rs b/src/client/dispatch.rs index b42eebe1e0c..6fd5b190332 100644 --- a/src/client/dispatch.rs +++ b/src/client/dispatch.rs @@ -1,15 +1,13 @@ use std::sync::Arc; -use tracing::debug; - #[cfg(feature = "gateway")] -use super::event_handler::{EventHandler, RawEventHandler}; +use super::event_handler::InternalEventHandler; use super::{Context, FullEvent}; #[cfg(feature = "cache")] use crate::cache::{Cache, CacheUpdate}; #[cfg(feature = "framework")] use crate::framework::Framework; -use crate::internal::tokio::spawn_named; +use crate::internal::prelude::*; use crate::model::channel::ChannelType; use crate::model::event::Event; use crate::model::guild::Member; @@ -42,46 +40,47 @@ macro_rules! update_cache { ($cache:ident, $event:ident) => {}; } -pub(crate) fn dispatch_model( +/// Calls the user's event handlers and the framework handler. +/// +/// This MUST be called from a different task to the recv_event loop, to allow for +/// intra-shard concurrency between the shard loop and event handler. +pub(crate) async fn dispatch_model( event: Event, - context: &Context, + context: Context, #[cfg(feature = "framework")] framework: Option>, - event_handlers: Vec>, - raw_event_handlers: Vec>, + event_handler: Option, ) { - for raw_handler in raw_event_handlers { - let (context, event) = (context.clone(), event.clone()); - tokio::spawn(async move { raw_handler.raw_event(context, event).await }); - } + let handler = match event_handler { + Some(InternalEventHandler::Normal(handler)) => Some(handler), + Some(InternalEventHandler::Raw(raw_handler)) => { + return raw_handler.raw_event(context, event).await; + }, + None => None, + }; - let full_events = update_cache_with_event( + let (full_event, extra_event) = update_cache_with_event( #[cfg(feature = "cache")] &context.cache, event, ); - if let Some(events) = full_events { - let iter = std::iter::once(events.0).chain(events.1); - for handler in event_handlers { - for event in iter.clone() { - let context = context.clone(); - let handler = Arc::clone(&handler); - spawn_named(event.snake_case_name(), async move { - event.dispatch(context, &*handler).await; - }); + #[cfg(feature = "framework")] + if let Some(framework) = framework { + if framework.dispatch_automatically() { + if let Some(extra_event) = &extra_event { + framework.dispatch(&context, extra_event).await; } + + framework.dispatch(&context, &full_event).await; } + } - #[cfg(feature = "framework")] - if let Some(framework) = framework { - for event in iter { - let context = context.clone(); - let framework = Arc::clone(&framework); - spawn_named("dispatch::framework::dispatch", async move { - framework.dispatch(context, event).await; - }); - } + if let Some(handler) = handler { + if let Some(extra_event) = extra_event { + extra_event.dispatch(context.clone(), &*handler).await; } + + full_event.dispatch(context, &*handler).await; } } @@ -92,10 +91,10 @@ pub(crate) fn dispatch_model( /// /// Can return `None` if an event is unknown. #[cfg_attr(not(feature = "cache"), allow(unused_mut))] -fn update_cache_with_event( +pub fn update_cache_with_event( #[cfg(feature = "cache")] cache: &Cache, event: Event, -) -> Option<(FullEvent, Option)> { +) -> (FullEvent, Option) { let mut extra_event = None; let event = match event { Event::CommandPermissionsUpdate(event) => FullEvent::CommandPermissionsUpdate { @@ -299,7 +298,7 @@ fn update_cache_with_event( }, Event::MessageDeleteBulk(event) => FullEvent::MessageDeleteBulk { channel_id: event.channel_id, - multiple_deleted_messages_ids: event.ids, + multiple_deleted_messages_ids: event.ids.into_vec(), guild_id: event.guild_id, }, Event::MessageDelete(event) => FullEvent::MessageDelete { @@ -317,14 +316,11 @@ fn update_cache_with_event( event, } }, - #[allow(deprecated)] - Event::PresencesReplace(event) => FullEvent::PresenceReplace { - presences: event.presences, - }, Event::PresenceUpdate(mut event) => { - update_cache!(cache, event); + let old_data = if_cache!(event.update(cache)); FullEvent::PresenceUpdate { + old_data, new_data: event.presence, } }, @@ -347,13 +343,12 @@ fn update_cache_with_event( #[cfg(feature = "cache")] { let mut shards = cache.shard_data.write(); - if shards.connected.len() as u32 == shards.total && !shards.has_sent_shards_ready { + if shards.connected.len() == shards.total.get() as usize + && !shards.has_sent_shards_ready + { shards.has_sent_shards_ready = true; - let total = shards.total; - drop(shards); - extra_event = Some(FullEvent::ShardsReady { - total_shards: total, + total_shards: shards.total, }); } } @@ -368,10 +363,6 @@ fn update_cache_with_event( Event::TypingStart(event) => FullEvent::TypingStart { event, }, - Event::Unknown(event) => { - debug!("An unknown event was received: {event:?}"); - return None; - }, Event::UserUpdate(mut event) => { let before = if_cache!(event.update(cache)); @@ -392,11 +383,11 @@ fn update_cache_with_event( } }, Event::VoiceChannelStatusUpdate(mut event) => { - let old = if_cache!(event.update(cache)); + let old = if_cache!(event.update(cache).map(FixedString::into_string)); FullEvent::VoiceChannelStatusUpdate { old, - status: event.status, + status: event.status.map(FixedString::into_string), id: event.id, guild_id: event.guild_id, } @@ -493,5 +484,5 @@ fn update_cache_with_event( }, }; - Some((event, extra_event)) + (event, extra_event) } diff --git a/src/client/error.rs b/src/client/error.rs index 049fd652b65..33da36de381 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -12,15 +12,12 @@ use std::fmt; pub enum Error { /// When a shard has completely failed to reboot after resume and/or reconnect attempts. ShardBootFailure, - /// When all shards that the client is responsible for have shutdown with an error. - Shutdown, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::ShardBootFailure => f.write_str("Failed to (re-)boot a shard"), - Self::Shutdown => f.write_str("The clients shards shutdown"), } } } diff --git a/src/client/event_handler.rs b/src/client/event_handler.rs index 233db6d0dd2..318893f881b 100644 --- a/src/client/event_handler.rs +++ b/src/client/event_handler.rs @@ -1,4 +1,10 @@ +use std::collections::VecDeque; +#[cfg(feature = "cache")] +use std::num::NonZeroU16; +use std::sync::Arc; + use async_trait::async_trait; +use strum::{EnumCount, IntoStaticStr, VariantNames}; use super::context::Context; use crate::gateway::ShardStageUpdateEvent; @@ -21,15 +27,35 @@ macro_rules! event_handler { $( #[deprecated = $deprecated] )? async fn $method_name(&self, $($context: Context,)? $( $arg_name: $arg_type ),*) { // Suppress unused argument warnings + #[allow(dropping_references, dropping_copy_types)] drop(( $($context,)? $($arg_name),* )) } )* + + /// Checks if the `event` should be dispatched (`true`) or ignored (`false`). + /// + /// This affects [`crate::collector::collect`], [`crate::framework::Framework::dispatch`] and this `EventHandler` trait. + /// + /// ## Warning + /// + /// This will run synchronously on every event in the dispatch loop + /// of the shard that is receiving the event. If your filter code + /// takes too long, it may delay other events from being dispatched + /// in a timely manner. It is recommended to keep the runtime + /// complexity of the filter code low to avoid unnecessarily blocking + /// your bot. + fn filter_event(&self, context: &Context, event: &Event) -> bool { + // Suppress unused argument warnings + #[allow(dropping_references, dropping_copy_types)] + drop(( context, event )); + true + } } /// This enum stores every possible event that an [`EventHandler`] can receive. - #[non_exhaustive] - #[allow(clippy::large_enum_variant)] // TODO: do some boxing to fix this - #[derive(Clone, Debug)] + #[cfg_attr(not(feature = "unstable"), non_exhaustive)] + #[derive(Clone, Debug, VariantNames, IntoStaticStr, EnumCount)] + #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] pub enum FullEvent { $( $( #[doc = $doc] )* @@ -117,7 +143,7 @@ event_handler! { /// Dispatched when every shard has received a Ready event #[cfg(feature = "cache")] - ShardsReady { total_shards: u32 } => async fn shards_ready(&self, ctx: Context); + ShardsReady { total_shards: NonZeroU16 } => async fn shards_ready(&self, ctx: Context); /// Dispatched when a channel is created. /// @@ -137,7 +163,7 @@ event_handler! { /// Dispatched when a channel is deleted. /// /// Provides said channel's data. - ChannelDelete { channel: GuildChannel, messages: Option> } => async fn channel_delete(&self, ctx: Context); + ChannelDelete { channel: GuildChannel, messages: Option> } => async fn channel_delete(&self, ctx: Context); /// Dispatched when a pin is added, deleted. /// @@ -186,7 +212,7 @@ event_handler! { /// Dispatched when the emojis are updated. /// /// Provides the guild's id and the new state of the emojis in the guild. - GuildEmojisUpdate { guild_id: GuildId, current_state: HashMap } => async fn guild_emojis_update(&self, ctx: Context); + GuildEmojisUpdate { guild_id: GuildId, current_state: ExtractMap } => async fn guild_emojis_update(&self, ctx: Context); /// Dispatched when a guild's integration is added, updated or removed. /// @@ -244,7 +270,7 @@ event_handler! { /// Dispatched when the stickers are updated. /// /// Provides the guild's id and the new state of the stickers in the guild. - GuildStickersUpdate { guild_id: GuildId, current_state: HashMap } => async fn guild_stickers_update(&self, ctx: Context); + GuildStickersUpdate { guild_id: GuildId, current_state: ExtractMap } => async fn guild_stickers_update(&self, ctx: Context); /// Dispatched when the guild is updated. /// @@ -303,16 +329,14 @@ event_handler! { /// Provides the channel's id and the message's id. ReactionRemoveEmoji { removed_reactions: Reaction } => async fn reaction_remove_emoji(&self, ctx: Context); - #[deprecated = "This event does not exist"] - PresenceReplace { presences: Vec } => async fn presence_replace(&self, ctx: Context); - /// Dispatched when a user's presence is updated (e.g off -> on). /// - /// Provides the presence's new data. + /// Provides the presence's new data, as well as the old presence data if the + /// cache feature is enabled and the data is available. /// /// Note: This event will not trigger unless the "guild presences" privileged intent is enabled /// on the bot application page. - PresenceUpdate { new_data: Presence } => async fn presence_update(&self, ctx: Context); + PresenceUpdate { old_data: Option, new_data: Presence } => async fn presence_update(&self, ctx: Context); /// Dispatched upon startup. /// @@ -490,4 +514,30 @@ event_handler! { pub trait RawEventHandler: Send + Sync { /// Dispatched when any event occurs async fn raw_event(&self, _ctx: Context, _ev: Event) {} + + /// Checks if the `event` should be dispatched (`true`) or ignored (`false`). + /// + /// This affects [`crate::collector::collect`], [`crate::framework::Framework::dispatch`] and + /// this `EventHandler` trait. + /// + /// ## Warning + /// + /// This will run synchronously on every event in the dispatch loop + /// of the shard that is receiving the event. If your filter code + /// takes too long, it may delay other events from being dispatched + /// in a timely manner. It is recommended to keep the runtime + /// complexity of the filter code low to avoid unnecessarily blocking + /// your bot. + fn filter_event(&self, context: &Context, event: &Event) -> bool { + // Suppress unused argument warnings + #[allow(dropping_references, dropping_copy_types)] + drop((context, event)); + true + } +} + +#[derive(Clone)] +pub enum InternalEventHandler { + Raw(Arc), + Normal(Arc), } diff --git a/src/client/mod.rs b/src/client/mod.rs index 067198aa3ed..3e2aa89ce60 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -16,12 +16,13 @@ mod context; #[cfg(feature = "gateway")] -pub(crate) mod dispatch; +pub mod dispatch; mod error; #[cfg(feature = "gateway")] mod event_handler; use std::future::IntoFuture; +use std::num::NonZeroU16; use std::ops::Range; use std::sync::Arc; #[cfg(feature = "framework")] @@ -30,14 +31,12 @@ use std::sync::OnceLock; use futures::channel::mpsc::UnboundedReceiver as Receiver; use futures::future::BoxFuture; use futures::StreamExt as _; -use tokio::sync::{Mutex, RwLock}; -use tracing::{debug, error, info, instrument}; -use typemap_rev::{TypeMap, TypeMapKey}; +use tracing::debug; pub use self::context::Context; pub use self::error::Error as ClientError; #[cfg(feature = "gateway")] -pub use self::event_handler::{EventHandler, FullEvent, RawEventHandler}; +pub use self::event_handler::{EventHandler, FullEvent, InternalEventHandler, RawEventHandler}; #[cfg(feature = "gateway")] use super::gateway::GatewayError; #[cfg(feature = "cache")] @@ -53,17 +52,21 @@ use crate::gateway::{ActivityData, PresenceData}; use crate::gateway::{ShardManager, ShardManagerOptions}; use crate::http::Http; use crate::internal::prelude::*; +use crate::internal::tokio::spawn_named; #[cfg(feature = "gateway")] use crate::model::gateway::GatewayIntents; use crate::model::id::ApplicationId; +#[cfg(feature = "voice")] +use crate::model::id::UserId; use crate::model::user::OnlineStatus; +use crate::utils::check_shard_total; /// A builder implementing [`IntoFuture`] building a [`Client`] to interact with Discord. #[cfg(feature = "gateway")] #[must_use = "Builders do nothing unless they are awaited"] pub struct ClientBuilder { - data: TypeMap, - http: Http, + data: Option>, + http: Arc, intents: GatewayIntents, #[cfg(feature = "cache")] cache_settings: CacheSettings, @@ -71,38 +74,21 @@ pub struct ClientBuilder { framework: Option>, #[cfg(feature = "voice")] voice_manager: Option>, - event_handlers: Vec>, - raw_event_handlers: Vec>, + event_handler: Option>, + raw_event_handler: Option>, presence: PresenceData, } #[cfg(feature = "gateway")] impl ClientBuilder { - fn _new(http: Http, intents: GatewayIntents) -> Self { - Self { - data: TypeMap::new(), - http, - intents, - #[cfg(feature = "cache")] - cache_settings: CacheSettings::default(), - #[cfg(feature = "framework")] - framework: None, - #[cfg(feature = "voice")] - voice_manager: None, - event_handlers: vec![], - raw_event_handlers: vec![], - presence: PresenceData::default(), - } - } - /// Construct a new builder to call methods on for the client construction. The `token` will /// automatically be prefixed "Bot " if not already. /// /// **Panic**: If you have enabled the `framework`-feature (on by default), you must specify a /// framework via the [`Self::framework`] method, otherwise awaiting the builder will cause a /// panic. - pub fn new(token: impl AsRef, intents: GatewayIntents) -> Self { - Self::_new(Http::new(token.as_ref()), intents) + pub fn new(token: &str, intents: GatewayIntents) -> Self { + Self::new_with_http(Arc::new(Http::new(token)), intents) } /// Construct a new builder with a [`Http`] instance to calls methods on for the client @@ -111,19 +97,25 @@ impl ClientBuilder { /// **Panic**: If you have enabled the `framework`-feature (on by default), you must specify a /// framework via the [`Self::framework`] method, otherwise awaiting the builder will cause a /// panic. - pub fn new_with_http(http: Http, intents: GatewayIntents) -> Self { - Self::_new(http, intents) - } - - /// Sets a token for the bot. If the token is not prefixed "Bot ", this method will - /// automatically do so. - pub fn token(mut self, token: impl AsRef) -> Self { - self.http = Http::new(token.as_ref()); - - self + pub fn new_with_http(http: Arc, intents: GatewayIntents) -> Self { + Self { + http, + intents, + data: None, + #[cfg(feature = "cache")] + cache_settings: CacheSettings::default(), + #[cfg(feature = "framework")] + framework: None, + #[cfg(feature = "voice")] + voice_manager: None, + event_handler: None, + raw_event_handler: None, + presence: PresenceData::default(), + } } /// Gets the current token used for the [`Http`] client. + #[must_use] pub fn get_token(&self) -> &str { self.http.token() } @@ -137,30 +129,14 @@ impl ClientBuilder { /// Gets the application ID, if already initialized. See [`Self::application_id`] for more /// info. + #[must_use] pub fn get_application_id(&self) -> Option { self.http.application_id() } - /// Sets the entire [`TypeMap`] that will be available in [`Context`]s. A [`TypeMap`] must not - /// be constructed manually: [`Self::type_map_insert`] can be used to insert one type at a - /// time. - pub fn type_map(mut self, type_map: TypeMap) -> Self { - self.data = type_map; - - self - } - - /// Gets the type map. See [`Self::type_map`] for more info. - pub fn get_type_map(&self) -> &TypeMap { - &self.data - } - - /// Insert a single `value` into the internal [`TypeMap`] that will be available in - /// [`Context::data`]. This method can be called multiple times in order to populate the - /// [`TypeMap`] with `value`s. - pub fn type_map_insert(mut self, value: T::Value) -> Self { - self.data.insert::(value); - + /// Sets the global data type that can be accessed from [`Context::data`]. + pub fn data(mut self, data: Arc) -> Self { + self.data = Some(data); self } @@ -175,6 +151,7 @@ impl ClientBuilder { /// Gets the cache settings. See [`Self::cache_settings`] for more info. #[cfg(feature = "cache")] + #[must_use] pub fn get_cache_settings(&self) -> &CacheSettings { &self.cache_settings } @@ -196,42 +173,25 @@ impl ClientBuilder { /// Gets the framework, if already initialized. See [`Self::framework`] for more info. #[cfg(feature = "framework")] + #[must_use] pub fn get_framework(&self) -> Option<&dyn Framework> { self.framework.as_deref() } /// Sets the voice gateway handler to be used. It will receive voice events sent over the /// gateway and then consider - based on its settings - whether to dispatch a command. - /// - /// *Info*: If a reference to the voice_manager is required for manual dispatch, use the - /// [`Self::voice_manager_arc`]-method instead. #[cfg(feature = "voice")] - pub fn voice_manager(mut self, voice_manager: V) -> Self + pub fn voice_manager(mut self, voice_manager: impl Into>) -> Self where V: VoiceGatewayManager + 'static, { - self.voice_manager = Some(Arc::new(voice_manager)); - - self - } - - /// This method allows to pass an [`Arc`]'ed `voice_manager` - this step is done for you in the - /// [`voice_manager`]-method, if you don't need the extra control. You can provide a clone and - /// keep the original to manually dispatch. - /// - /// [`voice_manager`]: Self::voice_manager - #[cfg(feature = "voice")] - pub fn voice_manager_arc( - mut self, - voice_manager: Arc, - ) -> Self { - self.voice_manager = Some(voice_manager); - + self.voice_manager = Some(voice_manager.into()); self } /// Gets the voice manager, if already initialized. See [`Self::voice_manager`] for more info. #[cfg(feature = "voice")] + #[must_use] pub fn get_voice_manager(&self) -> Option> { self.voice_manager.clone() } @@ -263,43 +223,40 @@ impl ClientBuilder { } /// Gets the intents. See [`Self::intents`] for more info. + #[must_use] pub fn get_intents(&self) -> GatewayIntents { self.intents } /// Adds an event handler with multiple methods for each possible event. - pub fn event_handler(mut self, event_handler: H) -> Self { - self.event_handlers.push(Arc::new(event_handler)); - - self - } - - /// Adds an event handler with multiple methods for each possible event. Passed by Arc. - pub fn event_handler_arc( - mut self, - event_handler_arc: Arc, - ) -> Self { - self.event_handlers.push(event_handler_arc); - + pub fn event_handler(mut self, event_handler: impl Into>) -> Self + where + H: EventHandler + 'static, + { + self.event_handler = Some(event_handler.into()); self } /// Gets the added event handlers. See [`Self::event_handler`] for more info. - pub fn get_event_handlers(&self) -> &[Arc] { - &self.event_handlers + #[must_use] + pub fn get_event_handler(&self) -> Option<&Arc> { + self.event_handler.as_ref() } /// Adds an event handler with a single method where all received gateway events will be /// dispatched. - pub fn raw_event_handler(mut self, raw_event_handler: H) -> Self { - self.raw_event_handlers.push(Arc::new(raw_event_handler)); - + pub fn raw_event_handler(mut self, raw_event_handler: impl Into>) -> Self + where + H: RawEventHandler + 'static, + { + self.raw_event_handler = Some(raw_event_handler.into()); self } /// Gets the added raw event handlers. See [`Self::raw_event_handler`] for more info. - pub fn get_raw_event_handlers(&self) -> &[Arc] { - &self.raw_event_handlers + #[must_use] + pub fn get_raw_event_handler(&self) -> Option<&Arc> { + self.raw_event_handler.as_ref() } /// Sets the initial activity. @@ -317,6 +274,7 @@ impl ClientBuilder { } /// Gets the initial presence. See [`Self::activity`] and [`Self::status`] for more info. + #[must_use] pub fn get_presence(&self) -> &PresenceData { &self.presence } @@ -328,30 +286,34 @@ impl IntoFuture for ClientBuilder { type IntoFuture = BoxFuture<'static, Result>; - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] fn into_future(self) -> Self::IntoFuture { - let data = Arc::new(RwLock::new(self.data)); + let data = self.data.unwrap_or(Arc::new(())); #[cfg(feature = "framework")] let framework = self.framework; - let event_handlers = self.event_handlers; - let raw_event_handlers = self.raw_event_handlers; let intents = self.intents; let presence = self.presence; + let http = self.http; - let mut http = self.http; + let event_handler = match (self.event_handler, self.raw_event_handler) { + (Some(_), Some(_)) => panic!("Cannot provide both a normal and raw event handlers"), + (Some(h), None) => Some(InternalEventHandler::Normal(h)), + (None, Some(h)) => Some(InternalEventHandler::Raw(h)), + (None, None) => None, + }; - if let Some(ratelimiter) = &mut http.ratelimiter { - let event_handlers_clone = event_handlers.clone(); - ratelimiter.set_ratelimit_callback(Box::new(move |info| { - for event_handler in event_handlers_clone.iter().map(Arc::clone) { - let info = info.clone(); - tokio::spawn(async move { event_handler.ratelimit(info).await }); - } - })); + if let Some(ratelimiter) = &http.ratelimiter { + if let Some(InternalEventHandler::Normal(event_handler)) = &event_handler { + let event_handler = Arc::clone(event_handler); + ratelimiter.set_ratelimit_callback(Box::new(move |info| { + let event_handler = Arc::clone(&event_handler); + spawn_named("ratelimit::dispatch", async move { + event_handler.ratelimit(info).await; + }); + })); + } } - let http = Arc::new(http); - #[cfg(feature = "voice")] let voice_manager = self.voice_manager; @@ -359,33 +321,35 @@ impl IntoFuture for ClientBuilder { let cache = Arc::new(Cache::new_with_settings(self.cache_settings)); Box::pin(async move { - let ws_url = Arc::new(Mutex::new(match http.get_gateway().await { - Ok(response) => response.url, + let (ws_url, shard_total, max_concurrency) = match http.get_bot_gateway().await { + Ok(response) => ( + Arc::from(response.url), + response.shards, + response.session_start_limit.max_concurrency, + ), Err(err) => { - tracing::warn!("HTTP request to get gateway URL failed: {}", err); - "wss://gateway.discord.gg".to_string() + tracing::warn!("HTTP request to get gateway URL failed: {err}"); + (Arc::from("wss://gateway.discord.gg"), NonZeroU16::MIN, NonZeroU16::MIN) }, - })); + }; #[cfg(feature = "framework")] let framework_cell = Arc::new(OnceLock::new()); let (shard_manager, shard_manager_ret_value) = ShardManager::new(ShardManagerOptions { data: Arc::clone(&data), - event_handlers, - raw_event_handlers, + event_handler, #[cfg(feature = "framework")] framework: Arc::clone(&framework_cell), - shard_index: 0, - shard_init: 0, - shard_total: 0, #[cfg(feature = "voice")] voice_manager: voice_manager.clone(), ws_url: Arc::clone(&ws_url), + shard_total, #[cfg(feature = "cache")] cache: Arc::clone(&cache), http: Arc::clone(&http), intents, presence: Some(presence), + max_concurrency, }); let client = Client { @@ -439,7 +403,7 @@ impl IntoFuture for ClientBuilder { /// impl EventHandler for Handler { /// async fn message(&self, context: Context, msg: Message) { /// if msg.content == "!ping" { -/// let _ = msg.channel_id.say(&context, "Pong!"); +/// let _ = msg.channel_id.say(&context.http, "Pong!"); /// } /// } /// } @@ -458,97 +422,7 @@ impl IntoFuture for ClientBuilder { /// [sharding docs]: crate::gateway#sharding #[cfg(feature = "gateway")] pub struct Client { - /// A TypeMap which requires types to be Send + Sync. This is a map that can be safely shared - /// across contexts. - /// - /// The purpose of the data field is to be accessible and persistent across contexts; that is, - /// data can be modified by one context, and will persist through the future and be accessible - /// through other contexts. This is useful for anything that should "live" through the program: - /// counters, database connections, custom user caches, etc. - /// - /// In the meaning of a context, this data can be accessed through [`Context::data`]. - /// - /// # Examples - /// - /// Create a `MessageEventCounter` to track the following events: - /// - /// - [`Event::MessageCreate`] - /// - [`Event::MessageDelete`] - /// - [`Event::MessageDeleteBulk`] - /// - [`Event::MessageUpdate`] - /// - /// ```rust,ignore - /// use std::collections::HashMap; - /// use std::env; - /// - /// use serenity::model::prelude::*; - /// use serenity::prelude::*; - /// - /// struct MessageEventCounter; - /// - /// impl TypeMapKey for MessageEventCounter { - /// type Value = HashMap; - /// } - /// - /// async fn reg>(ctx: Context, name: S) { - /// let mut data = ctx.data.write().await; - /// let counter = data.get_mut::().unwrap(); - /// let entry = counter.entry(name.into()).or_insert(0); - /// *entry += 1; - /// } - /// - /// struct Handler; - /// - /// #[serenity::async_trait] - /// impl EventHandler for Handler { - /// async fn message(&self, ctx: Context, _: Message) { - /// reg(ctx, "MessageCreate").await - /// } - /// async fn message_delete(&self, ctx: Context, _: ChannelId, _: MessageId) { - /// reg(ctx, "MessageDelete").await - /// } - /// async fn message_delete_bulk(&self, ctx: Context, _: ChannelId, _: Vec) { - /// reg(ctx, "MessageDeleteBulk").await - /// } - /// - /// #[cfg(feature = "cache")] - /// async fn message_update( - /// &self, - /// ctx: Context, - /// _old: Option, - /// _new: Option, - /// _: MessageUpdateEvent, - /// ) { - /// reg(ctx, "MessageUpdate").await - /// } - /// - /// #[cfg(not(feature = "cache"))] - /// async fn message_update(&self, ctx: Context, _new_data: MessageUpdateEvent) { - /// reg(ctx, "MessageUpdate").await - /// } - /// } - /// - /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).event_handler(Handler).await?; - /// { - /// let mut data = client.data.write().await; - /// data.insert::(HashMap::default()); - /// } - /// - /// client.start().await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// Refer to [example 05] for an example on using the [`Self::data`] field. - /// - /// [`Event::MessageCreate`]: crate::model::event::Event::MessageCreate - /// [`Event::MessageDelete`]: crate::model::event::Event::MessageDelete - /// [`Event::MessageDeleteBulk`]: crate::model::event::Event::MessageDeleteBulk - /// [`Event::MessageUpdate`]: crate::model::event::Event::MessageUpdate - /// [example 05]: https://github.com/serenity-rs/serenity/tree/current/examples/e05_command_framework - pub data: Arc>, + data: Arc, /// A HashMap of all shards instantiated by the Client. /// /// The key is the shard ID and the value is the shard itself. @@ -612,11 +486,7 @@ pub struct Client { #[cfg(feature = "voice")] pub voice_manager: Option>, /// URL that the client's shards will use to connect to the gateway. - /// - /// This is likely not important for production usage and is, at best, used for debugging. - /// - /// This is wrapped in an `Arc>` so all shards will have an updated value available. - pub ws_url: Arc>, + pub ws_url: Arc, /// The cache for the client. #[cfg(feature = "cache")] pub cache: Arc, @@ -625,10 +495,27 @@ pub struct Client { } impl Client { - pub fn builder(token: impl AsRef, intents: GatewayIntents) -> ClientBuilder { + pub fn builder(token: &str, intents: GatewayIntents) -> ClientBuilder { ClientBuilder::new(token, intents) } + /// Fetches the data type provided to [`ClientBuilder::data`]. + /// + /// See the documentation for [`Context::data`] for more information. + #[must_use] + pub fn data(&self) -> Arc { + self.try_data().expect("Client::data generic does not match ClientBuilder::data type") + } + + /// Tries to fetch the data type provided to [`ClientBuilder::data`]. + /// + /// This returns None if no data was provided or Data is the wrong type and + /// is mostly for Framework usage, normal bots should just use [`Self::data`]. + #[must_use] + pub fn try_data(&self) -> Option> { + Arc::clone(&self.data).downcast().ok() + } + /// Establish the connection and start listening for events. /// /// This will start receiving events in a loop and start dispatching the events to your @@ -661,10 +548,16 @@ impl Client { /// # } /// ``` /// + /// # Errors + /// + /// Returns [`Error::Gateway`] when all shards have shutdown due to an error. + /// Returns [`Error::Http`] if fetching the current User fails when initialising a voice + /// manager. + /// /// [gateway docs]: crate::gateway#sharding - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn start(&mut self) -> Result<()> { - self.start_connection(0, 0, 1).await + self.start_connection(0, 0, NonZeroU16::MIN).await } /// Establish the connection(s) and start listening for events. @@ -700,15 +593,16 @@ impl Client { /// /// # Errors /// - /// Returns a [`ClientError::Shutdown`] when all shards have shutdown due to an error. + /// Returns [`Error::Gateway`] when all shards have shutdown due to an error. + /// Returns [`Error::Http`] if fetching the current User fails when initialising a voice + /// manager. /// /// [gateway docs]: crate::gateway#sharding - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn start_autosharded(&mut self) -> Result<()> { let (end, total) = { let res = self.http.get_bot_gateway().await?; - - (res.shards - 1, res.shards) + (res.shards.get() - 1, res.shards) }; self.start_connection(0, end, total).await @@ -765,12 +659,14 @@ impl Client { /// /// # Errors /// - /// Returns a [`ClientError::Shutdown`] when all shards have shutdown due to an error. + /// Returns [`Error::Gateway`] when all shards have shutdown due to an error. + /// Returns [`Error::Http`] if fetching the current User fails when initialising a voice + /// manager. /// /// [gateway docs]: crate::gateway#sharding - #[instrument(skip(self))] - pub async fn start_shard(&mut self, shard: u32, shards: u32) -> Result<()> { - self.start_connection(shard, shard, shards).await + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + pub async fn start_shard(&mut self, shard: u16, shards: u16) -> Result<()> { + self.start_connection(shard, shard, check_shard_total(shards)).await } /// Establish sharded connections and start listening for events. @@ -806,12 +702,14 @@ impl Client { /// /// # Errors /// - /// Returns a [`ClientError::Shutdown`] when all shards have shutdown due to an error. + /// Returns [`Error::Gateway`] when all shards have shutdown due to an error. + /// Returns [`Error::Http`] if fetching the current User fails when initialising a voice + /// manager. /// /// [Gateway docs]: crate::gateway#sharding - #[instrument(skip(self))] - pub async fn start_shards(&mut self, total_shards: u32) -> Result<()> { - self.start_connection(0, total_shards - 1, total_shards).await + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + pub async fn start_shards(&mut self, total_shards: u16) -> Result<()> { + self.start_connection(0, total_shards - 1, check_shard_total(total_shards)).await } /// Establish a range of sharded connections and start listening for events. @@ -847,53 +745,51 @@ impl Client { /// /// # Errors /// - /// Returns a [`ClientError::Shutdown`] when all shards have shutdown due to an error. + /// Returns [`Error::Gateway`] when all shards have shutdown due to an error. + /// Returns [`Error::Http`] if fetching the current User fails when initialising a voice + /// manager. /// /// [Gateway docs]: crate::gateway#sharding - #[instrument(skip(self))] - pub async fn start_shard_range(&mut self, range: Range, total_shards: u32) -> Result<()> { - self.start_connection(range.start, range.end, total_shards).await + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + pub async fn start_shard_range(&mut self, range: Range, total_shards: u16) -> Result<()> { + self.start_connection(range.start, range.end, check_shard_total(total_shards)).await } - /// Shard data layout is: - /// 0: first shard number to initialize - /// 1: shard number to initialize up to and including - /// 2: total number of shards the bot is sharding for - /// - /// Not all shards need to be initialized in this process. - /// - /// # Errors - /// - /// Returns a [`ClientError::Shutdown`] when all shards have shutdown due to an error. - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] async fn start_connection( &mut self, - start_shard: u32, - end_shard: u32, - total_shards: u32, + start_shard: u16, + end_shard: u16, + total_shards: NonZeroU16, ) -> Result<()> { #[cfg(feature = "voice")] if let Some(voice_manager) = &self.voice_manager { - let user = self.http.get_current_user().await?; + #[cfg(feature = "cache")] + let cache_user_id = { + let cache_user = self.cache.current_user(); + if cache_user.id == UserId::default() { + None + } else { + Some(cache_user.id) + } + }; + + #[cfg(not(feature = "cache"))] + let cache_user_id: Option = None; - voice_manager.initialise(total_shards, user.id).await; + let user_id = match cache_user_id { + Some(u) => u, + None => self.http.get_current_user().await?.id, + }; + + voice_manager.initialise(total_shards, user_id).await; } let init = end_shard - start_shard + 1; - self.shard_manager.set_shards(start_shard, init, total_shards).await; - debug!("Initializing shard info: {} - {}/{}", start_shard, init, total_shards); - if let Err(why) = self.shard_manager.initialize() { - error!("Failed to boot a shard: {:?}", why); - info!("Shutting down all shards"); - - self.shard_manager.shutdown_all().await; - - return Err(Error::Client(ClientError::ShardBootFailure)); - } - + self.shard_manager.initialize(start_shard, init, total_shards); if let Some(Err(err)) = self.shard_manager_return_value.next().await { return Err(Error::Gateway(err)); } diff --git a/src/collector.rs b/src/collector.rs index bcec95e629d..459dcc496e1 100644 --- a/src/collector.rs +++ b/src/collector.rs @@ -1,10 +1,10 @@ -// Or we'll get deprecation warnings from our own deprecated type (seriously Rust?) -#![allow(deprecated)] +use std::sync::Arc; use futures::future::pending; use futures::{Stream, StreamExt as _}; use crate::gateway::{CollectorCallback, ShardMessenger}; +use crate::internal::prelude::*; use crate::model::prelude::*; /// Fundamental collector function. All collector types in this module are just wrappers around @@ -37,7 +37,7 @@ pub fn collect( let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); // Register an event callback in the shard. It's kept alive as long as we return `true` - shard.add_collector(CollectorCallback(Box::new(move |event| match extractor(event) { + shard.add_collector(CollectorCallback(Arc::new(move |event| match extractor(event) { // If this event matches, we send it to the receiver stream Some(item) => sender.send(item).is_ok(), None => !sender.is_closed(), @@ -66,9 +66,9 @@ macro_rules! make_specific_collector { impl $collector_type { /// Creates a new collector without any filters configured. - pub fn new(shard: impl AsRef) -> Self { + pub fn new(shard: ShardMessenger) -> Self { Self { - shard: shard.as_ref().clone(), + shard, duration: None, filter: None, $( $filter_name: None, )* @@ -88,7 +88,7 @@ macro_rules! make_specific_collector { } $( - #[doc = concat!("Filters [`", stringify!($item_type), "`]'s by a specific [`", stringify!($filter_type), "`].")] + #[doc = concat!("Filters [`", stringify!($item_type), "`]'s by a specific [`type@", stringify!($filter_type), "`].")] pub fn $filter_name(mut self, $filter_name: $filter_type) -> Self { self.$filter_name = Some($filter_name); self @@ -127,12 +127,6 @@ macro_rules! make_specific_collector { stream.take_until(Box::pin(timeout)) } - /// Deprecated, use [`Self::stream()`] instead. - #[deprecated = "use `.stream()` instead"] - pub fn build(self) -> impl Stream { - self.stream() - } - #[doc = concat!("Returns the next [`", stringify!($item_type), "`] which passes the filters.")] #[doc = concat!("You can also call `.await` on the [`", stringify!($collector_type), "`] directly.")] pub async fn next(self) -> Option<$item_type> { @@ -167,7 +161,7 @@ make_specific_collector!( channel_id: ChannelId => interaction.channel_id == *channel_id, guild_id: GuildId => interaction.guild_id.map_or(true, |x| x == *guild_id), message_id: MessageId => interaction.message.id == *message_id, - custom_ids: Vec => custom_ids.contains(&interaction.data.custom_id), + custom_ids: FixedArray => custom_ids.contains(&interaction.data.custom_id), ); make_specific_collector!( ModalInteractionCollector, ModalInteraction, @@ -178,7 +172,7 @@ make_specific_collector!( channel_id: ChannelId => interaction.channel_id == *channel_id, guild_id: GuildId => interaction.guild_id.map_or(true, |g| g == *guild_id), message_id: MessageId => interaction.message.as_ref().map_or(true, |m| m.id == *message_id), - custom_ids: Vec => custom_ids.contains(&interaction.data.custom_id), + custom_ids: Vec => custom_ids.contains(&interaction.data.custom_id), ); make_specific_collector!( ReactionCollector, Reaction, @@ -195,8 +189,3 @@ make_specific_collector!( channel_id: ChannelId => message.channel_id == *channel_id, guild_id: GuildId => message.guild_id.map_or(true, |g| g == *guild_id), ); -make_specific_collector!( - #[deprecated = "prefer the stand-alone collect() function to collect arbitrary events"] - EventCollector, Event, - event => event, -); diff --git a/src/constants.rs b/src/constants.rs index 953051fd07f..86a9d925cf6 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,5 +1,7 @@ //! A set of constants used by the library. +use nonmax::NonMaxU16; + /// The maximum length of the textual size of an embed. pub const EMBED_MAX_LENGTH: usize = 6000; @@ -19,7 +21,10 @@ pub const LARGE_THRESHOLD: u8 = 250; pub const MESSAGE_CODE_LIMIT: usize = 2000; /// The maximum number of members the bot can fetch at once -pub const MEMBER_FETCH_LIMIT: u64 = 1000; +pub const MEMBER_FETCH_LIMIT: NonMaxU16 = match NonMaxU16::new(1000) { + Some(m) => m, + None => unreachable!(), +}; /// The [UserAgent] sent along with every request. /// @@ -35,7 +40,6 @@ enum_number! { /// /// [Discord docs](https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-opcodes). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum Opcode { /// Dispatches an event. diff --git a/src/error.rs b/src/error.rs index dc449c3af91..eded864a826 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,12 +1,11 @@ use std::error::Error as StdError; -use std::fmt::{self, Error as FormatError}; +use std::fmt; use std::io::Error as IoError; #[cfg(feature = "http")] use reqwest::{header::InvalidHeaderValue, Error as ReqwestError}; #[cfg(feature = "gateway")] use tokio_tungstenite::tungstenite::error::Error as TungsteniteError; -use tracing::instrument; #[cfg(feature = "client")] use crate::client::ClientError; @@ -15,7 +14,6 @@ use crate::gateway::GatewayError; #[cfg(feature = "http")] use crate::http::HttpError; use crate::internal::prelude::*; -use crate::json::JsonError; use crate::model::ModelError; /// The common result type between most library functions. @@ -32,42 +30,14 @@ pub type Result = StdResult; #[derive(Debug)] #[non_exhaustive] pub enum Error { - /// An error while decoding a payload. - Decode(&'static str, Value), - /// There was an error with a format. - Format(FormatError), /// An [`std::io`] error. Io(IoError), - #[cfg_attr(not(feature = "simd_json"), doc = "An error from the [`serde_json`] crate.")] - #[cfg_attr(feature = "simd_json", doc = "An error from the [`simd_json`] crate.")] - Json(JsonError), + /// An error from the [`serde_json`] crate. + Json(serde_json::Error), /// An error from the [`model`] module. /// /// [`model`]: crate::model Model(ModelError), - /// Input exceeded a limit. Providing the input and the limit that's not supposed to be - /// exceeded. - /// - /// *This only exists for the [`GuildId::ban`] and [`Member::ban`] functions. For their cases, - /// it's the "reason".* - /// - /// [`GuildId::ban`]: crate::model::id::GuildId::ban - /// [`Member::ban`]: crate::model::guild::Member::ban - ExceededLimit(String, u32), - /// The input is not in the specified range. Returned by [`GuildId::members`], - /// [`Guild::members`] and [`PartialGuild::members`] - /// - /// (param_name, value, range_min, range_max) - /// - /// [`GuildId::members`]: crate::model::id::GuildId::members - /// [`Guild::members`]: crate::model::guild::Guild::members - /// [`PartialGuild::members`]: crate::model::guild::PartialGuild::members - NotInRange(&'static str, u64, u64, u64), - /// Some other error. This is only used for "Expected value \" errors, when a more - /// detailed error can not be easily provided via the [`Error::Decode`] variant. - Other(&'static str), - /// An error from the [`url`] crate. - Url(String), /// A [client] error. /// /// [client]: crate::client @@ -85,13 +55,7 @@ pub enum Error { Http(HttpError), /// An error from the `tungstenite` crate. #[cfg(feature = "gateway")] - Tungstenite(TungsteniteError), -} - -impl From for Error { - fn from(e: FormatError) -> Error { - Error::Format(e) - } + Tungstenite(Box), } #[cfg(feature = "gateway")] @@ -107,8 +71,8 @@ impl From for Error { } } -impl From for Error { - fn from(e: JsonError) -> Error { +impl From for Error { + fn from(e: serde_json::Error) -> Error { Error::Json(e) } } @@ -122,7 +86,7 @@ impl From for Error { #[cfg(feature = "gateway")] impl From for Error { fn from(e: TungsteniteError) -> Error { - Error::Tungstenite(e) + Error::Tungstenite(Box::new(e)) } } @@ -150,14 +114,9 @@ impl From for Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Decode(msg, _) | Self::Other(msg) => f.write_str(msg), - Self::ExceededLimit(..) => f.write_str("Input exceeded a limit"), - Self::NotInRange(..) => f.write_str("Input is not in the specified range"), - Self::Format(inner) => fmt::Display::fmt(&inner, f), Self::Io(inner) => fmt::Display::fmt(&inner, f), Self::Json(inner) => fmt::Display::fmt(&inner, f), Self::Model(inner) => fmt::Display::fmt(&inner, f), - Self::Url(msg) => f.write_str(msg), #[cfg(feature = "client")] Self::Client(inner) => fmt::Display::fmt(&inner, f), #[cfg(feature = "gateway")] @@ -171,20 +130,20 @@ impl fmt::Display for Error { } impl StdError for Error { - #[instrument] + #[cfg_attr(feature = "tracing_instrument", instrument)] fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { - Self::Format(inner) => Some(inner), Self::Io(inner) => Some(inner), Self::Json(inner) => Some(inner), Self::Model(inner) => Some(inner), #[cfg(feature = "client")] Self::Client(inner) => Some(inner), + #[cfg(feature = "gateway")] + Self::Gateway(inner) => Some(inner), #[cfg(feature = "http")] Self::Http(inner) => Some(inner), #[cfg(feature = "gateway")] Self::Tungstenite(inner) => Some(inner), - _ => None, } } } diff --git a/src/framework/mod.rs b/src/framework/mod.rs index 1ca3ae398a2..9118b0ac526 100644 --- a/src/framework/mod.rs +++ b/src/framework/mod.rs @@ -2,91 +2,10 @@ //! //! This is used in combination with [`ClientBuilder::framework`]. //! -//! The framework has a number of configurations, and can have any number of commands bound to it. -//! The primary purpose of it is to offer the utility of not needing to manually match message -//! content strings to determine if a message is a command. -//! -//! Additionally, "checks" can be added to commands, to ensure that a certain condition is met -//! prior to calling a command; this could be a check that the user who posted a message owns the -//! bot, for example. -//! -//! Each command has a given name, and an associated function/closure. For example, you might have -//! two commands: `"ping"` and `"weather"`. These each have an associated function that are called -//! if the framework determines that a message is of that command. -//! -//! Assuming a command prefix of `"~"`, then the following would occur with the two previous -//! commands: -//! -//! ```ignore -//! ~ping // calls the ping command's function -//! ~pin // does not -//! ~ ping // _does_ call it _if_ the `allow_whitespace` option is enabled -//! ~~ping // does not -//! ``` -//! -//! # Examples -//! -//! Configuring a Client with a framework, which has a prefix of `"~"` and a ping and about -//! command: -//! -//! ```rust,no_run -//! use serenity::framework::standard::macros::{command, group}; -//! use serenity::framework::standard::{CommandResult, Configuration, StandardFramework}; -//! use serenity::model::channel::Message; -//! use serenity::prelude::*; -//! -//! #[command] -//! async fn about(ctx: &Context, msg: &Message) -> CommandResult { -//! msg.channel_id.say(&ctx.http, "A simple test bot").await?; -//! -//! Ok(()) -//! } -//! -//! #[command] -//! async fn ping(ctx: &Context, msg: &Message) -> CommandResult { -//! msg.channel_id.say(&ctx.http, "pong!").await?; -//! -//! Ok(()) -//! } -//! -//! #[group] -//! #[commands(about, ping)] -//! struct General; -//! -//! struct Handler; -//! -//! impl EventHandler for Handler {} -//! -//! # async fn run() -> Result<(), Box> { -//! let token = std::env::var("DISCORD_TOKEN")?; -//! -//! let framework = StandardFramework::new() -//! // The `#[group]` (and similarly, `#[command]`) macro generates static instances -//! // containing any options you gave it. For instance, the group `name` and its `commands`. -//! // Their identifiers, names you can use to refer to these instances in code, are an -//! // all-uppercased version of the `name` with a `_GROUP` suffix appended at the end. -//! .group(&GENERAL_GROUP); -//! -//! framework.configure(Configuration::new().prefix("~")); -//! -//! let mut client = Client::builder(&token, GatewayIntents::default()) -//! .event_handler(Handler) -//! .framework(framework) -//! .await?; -//! # Ok(()) -//! # } -//! ``` -//! //! [`ClientBuilder::framework`]: crate::client::ClientBuilder::framework -#[cfg(feature = "standard_framework")] -pub mod standard; - use async_trait::async_trait; -#[cfg(feature = "standard_framework")] -#[allow(deprecated)] -pub use self::standard::StandardFramework; use crate::client::{Client, Context, FullEvent}; /// A trait for defining your own framework for serenity to use. @@ -102,7 +21,15 @@ pub trait Framework: Send + Sync { let _: &Client = client; } /// Called on every incoming event. - async fn dispatch(&self, ctx: Context, event: FullEvent); + async fn dispatch(&self, ctx: &Context, event: &FullEvent); + /// If [`Framework::dispatch`] should be called + /// automatically (`true`) or manually by the developer (`false`) + /// + /// This allows the developer to make checks and call other functions before commands are + /// handled at all. + fn dispatch_automatically(&self) -> bool { + true + } } #[async_trait] @@ -113,7 +40,7 @@ where async fn init(&mut self, client: &Client) { (**self).init(client).await; } - async fn dispatch(&self, ctx: Context, event: FullEvent) { + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { (**self).dispatch(ctx, event).await; } } @@ -126,7 +53,7 @@ where async fn init(&mut self, client: &Client) { (**self).init(client).await; } - async fn dispatch(&self, ctx: Context, event: FullEvent) { + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { (**self).dispatch(ctx, event).await; } } diff --git a/src/framework/standard/args.rs b/src/framework/standard/args.rs deleted file mode 100644 index 84face55ef0..00000000000 --- a/src/framework/standard/args.rs +++ /dev/null @@ -1,998 +0,0 @@ -use std::borrow::Cow; -use std::error::Error as StdError; -use std::fmt; -use std::marker::PhantomData; -use std::str::FromStr; - -use uwl::Stream; - -/// Defines how an operation on an [`Args`] method failed. -#[derive(Debug)] -#[non_exhaustive] -pub enum Error { - /// "END-OF-STRING". We reached the end. There's nothing to parse anymore. - Eos, - /// Parsing operation failed. Contains how it did. - Parse(E), -} - -impl From for Error { - fn from(e: E) -> Self { - Error::Parse(e) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Eos => f.write_str(r#"ArgError("end of string")"#), - Self::Parse(e) => write!(f, "ArgError(\"{e}\")"), - } - } -} - -impl StdError for Error {} - -type Result = ::std::result::Result>; - -/// Dictates how [`Args`] should split arguments, if by one character, or a string. -#[derive(Debug, Clone)] -pub enum Delimiter { - Single(char), - Multiple(String), -} - -impl Delimiter { - #[inline] - fn to_str(&self) -> Cow<'_, str> { - match self { - Self::Single(c) => Cow::Owned(c.to_string()), - Self::Multiple(s) => Cow::Borrowed(s), - } - } -} - -impl From for Delimiter { - #[inline] - fn from(c: char) -> Delimiter { - Delimiter::Single(c) - } -} - -impl From for Delimiter { - #[inline] - fn from(s: String) -> Delimiter { - Delimiter::Multiple(s) - } -} - -impl<'a> From<&'a String> for Delimiter { - #[inline] - fn from(s: &'a String) -> Delimiter { - Delimiter::Multiple(s.clone()) - } -} - -impl<'a> From<&'a str> for Delimiter { - #[inline] - fn from(s: &'a str) -> Delimiter { - Delimiter::Multiple(s.to_string()) - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -#[allow(clippy::enum_variant_names)] -enum TokenKind { - Argument, - QuotedArgument, -} - -#[derive(Clone, Copy, Debug)] -struct Token { - kind: TokenKind, - span: (usize, usize), -} - -impl Token { - #[inline] - fn new(kind: TokenKind, start: usize, end: usize) -> Self { - Token { - kind, - span: (start, end), - } - } -} - -// A utility enum to handle an edge case with Apple OSs. -// -// By default, a feature called "Smart Quotes" is enabled on MacOS and iOS devices. This feature -// automatically substitutes the lame, but simple `"` ASCII character for quotation with the cool -// `”` Unicode character. It can be disabled, but users may not want to do that as it is a global -// setting (i.e. they might not want to disable it just for properly invoking commands of bots on -// Discord). -#[derive(Clone, Copy)] -enum QuoteKind { - Ascii, - Apple, -} - -impl QuoteKind { - fn new(c: char) -> Option { - match c { - '"' => Some(QuoteKind::Ascii), - '\u{201C}' => Some(QuoteKind::Apple), - _ => None, - } - } - - fn is_ending_quote(self, c: char) -> bool { - match self { - Self::Ascii => c == '"', - Self::Apple => c == '\u{201D}', - } - } -} - -fn lex(stream: &mut Stream<'_>, delims: &[Cow<'_, str>]) -> Option { - if stream.is_empty() { - return None; - } - - let start = stream.offset(); - if let Some(kind) = QuoteKind::new(stream.current_char()?) { - stream.next_char(); - - let mut prev_was_backslash = false; - stream.take_until_char(|c| { - let result = kind.is_ending_quote(c) && !prev_was_backslash; - prev_was_backslash = c == '\\'; - result - }); - - let is_quote = stream.current_char().is_some_and(|c| kind.is_ending_quote(c)); - stream.next_char(); - - let end = stream.offset(); - - // Remove possible delimiters after the quoted argument. - for delim in delims { - stream.eat(delim); - } - - return Some(if is_quote { - Token::new(TokenKind::QuotedArgument, start, end) - } else { - // We're missing an end quote. View this as a normal argument. - Token::new(TokenKind::Argument, start, stream.len()) - }); - } - - let mut end = start; - - 'outer: while !stream.is_empty() { - for delim in delims { - end = stream.offset(); - - if stream.eat(delim) { - break 'outer; - } - } - - stream.next_char(); - end = stream.offset(); - } - - Some(Token::new(TokenKind::Argument, start, end)) -} - -fn is_surrounded_with(s: &str, begin: char, end: char) -> bool { - s.starts_with(begin) && s.ends_with(end) -} - -fn is_quoted(s: &str) -> bool { - if s.len() < 2 { - return false; - } - - // Refer to `QuoteKind` why we check for Unicode quote characters. - is_surrounded_with(s, '"', '"') || is_surrounded_with(s, '\u{201C}', '\u{201D}') -} - -fn strip(s: &str, begin: char, end: char) -> Option<&str> { - let s = s.strip_prefix(begin)?; - s.strip_suffix(end) -} - -fn remove_quotes(s: &str) -> &str { - if s.len() < 2 { - return s; - } - - if let Some(s) = strip(s, '"', '"') { - return s; - } - - // Refer to `QuoteKind` why we check for Unicode quote characters. - strip(s, '\u{201C}', '\u{201D}').unwrap_or(s) -} - -#[derive(Clone, Copy, Debug)] -enum State { - None, - Quoted, - Trimmed, - // Preserve the order they were called. - QuotedTrimmed, - TrimmedQuoted, -} - -/// A utility struct for handling "arguments" of a command. -/// -/// An "argument" is a part of the message up that ends at one of the specified delimiters, or the -/// end of the message. -/// -/// # Example -/// -/// ```rust -/// use serenity::framework::standard::{Args, Delimiter}; -/// -/// let mut args = Args::new("hello world!", &[Delimiter::Single(' ')]); // A space is our delimiter. -/// -/// // Parse our argument as a `String` and assert that it's the "hello" part of the message. -/// assert_eq!(args.single::().unwrap(), "hello"); -/// // Same here. -/// assert_eq!(args.single::().unwrap(), "world!"); -/// ``` -/// -/// We can also parse "quoted arguments" (no pun intended): -/// -/// ```rust -/// use serenity::framework::standard::{Args, Delimiter}; -/// -/// // Let us imagine this scenario: -/// // You have a `photo` command that grabs the avatar url of a user. This command accepts names only. -/// // Now, one of your users wants the avatar of a user named Princess Zelda. -/// // Problem is, her name contains a space; our delimiter. This would result in two arguments, "Princess" and "Zelda". -/// // So how shall we get around this? Through quotes! By surrounding her name in them we can perceive it as one single argument. -/// let mut args = Args::new(r#""Princess Zelda""#, &[Delimiter::Single(' ')]); -/// -/// // Hooray! -/// assert_eq!(args.single_quoted::().unwrap(), "Princess Zelda"); -/// ``` -/// -/// In case of a mistake, we can go back in time... er I mean, one step (or entirely): -/// -/// ```rust -/// use serenity::framework::standard::{Args, Delimiter}; -/// -/// let mut args = Args::new("4 2", &[Delimiter::Single(' ')]); -/// -/// assert_eq!(args.single::().unwrap(), 4); -/// -/// // Oh wait, oops, meant to double the 4. -/// // But I won't able to access it now... -/// // oh wait, I can `rewind`. -/// args.rewind(); -/// -/// assert_eq!(args.single::().unwrap() * 2, 8); -/// -/// // And the same for the 2 -/// assert_eq!(args.single::().unwrap() * 2, 4); -/// -/// // WAIT, NO. I wanted to concatenate them into a "42" string... -/// // Argh, what should I do now???? -/// // .... -/// // oh, `restore` -/// args.restore(); -/// -/// let res = format!("{}{}", args.single::().unwrap(), args.single::().unwrap()); -/// -/// // Yay. -/// assert_eq!(res, "42"); -/// ``` -/// -/// Hmm, taking a glance at the prior example, it seems we have an issue with reading the same -/// argument over and over. Is there a more sensible solution than rewinding...? Actually, there is! -/// The [`Self::current`] and [`Self::parse`] methods: -/// -/// ```rust -/// use serenity::framework::standard::{Args, Delimiter}; -/// -/// let mut args = Args::new("trois cinq quatre six", &[Delimiter::Single(' ')]); -/// -/// assert_eq!(args.parse::().unwrap(), "trois"); -/// -/// // It might suggest we've lost the `trois`. But in fact, we didn't! And not only that, we can do it an infinite amount of times! -/// assert_eq!(args.parse::().unwrap(), "trois"); -/// assert_eq!(args.current(), Some("trois")); -/// assert_eq!(args.parse::().unwrap(), "trois"); -/// assert_eq!(args.current(), Some("trois")); -/// -/// // Only if we use its brother method we'll then lose it. -/// assert_eq!(args.single::().unwrap(), "trois"); -/// assert_eq!(args.single::().unwrap(), "cinq"); -/// assert_eq!(args.single::().unwrap(), "quatre"); -/// assert_eq!(args.single::().unwrap(), "six"); -/// ``` -#[derive(Clone, Debug)] -pub struct Args { - message: String, - args: Vec, - offset: usize, - state: State, -} - -impl Args { - /// Create a new instance of [`Args`] for parsing arguments. - /// - /// For more reference, look at [`Args`]'s struct documentation. - /// - /// # Example - /// - /// ```rust - /// use serenity::framework::standard::{Args, Delimiter}; - /// - /// let mut args = Args::new( - /// // Our message from which we'll parse over. - /// "the quick brown fox jumps over the lazy", - /// - /// // The "delimiters", or aka the separators. They denote how we distinguish arguments as their own. - /// // For this example, we'll use one delimiter, the space (`0x20`), which will separate the message. - /// &[Delimiter::Single(' ')], - /// ); - /// - /// assert_eq!(args.single::().unwrap(), "the"); - /// assert_eq!(args.single::().unwrap(), "quick"); - /// assert_eq!(args.single::().unwrap(), "brown"); - /// - /// // We shall not see `the quick brown` again. - /// assert_eq!(args.rest(), "fox jumps over the lazy"); - /// ``` - #[must_use] - pub fn new(message: &str, possible_delimiters: &[Delimiter]) -> Self { - let delims = possible_delimiters - .iter() - .filter(|d| match d { - Delimiter::Single(c) => message.contains(*c), - Delimiter::Multiple(s) => message.contains(s), - }) - .map(Delimiter::to_str) - .collect::>(); - - let args = if delims.is_empty() { - let msg = message.trim(); - let kind = if is_quoted(msg) { TokenKind::QuotedArgument } else { TokenKind::Argument }; - - if msg.is_empty() { - Vec::new() - } else { - // If there are no delimiters, then the only possible argument is the whole - // message. - vec![Token::new(kind, 0, message.len())] - } - } else { - let mut args = Vec::new(); - let mut stream = Stream::new(message); - - while let Some(token) = lex(&mut stream, &delims) { - // Ignore empty arguments. - if message[token.span.0..token.span.1].is_empty() { - continue; - } - - args.push(token); - } - - args - }; - - Args { - args, - message: message.to_string(), - offset: 0, - state: State::None, - } - } - - #[inline] - fn span(&self) -> (usize, usize) { - self.args[self.offset].span - } - - #[inline] - fn slice(&self) -> &str { - let (start, end) = self.span(); - - &self.message[start..end] - } - - /// Move to the next argument. This increments the offset pointer. - /// - /// Does nothing if the message is empty. - pub fn advance(&mut self) -> &mut Self { - if self.is_empty() { - return self; - } - - self.offset += 1; - - self - } - - /// Go one step behind. This decrements the offset pointer. - /// - /// Does nothing if the offset pointer is `0`. - #[inline] - pub fn rewind(&mut self) -> &mut Self { - if self.offset == 0 { - return self; - } - - self.offset -= 1; - - self - } - - /// Go back to the starting point. - #[inline] - pub fn restore(&mut self) { - self.offset = 0; - } - - fn apply<'a>(&self, s: &'a str) -> &'a str { - fn trim(s: &str) -> &str { - let trimmed = s.trim(); - - // Search where the argument starts and ends between the whitespace. - let start = s.find(trimmed).unwrap_or(0); - let end = start + trimmed.len(); - - &s[start..end] - } - - let mut s = s; - - match self.state { - State::None => {}, - State::Quoted => { - s = remove_quotes(s); - }, - State::Trimmed => { - s = trim(s); - }, - State::QuotedTrimmed => { - s = remove_quotes(s); - s = trim(s); - }, - State::TrimmedQuoted => { - s = trim(s); - s = remove_quotes(s); - }, - } - - s - } - - /// Retrieve the current argument. - /// - /// Applies modifications set by [`Self::trimmed`] and [`Self::quoted`]. - /// - /// # Note - /// - /// This borrows [`Args`] for the entire lifetime of the returned argument. - /// - /// # Examples - /// - /// ```rust - /// use serenity::framework::standard::{Args, Delimiter}; - /// - /// let mut args = Args::new("4 2", &[Delimiter::Single(' ')]); - /// - /// assert_eq!(args.current(), Some("4")); - /// args.advance(); - /// assert_eq!(args.current(), Some("2")); - /// args.advance(); - /// assert_eq!(args.current(), None); - /// ``` - #[inline] - #[must_use] - pub fn current(&self) -> Option<&str> { - if self.is_empty() { - return None; - } - - let mut s = self.slice(); - s = self.apply(s); - - Some(s) - } - - /// Apply trimming of whitespace to all arguments. - /// - /// # Examples - /// - /// ```rust - /// use serenity::framework::standard::Args; - /// - /// let mut args = Args::new(" 42 ", &[]); - /// - /// // trimmed lasts for the whole lifetime of `Args` - /// args.trimmed(); - /// assert_eq!(args.current(), Some("42")); - /// // or until we decide ourselves - /// args.untrimmed(); - /// assert_eq!(args.current(), Some(" 42 ")); - /// assert_eq!(args.message(), " 42 "); - /// ``` - pub fn trimmed(&mut self) -> &mut Self { - match self.state { - State::None => self.state = State::Trimmed, - State::Quoted => self.state = State::QuotedTrimmed, - _ => {}, - } - - self - } - - /// Halt trimming of whitespace to all arguments. - /// - /// # Examples - /// - /// Refer to [`Self::trimmed`]'s examples. - pub fn untrimmed(&mut self) -> &mut Self { - match self.state { - State::Trimmed => self.state = State::None, - State::QuotedTrimmed | State::TrimmedQuoted => self.state = State::Quoted, - _ => {}, - } - - self - } - - /// Remove quotations surrounding all arguments. - /// - /// Note that only the quotes of the argument are taken into account. The quotes in the message - /// are preserved. - /// - /// # Examples - /// - /// ```rust - /// use serenity::framework::standard::Args; - /// - /// let mut args = Args::new("\"42\"", &[]); - /// - /// // `quoted` lasts the whole lifetime of `Args` - /// args.quoted(); - /// assert_eq!(args.current(), Some("42")); - /// // or until we decide - /// args.unquoted(); - /// assert_eq!(args.current(), Some("\"42\"")); - /// assert_eq!(args.message(), "\"42\""); - /// ``` - pub fn quoted(&mut self) -> &mut Self { - if self.is_empty() { - return self; - } - - let is_quoted = self.args[self.offset].kind == TokenKind::QuotedArgument; - - if is_quoted { - match self.state { - State::None => self.state = State::Quoted, - State::Trimmed => self.state = State::TrimmedQuoted, - _ => {}, - } - } - - self - } - - /// Stop removing quotations of all arguments. - /// - /// # Examples - /// - /// Refer to [`Self::quoted`]'s examples. - pub fn unquoted(&mut self) -> &mut Self { - match self.state { - State::Quoted => self.state = State::None, - State::QuotedTrimmed | State::TrimmedQuoted => self.state = State::Trimmed, - _ => {}, - } - - self - } - - /// Parse the current argument. - /// - /// Modifications of [`Self::trimmed`] and [`Self::quoted`] are also applied if they were - /// called. - /// - /// # Examples - /// - /// ```rust - /// use serenity::framework::standard::{Args, Delimiter}; - /// - /// let mut args = Args::new("4 2", &[Delimiter::Single(' ')]); - /// - /// assert_eq!(args.parse::().unwrap(), 4); - /// assert_eq!(args.current(), Some("4")); - /// ``` - /// - /// # Errors - /// - /// May return either [`Error::Parse`] if a parse error occurs, or - /// [`Error::Eos`] if there are no further remaining args. - #[inline] - pub fn parse(&self) -> Result { - T::from_str(self.current().ok_or(Error::Eos)?).map_err(Error::Parse) - } - - /// Parse the current argument and advance. - /// - /// Shorthand for calling [`Self::parse`], storing the result, calling [`Self::advance`] and - /// returning the result. - /// - /// # Examples - /// - /// ```rust - /// use serenity::framework::standard::{Args, Delimiter}; - /// - /// let mut args = Args::new("4 2", &[Delimiter::Single(' ')]); - /// - /// assert_eq!(args.single::().unwrap(), 4); - /// - /// // `4` is now out of the way. Next we have `2` - /// assert_eq!(args.single::().unwrap(), 2); - /// assert!(args.is_empty()); - /// ``` - /// - /// # Errors - /// - /// May return the same errors as `parse`. - #[inline] - pub fn single(&mut self) -> Result { - let p = self.parse::()?; - self.advance(); - Ok(p) - } - - /// Remove surrounding quotations, if present, from the argument; parse it and advance. - /// - /// Shorthand for `.quoted().single::()` - /// - /// # Examples - /// - /// ```rust - /// use serenity::framework::standard::{Args, Delimiter}; - /// - /// let mut args = Args::new(r#""4" "2""#, &[Delimiter::Single(' ')]); - /// - /// assert_eq!(args.single_quoted::().unwrap(), "4"); - /// assert_eq!(args.single_quoted::().unwrap(), 2); - /// assert!(args.is_empty()); - /// ``` - /// - /// # Errors - /// - /// May return the same errors as [`Self::parse`]. - #[inline] - pub fn single_quoted(&mut self) -> Result { - let p = self.quoted().parse::()?; - self.advance(); - Ok(p) - } - - /// By starting from the current offset, iterate over any available arguments until there are - /// none. - /// - /// Modifications of [`Iter::trimmed`] and [`Iter::quoted`] are also applied to all arguments if - /// they were called. - /// - /// # Examples - /// - /// Assert that all of the numbers in the message are even. - /// - /// ```rust - /// use serenity::framework::standard::{Args, Delimiter}; - /// - /// let mut args = Args::new("4 2", &[Delimiter::Single(' ')]); - /// - /// for arg in args.iter::() { - /// // Zero troubles, zero worries. - /// let arg = arg.unwrap_or(0); - /// assert!(arg % 2 == 0); - /// } - /// - /// assert!(args.is_empty()); - /// ``` - #[inline] - pub fn iter(&mut self) -> Iter<'_, T> { - Iter { - args: self, - state: State::None, - _marker: PhantomData, - } - } - - /// Return an iterator over all unmodified arguments. - /// - /// # Examples - /// - /// Join the arguments by a comma and a space. - /// - /// ```rust - /// use serenity::framework::standard::{Args, Delimiter}; - /// - /// let args = Args::new("Harry Hermione Ronald", &[Delimiter::Single(' ')]); - /// - /// let protagonists = args.raw().collect::>().join(", "); - /// - /// assert_eq!(protagonists, "Harry, Hermione, Ronald"); - /// ``` - #[inline] - #[must_use] - pub fn raw(&self) -> RawArguments<'_> { - RawArguments { - tokens: &self.args, - msg: &self.message, - quoted: false, - } - } - - /// Return an iterator over all arguments, stripped of their quotations if any were present. - /// - /// # Examples - /// - /// ```rust - /// use serenity::framework::standard::{Args, Delimiter}; - /// - /// let args = Args::new("Saw \"The Mist\" \"A Quiet Place\"", &[Delimiter::Single(' ')]); - /// - /// let horror_movies = args.raw_quoted().collect::>(); - /// - /// assert_eq!(&*horror_movies, &["Saw", "The Mist", "A Quiet Place"]); - /// ``` - #[inline] - #[must_use] - pub fn raw_quoted(&self) -> RawArguments<'_> { - let mut raw = self.raw(); - raw.quoted = true; - raw - } - - /// Search for any available argument that can be parsed, and remove it from the arguments - /// queue. - /// - /// # Note - /// The removal is irreversible. And happens after the search *and* the parse were successful. - /// - /// # Note 2 - /// "Arguments queue" is the list which contains all arguments that were deemed unique as - /// defined by quotations and delimiters. The 'removed' argument can be, likewise, still - /// accessed via [`Self::message`]. - /// - /// # Examples - /// - /// ```rust - /// use serenity::framework::standard::{Args, Delimiter}; - /// - /// let mut args = Args::new("c4 2", &[Delimiter::Single(' ')]); - /// - /// assert_eq!(args.find::().unwrap(), 2); - /// assert_eq!(args.single::().unwrap(), "c4"); - /// assert!(args.is_empty()); - /// ``` - /// - /// # Errors - /// - /// Returns [`Error::Eos`] if no argument can be parsed. - pub fn find(&mut self) -> Result { - if self.is_empty() { - return Err(Error::Eos); - } - - let before = self.offset; - self.restore(); - - let Some(pos) = self.iter::().quoted().position(|res| res.is_ok()) else { - self.offset = before; - return Err(Error::Eos); - }; - - self.offset = pos; - let parsed = self.single_quoted::()?; - - self.args.remove(pos); - self.offset = before; - self.rewind(); - - Ok(parsed) - } - - /// Search for any available argument that can be parsed. - /// - /// # Examples - /// - /// ```rust - /// use serenity::framework::standard::{Args, Delimiter}; - /// - /// let mut args = Args::new("c4 2", &[Delimiter::Single(' ')]); - /// - /// assert_eq!(args.find_n::().unwrap(), 2); - /// - /// // The `2` is still here, so let's parse it again. - /// assert_eq!(args.single::().unwrap(), "c4"); - /// assert_eq!(args.single::().unwrap(), 2); - /// assert!(args.is_empty()); - /// ``` - /// - /// # Errors - /// - /// Returns [`Error::Eos`] if no argument can be parsed. - pub fn find_n(&mut self) -> Result { - if self.is_empty() { - return Err(Error::Eos); - } - - let before = self.offset; - self.restore(); - - let Some(pos) = self.iter::().quoted().position(|res| res.is_ok()) else { - self.offset = before; - return Err(Error::Eos); - }; - - self.offset = pos; - let parsed = self.quoted().parse::()?; - - self.offset = before; - - Ok(parsed) - } - - /// Get the original, unmodified message passed to the command. - #[inline] - #[must_use] - pub fn message(&self) -> &str { - &self.message - } - - /// Starting from the offset, return the remainder of available arguments. - #[inline] - #[must_use] - pub fn rest(&self) -> &str { - self.remains().unwrap_or_default() - } - - /// Starting from the offset, return the remainder of available arguments. - /// - /// Returns [`None`] if there are no remaining arguments. - #[inline] - #[must_use] - pub fn remains(&self) -> Option<&str> { - if self.is_empty() { - return None; - } - - let (start, _) = self.span(); - - Some(&self.message[start..]) - } - - /// Return the full amount of recognised arguments. The length of the "arguments queue". - /// - /// # Note - /// - /// The value returned is to be assumed to stay static. However, if [`Self::find`] was called - /// previously, and was successful, then the value is subtracted by one. - #[inline] - #[must_use] - pub fn len(&self) -> usize { - self.args.len() - } - - /// Assert that there are no more arguments left. - #[inline] - #[must_use] - pub fn is_empty(&self) -> bool { - self.offset >= self.len() - } - - /// Return the amount of arguments still available. - #[inline] - #[must_use] - pub fn remaining(&self) -> usize { - if self.is_empty() { - return 0; - } - - self.len() - self.offset - } -} - -/// Parse each argument individually, as an iterator. -pub struct Iter<'a, T: FromStr> { - args: &'a mut Args, - state: State, - _marker: PhantomData, -} - -#[allow(clippy::missing_errors_doc)] -impl<'a, T: FromStr> Iter<'a, T> { - /// Retrieve the current argument. - pub fn current(&mut self) -> Option<&str> { - self.args.state = self.state; - self.args.current() - } - - /// Parse the current argument independently. - pub fn parse(&mut self) -> Result { - self.args.state = self.state; - self.args.parse::() - } - - /// Remove surrounding quotation marks from all of the arguments. - #[inline] - pub fn quoted(&mut self) -> &mut Self { - match self.state { - State::None => self.state = State::Quoted, - State::Trimmed => self.state = State::TrimmedQuoted, - _ => {}, - } - - self - } - - /// Trim leading and trailing whitespace off all arguments. - #[inline] - pub fn trimmed(&mut self) -> &mut Self { - match self.state { - State::None => self.state = State::Trimmed, - State::Quoted => self.state = State::QuotedTrimmed, - _ => {}, - } - - self - } -} - -impl<'a, T: FromStr> Iterator for Iter<'a, T> { - type Item = Result; - - fn next(&mut self) -> Option { - if self.args.is_empty() { - None - } else { - let arg = self.parse(); - self.args.advance(); - Some(arg) - } - } -} - -/// Access to all of the arguments, as an iterator. -#[derive(Debug)] -pub struct RawArguments<'a> { - msg: &'a str, - tokens: &'a [Token], - quoted: bool, -} - -impl<'a> Iterator for RawArguments<'a> { - type Item = &'a str; - - #[inline] - fn next(&mut self) -> Option { - let (start, end) = self.tokens.first()?.span; - - self.tokens = &self.tokens[1..]; - - let mut s = &self.msg[start..end]; - - if self.quoted { - s = remove_quotes(s); - } - - Some(s) - } -} diff --git a/src/framework/standard/configuration.rs b/src/framework/standard/configuration.rs deleted file mode 100644 index 9121fb2e347..00000000000 --- a/src/framework/standard/configuration.rs +++ /dev/null @@ -1,607 +0,0 @@ -use std::collections::HashSet; - -use futures::future::BoxFuture; - -use super::Delimiter; -use crate::client::Context; -use crate::model::channel::Message; -use crate::model::id::{ChannelId, GuildId, UserId}; - -type DynamicPrefixHook = - for<'fut> fn(&'fut Context, &'fut Message) -> BoxFuture<'fut, Option>; - -/// A configuration struct for deciding whether the framework should allow optional whitespace -/// between prefixes, group prefixes and command names. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct WithWhiteSpace { - pub prefixes: bool, - pub groups: bool, - pub commands: bool, -} - -impl Default for WithWhiteSpace { - /// Impose the default settings to (false, true, true). - fn default() -> Self { - WithWhiteSpace { - prefixes: false, - groups: true, - commands: true, - } - } -} - -impl From for WithWhiteSpace { - /// Impose the prefix setting. - fn from(b: bool) -> Self { - // Assume that they want to do this for prefixes - WithWhiteSpace { - prefixes: b, - ..Default::default() - } - } -} - -impl From<(bool, bool)> for WithWhiteSpace { - /// Impose the prefix and group prefix settings. - fn from((prefixes, groups): (bool, bool)) -> Self { - WithWhiteSpace { - prefixes, - groups, - ..Default::default() - } - } -} - -impl From<(bool, bool, bool)> for WithWhiteSpace { - /// Impose the prefix, group prefix and command names settings. - fn from((prefixes, groups, commands): (bool, bool, bool)) -> Self { - WithWhiteSpace { - prefixes, - groups, - commands, - } - } -} - -/// The configuration to use for a [`StandardFramework`] associated with a [`Client`] instance. -/// -/// This allows setting configurations like the depth to search for commands, whether to treat -/// mentions like a command prefix, etc. -/// -/// To see the default values, refer to the [default implementation]. -/// -/// # Examples -/// -/// Responding to mentions and setting a command prefix of `"~"`: -/// -/// ```rust,no_run -/// # use serenity::prelude::*; -/// struct Handler; -/// -/// impl EventHandler for Handler {} -/// -/// use serenity::framework::standard::{Configuration, StandardFramework}; -/// use serenity::model::id::UserId; -/// use serenity::Client; -/// -/// # async fn run() -> Result<(), Box> { -/// let token = std::env::var("DISCORD_BOT_TOKEN")?; -/// -/// let framework = StandardFramework::new(); -/// framework.configure(Configuration::new().on_mention(Some(UserId::new(5))).prefix("~")); -/// -/// let mut client = Client::builder(&token, GatewayIntents::default()) -/// .event_handler(Handler) -/// .framework(framework) -/// .await?; -/// # Ok(()) -/// # } -/// ``` -/// -/// [`Client`]: crate::Client -/// [`StandardFramework`]: super::StandardFramework -/// [default implementation]: Self::default -#[derive(Clone)] -pub struct Configuration { - pub(crate) allow_dm: bool, - pub(crate) with_whitespace: WithWhiteSpace, - pub(crate) by_space: bool, - pub(crate) blocked_guilds: HashSet, - pub(crate) blocked_users: HashSet, - pub(crate) allowed_channels: HashSet, - pub(crate) disabled_commands: HashSet, - pub(crate) dynamic_prefixes: Vec, - pub(crate) ignore_bots: bool, - pub(crate) ignore_webhooks: bool, - pub(crate) on_mention: Option, - pub(crate) owners: HashSet, - pub(crate) prefixes: Vec, - pub(crate) no_dm_prefix: bool, - pub(crate) delimiters: Vec, - pub(crate) case_insensitive: bool, -} - -impl Configuration { - /// Alias for Configuration::default - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// If set to false, bot will ignore any private messages. - /// - /// **Note**: Defaults to `true`. - #[must_use] - pub fn allow_dm(mut self, allow_dm: bool) -> Self { - self.allow_dm = allow_dm; - self - } - - /// Whether to allow whitespace being optional between a prefix/group-prefix/command and a - /// command. - /// - /// **Note**: Defaults to `false` (for prefixes), `true` (commands), `true` (group prefixes). - /// - /// # Examples - /// - /// Setting `false` for prefixes will _only_ allow this scenario to occur: - /// - /// ```ignore - /// !about - /// - /// // bot processes and executes the "about" command if it exists - /// ``` - /// - /// while setting it to `true` will _also_ allow this scenario to occur: - /// - /// ```ignore - /// ! about - /// - /// // bot processes and executes the "about" command if it exists - /// ``` - #[must_use] - pub fn with_whitespace(mut self, with: impl Into) -> Self { - self.with_whitespace = with.into(); - self - } - - /// Whether the framework should split the message by a space first to parse the group or - /// command. If set to false, it will only test part of the message by the *length* of the - /// group's or command's names. - /// - /// **Note**: Defaults to `true` - #[must_use] - pub fn by_space(mut self, b: bool) -> Self { - self.by_space = b; - self - } - - /// HashSet of channels Ids where commands will be working. - /// - /// **Note**: Defaults to an empty HashSet. - /// - /// # Examples - /// - /// Create a HashSet in-place: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// use serenity::model::id::ChannelId; - /// - /// let framework = StandardFramework::new(); - /// framework.configure( - /// Configuration::new() - /// .allowed_channels(vec![ChannelId::new(7), ChannelId::new(77)].into_iter().collect()), - /// ); - /// ``` - #[must_use] - pub fn allowed_channels(mut self, channels: HashSet) -> Self { - self.allowed_channels = channels; - self - } - - /// HashSet of guild Ids where commands will be ignored. - /// - /// **Note**: Defaults to an empty HashSet. - /// - /// # Examples - /// - /// Create a HashSet in-place: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// use serenity::model::id::GuildId; - /// - /// let framework = StandardFramework::new(); - /// framework.configure( - /// Configuration::new() - /// .blocked_guilds(vec![GuildId::new(7), GuildId::new(77)].into_iter().collect()), - /// ); - /// ``` - #[must_use] - pub fn blocked_guilds(mut self, guilds: HashSet) -> Self { - self.blocked_guilds = guilds; - self - } - - /// HashSet of user Ids whose commands will be ignored. - /// - /// Guilds owned by user Ids will also be ignored. - /// - /// **Note**: Defaults to an empty HashSet. - /// - /// # Examples - /// - /// Create a HashSet in-place: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// use serenity::model::id::UserId; - /// - /// let framework = StandardFramework::new(); - /// framework.configure( - /// Configuration::new() - /// .blocked_users(vec![UserId::new(7), UserId::new(77)].into_iter().collect()), - /// ); - /// ``` - #[must_use] - pub fn blocked_users(mut self, users: HashSet) -> Self { - self.blocked_users = users; - self - } - - /// HashSet of command names that won't be run. - /// - /// **Note**: Defaults to an empty HashSet. - /// - /// # Examples - /// - /// Ignore a set of commands, assuming they exist: - /// - /// ```rust,no_run - /// use serenity::client::Context; - /// use serenity::framework::standard::macros::{command, group}; - /// use serenity::framework::standard::{CommandResult, Configuration}; - /// use serenity::framework::StandardFramework; - /// use serenity::model::channel::Message; - /// - /// #[command] - /// async fn ping(ctx: &Context, msg: &Message) -> CommandResult { - /// msg.channel_id.say(&ctx.http, "Pong!").await?; - /// Ok(()) - /// } - /// - /// #[group] - /// #[commands(ping)] - /// struct Peng; - /// - /// let disabled = vec!["ping"].into_iter().map(|x| x.to_string()).collect(); - /// - /// let framework = StandardFramework::new().group(&PENG_GROUP); - /// framework.configure(Configuration::new().disabled_commands(disabled)); - /// ``` - #[inline] - #[must_use] - pub fn disabled_commands(mut self, commands: HashSet) -> Self { - self.disabled_commands = commands; - self - } - - /// Sets the prefix to respond to dynamically, in addition to the one configured with - /// [`Self::prefix`] or [`Self::prefixes`]. This is useful if you want to have user - /// configurable per-guild or per-user prefixes, such as by fetching a guild's prefix from a - /// database accessible via [`Context::data`]. - /// - /// Return [`None`] to not have a special prefix for the dispatch and to only use the - /// configured prefix from [`Self::prefix`] or [`Self::prefixes`]. - /// - /// This method can be called many times to add more dynamic prefix hooks. - /// - /// **Note**: Defaults to no dynamic prefix check. - /// - /// **Note**: If using dynamic_prefix *without* [`Self::prefix`] or [`Self::prefixes`], there - /// will still be the default framework prefix of `"~"`. You can disable the default prefix by - /// setting the prefix to an empty string `""` with [`Self::prefix`]. - /// - /// # Examples - /// - /// If the Id of the channel is divisible by 5, use the prefix `"!"`, otherwise use `"*"`. The - /// default framework prefix `"~"` will always be valid in addition to the one returned by - /// dynamic_prefix. - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// - /// let framework = - /// StandardFramework::new().configure(Configuration::new().dynamic_prefix(|_, msg| { - /// Box::pin(async move { - /// Some(if msg.channel_id.get() % 5 == 0 { "!" } else { "*" }.to_string()) - /// }) - /// })); - /// ``` - /// - /// This will only use the prefix `"!"` or `"*"` depending on channel ID, - /// with the default prefix `"~"` disabled. - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// - /// let framework = StandardFramework::new(); - /// framework.configure( - /// Configuration::new() - /// .dynamic_prefix(|_, msg| { - /// Box::pin(async move { - /// Some(if msg.channel_id.get() % 5 == 0 { "!" } else { "*" }.to_string()) - /// }) - /// }) - /// .prefix(""), // This disables the default prefix "~" - /// ); - /// ``` - /// - /// [`Context::data`]: crate::client::Context::data - #[inline] - #[must_use] - pub fn dynamic_prefix(mut self, dynamic_prefix: DynamicPrefixHook) -> Self { - self.dynamic_prefixes.push(dynamic_prefix); - self - } - - /// Whether the bot should respond to other bots. - /// - /// For example, if this is set to false, then the bot will respond to any other bots including - /// itself. - /// - /// **Note**: Defaults to `true`. - #[must_use] - pub fn ignore_bots(mut self, ignore_bots: bool) -> Self { - self.ignore_bots = ignore_bots; - self - } - - /// If set to true, bot will ignore all commands called by webhooks. - /// - /// **Note**: Defaults to `true`. - #[must_use] - pub fn ignore_webhooks(mut self, ignore_webhooks: bool) -> Self { - self.ignore_webhooks = ignore_webhooks; - self - } - - /// Whether or not to respond to commands initiated with `id_to_mention`. - /// - /// **Note**: that this can be used in conjunction with [`Self::prefix`]. - /// - /// **Note**: Defaults to ignore mentions. - /// - /// # Examples - /// - /// Setting this to an ID will allow the following types of mentions to be responded to: - /// - /// ```ignore - /// <@245571012924538880> about - /// <@!245571012924538880> about - /// ``` - /// - /// The former is a direct mention, while the latter is a nickname mention, which aids mobile - /// devices in determining whether to display a user's nickname. It has no real meaning for - /// your bot, and the library encourages you to ignore differentiating between the two. - #[must_use] - pub fn on_mention(mut self, id_to_mention: Option) -> Self { - self.on_mention = id_to_mention.map(|id| id.to_string()); - self - } - - /// A [`HashSet`] of user Ids checks won't apply to. - /// - /// **Note**: Defaults to an empty HashSet. - /// - /// # Examples - /// - /// Create a HashSet in-place: - /// - /// ```rust,no_run - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// use serenity::model::id::UserId; - /// - /// let framework = StandardFramework::new(); - /// framework.configure( - /// Configuration::new().owners(vec![UserId::new(7), UserId::new(77)].into_iter().collect()), - /// ); - /// ``` - /// - /// Create a HashSet beforehand: - /// - /// ```rust,no_run - /// use std::collections::HashSet; - /// - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// use serenity::model::id::UserId; - /// - /// let mut set = HashSet::new(); - /// set.insert(UserId::new(7)); - /// set.insert(UserId::new(77)); - /// - /// let framework = StandardFramework::new(); - /// framework.configure(Configuration::new().owners(set)); - /// ``` - #[must_use] - pub fn owners(mut self, user_ids: HashSet) -> Self { - self.owners = user_ids; - self - } - - /// Sets the prefix to respond to. A prefix can be a string slice of any non-zero length. - /// - /// **Note**: Defaults to "~". - /// - /// **Note**: Passing empty string `""` will set no prefix. - /// - /// **Note**: This prefix will always be usable, even if there is a [`Self::dynamic_prefix`] - /// configured. - /// - /// # Examples - /// - /// Assign a basic prefix: - /// - /// ```rust,no_run - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// - /// let framework = StandardFramework::new(); - /// framework.configure(Configuration::new().prefix("!")); - /// ``` - #[must_use] - pub fn prefix(mut self, prefix: impl Into) -> Self { - let p = prefix.into(); - self.prefixes = if p.is_empty() { vec![] } else { vec![p] }; - self - } - - /// Sets the prefixes to respond to. Each can be a string slice of any non-zero length. - /// - /// **Note**: Refer to [`Self::prefix`] for the default value. - /// - /// **Note**: These prefixes will always be usable, even if there is a [`Self::dynamic_prefix`] - /// configured. - /// - /// # Examples - /// - /// Assign a set of prefixes the bot can respond to: - /// - /// ```rust,no_run - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// - /// let framework = StandardFramework::new(); - /// framework.configure(Configuration::new().prefixes(vec!["!", ">", "+"])); - /// ``` - #[inline] - #[must_use] - pub fn prefixes(mut self, prefixes: impl IntoIterator>) -> Self { - self.prefixes = prefixes.into_iter().map(Into::into).filter(|p| !p.is_empty()).collect(); - self - } - - /// Sets whether command execution can be done without a prefix. Works only in private channels. - /// - /// **Note**: Defaults to `false`. - /// - /// # Note - /// - /// The `cache` feature is required. If disabled this does absolutely nothing. - #[inline] - #[must_use] - pub fn no_dm_prefix(mut self, b: bool) -> Self { - self.no_dm_prefix = b; - self - } - - /// Sets a single delimiter to be used when splitting the content after a command. - /// - /// **Note**: Defaults to a vector with a single element of `' '`. - /// - /// # Examples - /// - /// Have the args be separated by a comma and a space: - /// - /// ```rust,no_run - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// - /// let framework = StandardFramework::new().configure(Configuration::new().delimiter(", ")); - /// ``` - #[must_use] - pub fn delimiter(mut self, delimiter: impl Into) -> Self { - self.delimiters.clear(); - self.delimiters.push(delimiter.into()); - - self - } - - /// Sets multiple delimiters to be used when splitting the content after a command. - /// Additionally cleans the default delimiter from the vector. - /// - /// **Note**: Refer to [`Self::delimiter`] for the default value. - /// - /// # Examples - /// - /// Have the args be separated by a comma and a space; and a regular space: - /// - /// ```rust,no_run - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// - /// let framework = StandardFramework::new(); - /// framework.configure(Configuration::new().delimiters(vec![", ", " "])); - /// ``` - #[must_use] - pub fn delimiters( - mut self, - delimiters: impl IntoIterator>, - ) -> Self { - self.delimiters.clear(); - self.delimiters.extend(delimiters.into_iter().map(Into::into)); - - self - } - - /// Whether the framework shouldn't care about the user's input if it's: `~command`, - /// `~Command`, or `~COMMAND`; `mayacommand`, `MayACommand`, `MAYACOMMAND`, et cetera. - /// - /// Setting this to `true` will result in *all* prefixes and command names to be case - /// insensitive. - /// - /// **Note**: Defaults to `false`. - #[must_use] - pub fn case_insensitivity(mut self, cs: bool) -> Self { - self.case_insensitive = cs; - - for prefix in &mut self.prefixes { - *prefix = prefix.to_lowercase(); - } - - self - } -} - -impl Default for Configuration { - /// Builds a default framework configuration, setting the following: - /// - /// - **allow_dm** to `true` - /// - **with_whitespace** to `(false, true, true)` - /// - **by_space** to `true` - /// - **blocked_guilds** to an empty HashSet - /// - **blocked_users** to an empty HashSet, - /// - **allowed_channels** to an empty HashSet, - /// - **case_insensitive** to `false` - /// - **delimiters** to `vec![' ']` - /// - **disabled_commands** to an empty HashSet - /// - **dynamic_prefixes** to an empty vector - /// - **ignore_bots** to `true` - /// - **ignore_webhooks** to `true` - /// - **no_dm_prefix** to `false` - /// - **on_mention** to `false` - /// - **owners** to an empty HashSet - /// - **prefix** to "~" - fn default() -> Configuration { - Configuration { - allow_dm: true, - with_whitespace: WithWhiteSpace::default(), - by_space: true, - blocked_guilds: HashSet::default(), - blocked_users: HashSet::default(), - allowed_channels: HashSet::default(), - case_insensitive: false, - delimiters: vec![Delimiter::Single(' ')], - disabled_commands: HashSet::default(), - dynamic_prefixes: Vec::new(), - ignore_bots: true, - ignore_webhooks: true, - no_dm_prefix: false, - on_mention: None, - owners: HashSet::default(), - prefixes: vec![String::from("~")], - } - } -} diff --git a/src/framework/standard/help_commands.rs b/src/framework/standard/help_commands.rs deleted file mode 100644 index c8120d775f7..00000000000 --- a/src/framework/standard/help_commands.rs +++ /dev/null @@ -1,1415 +0,0 @@ -//! A collection of default help commands for the framework. -//! -//! # Example -//! -//! Using the [`with_embeds`] function to have the framework's help message use embeds: -//! -//! ```rust,no_run -//! use std::collections::HashSet; -//! use std::env; -//! -//! use serenity::client::{Client, Context, EventHandler}; -//! use serenity::framework::standard::macros::help; -//! use serenity::framework::standard::{ -//! help_commands, -//! Args, -//! CommandGroup, -//! CommandResult, -//! HelpOptions, -//! StandardFramework, -//! }; -//! use serenity::model::prelude::{Message, UserId}; -//! -//! struct Handler; -//! -//! impl EventHandler for Handler {} -//! -//! #[help] -//! async fn my_help( -//! context: &Context, -//! msg: &Message, -//! args: Args, -//! help_options: &'static HelpOptions, -//! groups: &[&'static CommandGroup], -//! owners: HashSet, -//! ) -> CommandResult { -//! # #[cfg(all(feature = "cache", feature = "http"))] -//! # { -//! let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await; -//! Ok(()) -//! # } -//! # -//! # #[cfg(not(all(feature = "cache", feature = "http")))] -//! # Ok(()) -//! } -//! -//! let framework = StandardFramework::new().help(&MY_HELP); -//! ``` -//! -//! The same can be accomplished with no embeds by substituting `with_embeds` with the [`plain`] -//! function. - -#[cfg(all(feature = "cache", feature = "http"))] -use std::{collections::HashSet, fmt::Write}; - -#[cfg(all(feature = "cache", feature = "http"))] -use futures::future::{BoxFuture, FutureExt}; -#[cfg(all(feature = "cache", feature = "http"))] -use levenshtein::levenshtein; -#[cfg(all(feature = "cache", feature = "http"))] -use tracing::warn; - -#[cfg(all(feature = "cache", feature = "http"))] -use super::structures::Command as InternalCommand; -#[cfg(all(feature = "cache", feature = "http"))] -use super::{ - has_correct_permissions, - has_correct_roles, - Args, - Check, - CommandGroup, - CommandOptions, - HelpBehaviour, - HelpOptions, - OnlyIn, -}; -#[cfg(all(feature = "cache", feature = "http"))] -use crate::{ - builder::{CreateEmbed, CreateMessage}, - cache::Cache, - client::Context, - framework::standard::CommonOptions, - http::CacheHttp, - model::channel::Message, - model::id::{ChannelId, UserId}, - model::Colour, - Error, -}; - -/// Macro to format a command according to a [`HelpBehaviour`] or continue to the next command-name -/// upon hiding. -#[cfg(all(feature = "cache", feature = "http"))] -macro_rules! format_command_name { - ($behaviour:expr, $command_name:expr) => { - match $behaviour { - HelpBehaviour::Strike => format!("~~`{}`~~", $command_name), - HelpBehaviour::Nothing => format!("`{}`", $command_name), - HelpBehaviour::Hide => continue, - } - }; -} - -/// A single group containing its name and all related commands that are eligible in relation of -/// help-settings measured to the user. -#[derive(Clone, Debug, Default)] -pub struct GroupCommandsPair { - pub name: &'static str, - pub prefixes: Vec<&'static str>, - pub command_names: Vec, - pub summary: Option<&'static str>, - pub sub_groups: Vec, -} - -/// A single suggested command containing its name and Levenshtein distance to the actual user's -/// searched command name. -#[derive(Clone, Debug, Default)] -pub struct SuggestedCommandName { - pub name: String, - pub levenshtein_distance: usize, -} - -/// A single command containing all related pieces of information. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub struct Command<'a> { - pub name: &'static str, - pub group_name: &'static str, - pub group_prefixes: &'a [&'static str], - pub sub_commands: Vec, - pub aliases: Vec<&'static str>, - pub availability: &'a str, - pub description: Option<&'static str>, - pub usage: Option<&'static str>, - pub usage_sample: Vec<&'static str>, - pub checks: Vec, -} - -/// Contains possible suggestions in case a command could not be found but are similar enough. -#[derive(Clone, Debug, Default)] -pub struct Suggestions(pub Vec); - -#[cfg(all(feature = "cache", feature = "http"))] -impl Suggestions { - /// Immutably borrow inner [`Vec`]. - #[inline] - #[must_use] - pub fn as_vec(&self) -> &Vec { - &self.0 - } - - /// Concats names of suggestions with a given `separator`. - #[must_use] - pub fn join(&self, separator: &str) -> String { - match self.as_vec().as_slice() { - [] => String::new(), - [one] => one.name.clone(), - [first, rest @ ..] => { - let size = first.name.len() + rest.iter().map(|e| e.name.len()).sum::(); - let sep_size = rest.len() * separator.len(); - - let mut joined = String::with_capacity(size + sep_size); - joined.push_str(&first.name); - for e in rest { - joined.push_str(separator); - joined.push_str(&e.name); - } - joined - }, - } - } -} - -/// Covers possible outcomes of a help-request and yields relevant data in customised textual -/// representation. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub enum CustomisedHelpData<'a> { - /// To display suggested commands. - SuggestedCommands { help_description: String, suggestions: Suggestions }, - /// To display groups and their commands by name. - GroupedCommands { help_description: String, groups: Vec }, - /// To display one specific command. - SingleCommand { command: Command<'a> }, - /// To display failure in finding a fitting command. - NoCommandFound { help_error_message: &'a str }, -} - -/// Checks whether a user is member of required roles and given the required permissions. -#[cfg(feature = "cache")] -pub fn has_all_requirements(cache: impl AsRef, cmd: &CommandOptions, msg: &Message) -> bool { - let cache = cache.as_ref(); - - if let Some(guild_id) = msg.guild_id { - if let Some(member) = cache.member(guild_id, msg.author.id) { - if let Ok(permissions) = member.permissions(cache) { - return if cmd.allowed_roles.is_empty() { - permissions.administrator() || has_correct_permissions(cache, &cmd, msg) - } else if let Some(roles) = cache.guild_roles(guild_id) { - permissions.administrator() - || (has_correct_roles(&cmd, &roles, &member) - && has_correct_permissions(cache, &cmd, msg)) - } else { - warn!("Failed to find the guild and its roles."); - - false - }; - } - } - } - - cmd.only_in != OnlyIn::Guild -} - -/// Checks if `search_on` starts with `word` and is then cleanly followed by a `" "`. -#[inline] -#[cfg(all(feature = "cache", feature = "http"))] -fn starts_with_whole_word(search_on: &str, word: &str) -> bool { - search_on.starts_with(word) - && search_on.get(word.len()..=word.len()).is_some_and(|slice| slice == " ") -} - -// Decides how a listed help entry shall be displayed. -#[cfg(all(feature = "cache", feature = "http"))] -fn check_common_behaviour( - cache: impl AsRef, - msg: &Message, - options: &impl CommonOptions, - owners: &HashSet, - help_options: &HelpOptions, -) -> HelpBehaviour { - if !options.help_available() { - return HelpBehaviour::Hide; - } - - if options.only_in() == OnlyIn::Dm && !msg.is_private() - || options.only_in() == OnlyIn::Guild && msg.is_private() - { - return help_options.wrong_channel; - } - - if options.owners_only() && !owners.contains(&msg.author.id) { - return help_options.lacking_ownership; - } - - if options.owner_privilege() && owners.contains(&msg.author.id) { - return HelpBehaviour::Nothing; - } - - if !has_correct_permissions(&cache, options, msg) { - return help_options.lacking_permissions; - } - - msg.guild(cache.as_ref()) - .and_then(|guild| { - if let Some(member) = guild.members.get(&msg.author.id) { - if !has_correct_roles(options, &guild.roles, member) { - return Some(help_options.lacking_role); - } - } - - None - }) - .unwrap_or(HelpBehaviour::Nothing) -} - -#[cfg(all(feature = "cache", feature = "http"))] -async fn check_command_behaviour( - ctx: &Context, - msg: &Message, - options: &CommandOptions, - group_checks: &[&Check], - owners: &HashSet, - help_options: &HelpOptions, -) -> HelpBehaviour { - let behaviour = check_common_behaviour(ctx, msg, &options, owners, help_options); - - if behaviour == HelpBehaviour::Nothing - && (!options.owner_privilege || !owners.contains(&msg.author.id)) - { - for check in group_checks.iter().chain(options.checks) { - if !check.check_in_help { - continue; - } - - let mut args = Args::new("", &[]); - - if (check.function)(ctx, msg, &mut args, options).await.is_err() { - return help_options.lacking_conditions; - } - } - } - - behaviour -} - -// This function will recursively go through all commands and their sub-commands, trying to find -// `name`. Similar commands will be collected into `similar_commands`. -#[cfg(all(feature = "cache", feature = "http"))] -#[allow(clippy::too_many_arguments)] -fn nested_commands_search<'rec, 'a: 'rec>( - ctx: &'rec Context, - msg: &'rec Message, - group: &'rec CommandGroup, - commands: &'rec [&'static InternalCommand], - name: &'rec mut String, - help_options: &'a HelpOptions, - similar_commands: &'rec mut Vec, - owners: &'rec HashSet, -) -> BoxFuture<'rec, Option<&'a InternalCommand>> { - async move { - for command in commands { - let mut command = *command; - - let search_command_name_matched = { - let mut command_found = None; - - for command_name in command.options.names { - if name == *command_name { - command_found = Some(*command_name); - - break; - } - } - - if command_found.is_some() { - command_found - } else { - // Since the command could not be found in the group, we now will identify if - // the command is actually using a sub-command. We iterate all command names - // and check if one matches, if it does, we potentially have a sub-command. - for command_name in command.options.names { - if starts_with_whole_word(name, command_name) { - name.drain(..=command_name.len()); - break; - } - - if help_options.max_levenshtein_distance > 0 { - let levenshtein_distance = levenshtein(command_name, name); - - if levenshtein_distance <= help_options.max_levenshtein_distance - && HelpBehaviour::Nothing - == check_command_behaviour( - ctx, - msg, - command.options, - group.options.checks, - owners, - help_options, - ) - .await - { - similar_commands.push(SuggestedCommandName { - name: (*command_name).to_string(), - levenshtein_distance, - }); - } - } - } - - // We check all sub-command names in order to see if one variant has been - // issued to the help-system. - let name_str = name.as_str(); - let sub_command_found = command - .options - .sub_commands - .iter() - .find(|n| n.options.names.contains(&name_str)) - .copied(); - - // If we found a sub-command, we replace the parent with it. This allows the - // help-system to extract information from the sub-command. - if let Some(sub_command) = sub_command_found { - // Check parent command's behaviour and permission first before we consider - // the sub-command overwrite it. - if HelpBehaviour::Nothing - == check_command_behaviour( - ctx, - msg, - command.options, - group.options.checks, - owners, - help_options, - ) - .await - { - command = sub_command; - Some(sub_command.options.names[0]) - } else { - break; - } - } else { - match nested_commands_search( - ctx, - msg, - group, - command.options.sub_commands, - name, - help_options, - similar_commands, - owners, - ) - .await - { - Some(found) => return Some(found), - None => None, - } - } - } - }; - - if search_command_name_matched.is_some() { - if HelpBehaviour::Nothing - == check_command_behaviour( - ctx, - msg, - command.options, - group.options.checks, - owners, - help_options, - ) - .await - { - return Some(command); - } - break; - } - } - - None - } - .boxed() -} - -// This function will recursively go through all groups and their groups, trying to find `name`. -// Similar commands will be collected into `similar_commands`. -#[cfg(all(feature = "cache", feature = "http"))] -fn nested_group_command_search<'rec, 'a: 'rec>( - ctx: &'rec Context, - msg: &'rec Message, - groups: &'rec [&'static CommandGroup], - name: &'rec mut String, - help_options: &'a HelpOptions, - similar_commands: &'rec mut Vec, - owners: &'rec HashSet, -) -> BoxFuture<'rec, Result, ()>> { - async move { - for group in groups { - let group = *group; - let group_behaviour = - check_common_behaviour(ctx, msg, &group.options, owners, help_options); - - match &group_behaviour { - HelpBehaviour::Nothing => (), - _ => { - continue; - }, - } - - if !group.options.prefixes.is_empty() - && !group.options.prefixes.iter().any(|prefix| trim_prefixless_group(prefix, name)) - { - continue; - } - - let found = nested_commands_search( - ctx, - msg, - group, - group.options.commands, - name, - help_options, - similar_commands, - owners, - ) - .await; - - if let Some(command) = found { - let options = &command.options; - - if !options.help_available { - return Ok(CustomisedHelpData::NoCommandFound { - help_error_message: help_options.no_help_available_text, - }); - } - - let is_only = |only| group.options.only_in == only || options.only_in == only; - - let available_text = if is_only(OnlyIn::Dm) { - &help_options.dm_only_text - } else if is_only(OnlyIn::Guild) { - &help_options.guild_only_text - } else { - &help_options.dm_and_guild_text - }; - - similar_commands - .sort_unstable_by(|a, b| a.levenshtein_distance.cmp(&b.levenshtein_distance)); - - let check_names: Vec = command - .options - .checks - .iter() - .chain(group.options.checks.iter()) - .filter(|check| check.display_in_help) - .map(|check| check.name.to_string()) - .collect(); - - let sub_command_names: Vec = options - .sub_commands - .iter() - .filter(|cmd| cmd.options.help_available) - .map(|cmd| cmd.options.names[0].to_string()) - .collect(); - - return Ok(CustomisedHelpData::SingleCommand { - command: Command { - name: options.names[0], - description: options.desc, - group_name: group.name, - group_prefixes: group.options.prefixes, - checks: check_names, - aliases: options.names[1..].to_vec(), - availability: available_text, - usage: options.usage, - usage_sample: options.examples.to_vec(), - sub_commands: sub_command_names, - }, - }); - } - - if let Ok(found) = nested_group_command_search( - ctx, - msg, - group.options.sub_groups, - name, - help_options, - similar_commands, - owners, - ) - .await - { - return Ok(found); - } - } - - Err(()) - } - .boxed() -} - -/// Tries to extract a single command matching searched command name otherwise returns similar -/// commands. -#[cfg(feature = "cache")] -async fn fetch_single_command<'a>( - ctx: &Context, - msg: &Message, - groups: &[&'static CommandGroup], - name: &'a str, - help_options: &'a HelpOptions, - owners: &HashSet, -) -> Result, Vec> { - let mut similar_commands: Vec = Vec::new(); - let mut name = name.to_string(); - - nested_group_command_search( - ctx, - msg, - groups, - &mut name, - help_options, - &mut similar_commands, - owners, - ) - .await - .map_err(|()| similar_commands) -} - -#[cfg(feature = "cache")] -#[allow(clippy::too_many_arguments)] -async fn fill_eligible_commands<'a>( - ctx: &Context, - msg: &Message, - commands: &[&'static InternalCommand], - owners: &HashSet, - help_options: &'a HelpOptions, - group: &'a CommandGroup, - to_fill: &mut GroupCommandsPair, - highest_formatter: &mut HelpBehaviour, -) { - to_fill.name = group.name; - to_fill.prefixes = group.options.prefixes.to_vec(); - - let group_behaviour = { - if let HelpBehaviour::Hide = highest_formatter { - HelpBehaviour::Hide - } else { - std::cmp::max( - *highest_formatter, - check_common_behaviour(ctx, msg, &group.options, owners, help_options), - ) - } - }; - - *highest_formatter = group_behaviour; - - for command in commands { - let command = *command; - let options = &command.options; - let name = &options.names[0]; - - if group_behaviour != HelpBehaviour::Nothing { - let name = format_command_name!(&group_behaviour, &name); - to_fill.command_names.push(name); - - continue; - } - - let command_behaviour = check_command_behaviour( - ctx, - msg, - command.options, - group.options.checks, - owners, - help_options, - ) - .await; - - let name = format_command_name!(command_behaviour, &name); - to_fill.command_names.push(name); - } -} - -/// Tries to fetch all commands visible to the user within a group and its sub-groups. -#[cfg(feature = "cache")] -#[allow(clippy::too_many_arguments)] -fn fetch_all_eligible_commands_in_group<'rec, 'a: 'rec>( - ctx: &'rec Context, - msg: &'rec Message, - commands: &'rec [&'static InternalCommand], - owners: &'rec HashSet, - help_options: &'a HelpOptions, - group: &'a CommandGroup, - highest_formatter: HelpBehaviour, -) -> BoxFuture<'rec, GroupCommandsPair> { - async move { - let mut group_with_cmds = GroupCommandsPair::default(); - let mut highest_formatter = highest_formatter; - - fill_eligible_commands( - ctx, - msg, - commands, - owners, - help_options, - group, - &mut group_with_cmds, - &mut highest_formatter, - ) - .await; - - for sub_group in group.options.sub_groups { - if HelpBehaviour::Hide == highest_formatter { - break; - } else if sub_group.options.commands.is_empty() - && sub_group.options.sub_groups.is_empty() - { - continue; - } - - let grouped_cmd = fetch_all_eligible_commands_in_group( - ctx, - msg, - sub_group.options.commands, - owners, - help_options, - sub_group, - highest_formatter, - ) - .await; - - group_with_cmds.sub_groups.push(grouped_cmd); - } - - group_with_cmds - } - .boxed() -} - -/// Fetch groups with their commands. -#[cfg(feature = "cache")] -async fn create_command_group_commands_pair_from_groups<'a>( - ctx: &Context, - msg: &Message, - groups: &[&'static CommandGroup], - owners: &HashSet, - help_options: &'a HelpOptions, -) -> Vec { - let mut listed_groups: Vec = Vec::default(); - - for group in groups { - let group = *group; - - let group_with_cmds = create_single_group(ctx, msg, group, owners, help_options).await; - - if !group_with_cmds.command_names.is_empty() || !group_with_cmds.sub_groups.is_empty() { - listed_groups.push(group_with_cmds); - } - } - - listed_groups -} - -/// Fetches a single group with its commands. -#[cfg(feature = "cache")] -async fn create_single_group( - ctx: &Context, - msg: &Message, - group: &CommandGroup, - owners: &HashSet, - help_options: &HelpOptions, -) -> GroupCommandsPair { - let mut group_with_cmds = fetch_all_eligible_commands_in_group( - ctx, - msg, - group.options.commands, - owners, - help_options, - group, - HelpBehaviour::Nothing, - ) - .await; - - group_with_cmds.name = group.name; - group_with_cmds.summary = group.options.summary; - - group_with_cmds -} - -/// If `searched_group` is exactly matching `group_name`, this function returns `true` but does not -/// trim. Otherwise, it is treated as an optionally passed group-name and ends up being removed -/// from `searched_group`. -/// -/// If a group has no prefixes, it is not required to be part of `searched_group` to reach a -/// sub-group of `group_name`. -#[cfg(feature = "cache")] -fn trim_prefixless_group(group_name: &str, searched_group: &mut String) -> bool { - if group_name == searched_group.as_str() { - return true; - } else if starts_with_whole_word(searched_group, group_name) { - searched_group.drain(..=group_name.len()); - return true; - } - - false -} - -#[cfg(feature = "cache")] -pub fn searched_lowercase<'rec, 'a: 'rec>( - ctx: &'rec Context, - msg: &'rec Message, - group: &'rec CommandGroup, - owners: &'rec HashSet, - help_options: &'a HelpOptions, - searched_named_lowercase: &'rec mut String, -) -> BoxFuture<'rec, Option>> { - async move { - let is_prefixless_group = { - group.options.prefixes.is_empty() - && trim_prefixless_group(&group.name.to_lowercase(), searched_named_lowercase) - }; - let mut progressed = is_prefixless_group; - let is_word_prefix = group.options.prefixes.iter().any(|prefix| { - if starts_with_whole_word(searched_named_lowercase, prefix) { - searched_named_lowercase.drain(..=prefix.len()); - progressed = true; - } - - prefix == searched_named_lowercase - }); - - if is_prefixless_group || is_word_prefix { - let single_group = create_single_group(ctx, msg, group, owners, help_options).await; - - if !single_group.command_names.is_empty() { - return Some(CustomisedHelpData::GroupedCommands { - help_description: group - .options - .description - .map(ToString::to_string) - .unwrap_or_default(), - groups: vec![single_group], - }); - } - } else if progressed || group.options.prefixes.is_empty() { - for sub_group in group.options.sub_groups { - if let Some(found_set) = searched_lowercase( - ctx, - msg, - sub_group, - owners, - help_options, - searched_named_lowercase, - ) - .await - { - return Some(found_set); - } - } - } - - None - } - .boxed() -} - -/// Iterates over all commands and forges them into a [`CustomisedHelpData`], taking -/// [`HelpOptions`] into consideration when deciding on whether a command shall be picked and in -/// what textual format. -#[cfg(feature = "cache")] -pub async fn create_customised_help_data<'a>( - ctx: &Context, - msg: &Message, - args: &'a Args, - groups: &[&'static CommandGroup], - owners: &HashSet, - help_options: &'a HelpOptions, -) -> CustomisedHelpData<'a> { - if !args.is_empty() { - let name = args.message(); - - return match fetch_single_command(ctx, msg, groups, name, help_options, owners).await { - Ok(single_command) => single_command, - Err(suggestions) => { - let mut searched_named_lowercase = name.to_lowercase(); - - for group in groups { - if let Some(found_command) = searched_lowercase( - ctx, - msg, - group, - owners, - help_options, - &mut searched_named_lowercase, - ) - .await - { - return found_command; - } - } - - if suggestions.is_empty() { - CustomisedHelpData::NoCommandFound { - help_error_message: help_options.no_help_available_text, - } - } else { - CustomisedHelpData::SuggestedCommands { - help_description: help_options.suggestion_text.to_string(), - suggestions: Suggestions(suggestions), - } - } - }, - }; - } - - let strikethrough_command_tip = if msg.is_private() { - help_options.strikethrough_commands_tip_in_dm - } else { - help_options.strikethrough_commands_tip_in_guild - }; - - let description = if let Some(strikethrough_command_text) = strikethrough_command_tip { - format!("{}\n{strikethrough_command_text}", help_options.individual_command_tip) - } else { - help_options.individual_command_tip.to_string() - }; - - let listed_groups = - create_command_group_commands_pair_from_groups(ctx, msg, groups, owners, help_options) - .await; - - if listed_groups.is_empty() { - CustomisedHelpData::NoCommandFound { - help_error_message: help_options.no_help_available_text, - } - } else { - CustomisedHelpData::GroupedCommands { - help_description: description, - groups: listed_groups, - } - } -} - -/// Flattens a group with all its nested sub-groups into the passed `group_text` buffer. If -/// `nest_level` is `0`, this function will skip the group's name. -#[cfg(all(feature = "cache", feature = "http"))] -fn flatten_group_to_string( - group_text: &mut String, - group: &GroupCommandsPair, - nest_level: usize, - help_options: &HelpOptions, -) -> Result<(), Error> { - let repeated_indent_str = help_options.indention_prefix.repeat(nest_level); - - if nest_level > 0 { - writeln!(group_text, "{repeated_indent_str}__**{}**__", group.name,)?; - } - - let mut summary_or_prefixes = false; - - if let Some(group_summary) = group.summary { - writeln!(group_text, "{}*{group_summary}*", &repeated_indent_str)?; - summary_or_prefixes = true; - } - - if !group.prefixes.is_empty() { - writeln!( - group_text, - "{}{}: `{}`", - &repeated_indent_str, - help_options.group_prefix, - group.prefixes.join("`, `"), - )?; - summary_or_prefixes = true; - }; - - if summary_or_prefixes { - writeln!(group_text)?; - } - - let mut joined_commands = group.command_names.join(&format!("\n{}", &repeated_indent_str)); - - if !group.command_names.is_empty() { - joined_commands.insert_str(0, &repeated_indent_str); - } - - writeln!(group_text, "{joined_commands}")?; - - for sub_group in &group.sub_groups { - if !(sub_group.command_names.is_empty() && sub_group.sub_groups.is_empty()) { - let mut sub_group_text = String::default(); - - flatten_group_to_string(&mut sub_group_text, sub_group, nest_level + 1, help_options)?; - - write!(group_text, "{sub_group_text}")?; - } - } - - Ok(()) -} - -/// Flattens a group with all its nested sub-groups into the passed `group_text` buffer respecting -/// the plain help format. If `nest_level` is `0`, this function will skip the group's name. -#[cfg(all(feature = "cache", feature = "http"))] -fn flatten_group_to_plain_string( - group_text: &mut String, - group: &GroupCommandsPair, - nest_level: usize, - help_options: &HelpOptions, -) { - let repeated_indent_str = help_options.indention_prefix.repeat(nest_level); - - if nest_level > 0 { - write!(group_text, "\n{repeated_indent_str}**{}**", group.name).unwrap(); - } - - if group.prefixes.is_empty() { - group_text.push_str(": "); - } else { - write!( - group_text, - " ({}: `{}`): ", - help_options.group_prefix, - group.prefixes.join("`, `"), - ).unwrap(); - } - - let joined_commands = group.command_names.join(", "); - - group_text.push_str(&joined_commands); - - for sub_group in &group.sub_groups { - let mut sub_group_text = String::default(); - - flatten_group_to_plain_string(&mut sub_group_text, sub_group, nest_level + 1, help_options); - - group_text.push_str(&sub_group_text); - } -} - -/// Sends an embed listing all groups with their commands. -#[cfg(all(feature = "cache", feature = "http"))] -async fn send_grouped_commands_embed( - cache_http: impl CacheHttp, - help_options: &HelpOptions, - channel_id: ChannelId, - help_description: &str, - groups: &[GroupCommandsPair], - colour: Colour, -) -> Result { - // creating embed outside message builder since flatten_group_to_string may return an error. - - let mut embed = CreateEmbed::new().colour(colour).description(help_description); - for group in groups { - let mut embed_text = String::default(); - - flatten_group_to_string(&mut embed_text, group, 0, help_options)?; - - embed = embed.field(group.name, &embed_text, true); - } - - let builder = CreateMessage::new().embed(embed); - channel_id.send_message(cache_http, builder).await -} - -/// Sends embed showcasing information about a single command. -#[cfg(all(feature = "cache", feature = "http"))] -async fn send_single_command_embed( - cache_http: impl CacheHttp, - help_options: &HelpOptions, - channel_id: ChannelId, - command: &Command<'_>, - colour: Colour, -) -> Result { - let mut embed = CreateEmbed::new().title(command.name).colour(colour); - - if let Some(desc) = command.description { - embed = embed.description(desc); - } - - if let Some(usage) = command.usage { - let full_usage_text = if let Some(first_prefix) = command.group_prefixes.first() { - format!("`{first_prefix} {} {usage}`", command.name) - } else { - format!("`{} {usage}`", command.name) - }; - - embed = embed.field(help_options.usage_label, full_usage_text, true); - } - - if !command.usage_sample.is_empty() { - let full_example_text = if let Some(first_prefix) = command.group_prefixes.first() { - let format_example = |example| format!("`{first_prefix} {} {example}`\n", command.name); - command.usage_sample.iter().map(format_example).collect::() - } else { - let format_example = |example| format!("`{} {example}`\n", command.name); - command.usage_sample.iter().map(format_example).collect::() - }; - embed = embed.field(help_options.usage_sample_label, full_example_text, true); - } - - embed = embed.field(help_options.grouped_label, command.group_name, true); - - if !command.aliases.is_empty() { - embed = embed.field( - help_options.aliases_label, - format!("`{}`", command.aliases.join("`, `")), - true, - ); - } - - if !help_options.available_text.is_empty() && !command.availability.is_empty() { - embed = embed.field(help_options.available_text, command.availability, true); - } - - if !command.checks.is_empty() { - embed = embed.field( - help_options.checks_label, - format!("`{}`", command.checks.join("`, `")), - true, - ); - } - - if !command.sub_commands.is_empty() { - embed = embed.field( - help_options.sub_commands_label, - format!("`{}`", command.sub_commands.join("`, `")), - true, - ); - } - - let builder = CreateMessage::new().embed(embed); - channel_id.send_message(cache_http, builder).await -} - -/// Sends embed listing commands that are similar to the sent one. -#[cfg(all(feature = "cache", feature = "http"))] -async fn send_suggestion_embed( - cache_http: impl CacheHttp, - channel_id: ChannelId, - help_description: &str, - suggestions: &Suggestions, - colour: Colour, -) -> Result { - let text = help_description.replace("{}", &suggestions.join("`, `")); - - let embed = CreateEmbed::new().colour(colour).description(text); - let builder = CreateMessage::new().embed(embed); - channel_id.send_message(cache_http, builder).await -} - -/// Sends an embed explaining fetching commands failed. -#[cfg(all(feature = "cache", feature = "http"))] -async fn send_error_embed( - cache_http: impl CacheHttp, - channel_id: ChannelId, - input: &str, - colour: Colour, -) -> Result { - let embed = CreateEmbed::new().colour(colour).description(input); - let builder = CreateMessage::new().embed(embed); - channel_id.send_message(cache_http, builder).await -} - -/// Posts an embed showing each individual command group and its commands. -/// -/// # Examples -/// -/// Use the command with [`StandardFramework::help`]: -/// -/// ```rust,no_run -/// # use serenity::prelude::*; -/// use std::collections::HashSet; -/// use std::hash::BuildHasher; -/// -/// use serenity::framework::standard::help_commands::*; -/// use serenity::framework::standard::macros::help; -/// use serenity::framework::standard::{ -/// Args, -/// CommandGroup, -/// CommandResult, -/// HelpOptions, -/// StandardFramework, -/// }; -/// use serenity::model::prelude::*; -/// -/// #[help] -/// async fn my_help( -/// context: &Context, -/// msg: &Message, -/// args: Args, -/// help_options: &'static HelpOptions, -/// groups: &[&'static CommandGroup], -/// owners: HashSet, -/// ) -> CommandResult { -/// let _ = with_embeds(context, msg, args, &help_options, groups, owners).await?; -/// Ok(()) -/// } -/// -/// let framework = StandardFramework::new().help(&MY_HELP); -/// ``` -/// -/// # Errors -/// -/// Returns the same errors as [`ChannelId::send_message`]. -/// -/// [`StandardFramework::help`]: crate::framework::standard::StandardFramework::help -#[cfg(all(feature = "cache", feature = "http"))] -pub async fn with_embeds( - ctx: &Context, - msg: &Message, - args: Args, - help_options: &HelpOptions, - groups: &[&'static CommandGroup], - owners: HashSet, -) -> Result { - let formatted_help = - create_customised_help_data(ctx, msg, &args, groups, &owners, help_options).await; - - match formatted_help { - CustomisedHelpData::SuggestedCommands { - ref help_description, - ref suggestions, - } => { - send_suggestion_embed( - &ctx.http, - msg.channel_id, - help_description, - suggestions, - help_options.embed_error_colour, - ) - .await - }, - CustomisedHelpData::NoCommandFound { - help_error_message, - } => { - send_error_embed( - &ctx.http, - msg.channel_id, - help_error_message, - help_options.embed_error_colour, - ) - .await - }, - CustomisedHelpData::GroupedCommands { - ref help_description, - ref groups, - } => { - send_grouped_commands_embed( - &ctx.http, - help_options, - msg.channel_id, - help_description, - groups, - help_options.embed_success_colour, - ) - .await - }, - CustomisedHelpData::SingleCommand { - ref command, - } => { - send_single_command_embed( - &ctx.http, - help_options, - msg.channel_id, - command, - help_options.embed_success_colour, - ) - .await - }, - } -} - -/// Turns grouped commands into a [`String`] taking plain help format into account. -#[cfg(all(feature = "cache", feature = "http"))] -fn grouped_commands_to_plain_string( - help_options: &HelpOptions, - help_description: &str, - groups: &[GroupCommandsPair], -) -> String { - let mut result = "__**Commands**__\n".to_string(); - - result.push_str(help_description); - result.push('\n'); - - for group in groups { - write!(result, "\n**{}**", &group.name).unwrap(); - - flatten_group_to_plain_string(&mut result, group, 0, help_options); - } - - result -} - -/// Turns a single command into a [`String`] taking plain help format into account. -#[cfg(all(feature = "cache", feature = "http"))] -fn single_command_to_plain_string(help_options: &HelpOptions, command: &Command<'_>) -> String { - let mut result = String::new(); - - writeln!(result, "__**{}**__", command.name).unwrap(); - - if !command.aliases.is_empty() { - write!(result, "**{}**: `{}`", help_options.aliases_label, command.aliases.join("`, `")) - .unwrap(); - } - - if let Some(description) = command.description { - writeln!(result, "**{}**: {description}", help_options.description_label).unwrap(); - }; - - if let Some(usage) = command.usage { - if let Some(first_prefix) = command.group_prefixes.first() { - writeln!( - result, - "**{}**: `{first_prefix} {} {usage}`", - help_options.usage_label, command.name - ) - .unwrap(); - } else { - writeln!(result, "**{}**: `{} {usage}`", help_options.usage_label, command.name) - .unwrap(); - } - } - - if !command.usage_sample.is_empty() { - if let Some(first_prefix) = command.group_prefixes.first() { - let format_example = |example| { - writeln!( - result, - "**{}**: `{first_prefix} {} {example}`", - help_options.usage_sample_label, command.name - ) - .unwrap(); - }; - command.usage_sample.iter().for_each(format_example); - } else { - let format_example = |example| { - writeln!( - result, - "**{}**: `{} {example}`", - help_options.usage_sample_label, command.name - ) - .unwrap(); - }; - command.usage_sample.iter().for_each(format_example); - } - } - - writeln!(result, "**{}**: {}", help_options.grouped_label, command.group_name).unwrap(); - - if !help_options.available_text.is_empty() && !command.availability.is_empty() { - writeln!(result, "**{}**: {}", help_options.available_text, command.availability).unwrap(); - } - - if !command.sub_commands.is_empty() { - writeln!( - result, - "**{}**: `{}`", - help_options.sub_commands_label, - command.sub_commands.join("`, `"), - ) - .unwrap(); - } - - result -} - -/// Posts formatted text displaying each individual command group and its commands. -/// -/// # Examples -/// -/// Use the command with `exec_help`: -/// -/// ```rust,no_run -/// # use serenity::prelude::*; -/// use std::collections::HashSet; -/// use std::hash::BuildHasher; -/// -/// use serenity::framework::standard::help_commands::*; -/// use serenity::framework::standard::macros::help; -/// use serenity::framework::standard::{ -/// Args, -/// CommandGroup, -/// CommandResult, -/// HelpOptions, -/// StandardFramework, -/// }; -/// use serenity::model::prelude::*; -/// -/// #[help] -/// async fn my_help( -/// context: &Context, -/// msg: &Message, -/// args: Args, -/// help_options: &'static HelpOptions, -/// groups: &[&'static CommandGroup], -/// owners: HashSet, -/// ) -> CommandResult { -/// let _ = plain(context, msg, args, &help_options, groups, owners).await?; -/// Ok(()) -/// } -/// -/// let framework = StandardFramework::new().help(&MY_HELP); -/// ``` -/// # Errors -/// -/// Returns the same errors as [`ChannelId::send_message`]. -#[cfg(all(feature = "cache", feature = "http"))] -pub async fn plain( - ctx: &Context, - msg: &Message, - args: Args, - help_options: &HelpOptions, - groups: &[&'static CommandGroup], - owners: HashSet, -) -> Result { - let formatted_help = - create_customised_help_data(ctx, msg, &args, groups, &owners, help_options).await; - - let result = match formatted_help { - CustomisedHelpData::SuggestedCommands { - ref help_description, - ref suggestions, - } => help_description.replace("{}", &suggestions.join("`, `")), - CustomisedHelpData::NoCommandFound { - help_error_message, - } => help_error_message.to_string(), - CustomisedHelpData::GroupedCommands { - ref help_description, - ref groups, - } => grouped_commands_to_plain_string(help_options, help_description, groups), - CustomisedHelpData::SingleCommand { - ref command, - } => single_command_to_plain_string(help_options, command), - }; - - msg.channel_id.say(&ctx, result).await -} - -#[cfg(test)] -#[cfg(all(feature = "cache", feature = "http"))] -mod tests { - use super::{SuggestedCommandName, Suggestions}; - - #[test] - fn suggestions_join() { - let names = vec![ - SuggestedCommandName { - name: "aa".to_owned(), - levenshtein_distance: 0, - }, - SuggestedCommandName { - name: "bbb".to_owned(), - levenshtein_distance: 0, - }, - SuggestedCommandName { - name: "cccc".to_owned(), - levenshtein_distance: 0, - }, - ]; - - let actual = Suggestions(names).join(", "); - - assert_eq!(actual, "aa, bbb, cccc"); - assert_eq!(actual.capacity(), 13); - } -} diff --git a/src/framework/standard/mod.rs b/src/framework/standard/mod.rs deleted file mode 100644 index fae9b909417..00000000000 --- a/src/framework/standard/mod.rs +++ /dev/null @@ -1,885 +0,0 @@ -#![cfg_attr( - not(ignore_serenity_deprecated), - deprecated = "The standard framework is deprecated, and will be removed in 0.13. Please migrate to `poise` for command handling" -)] -#![allow(deprecated)] // Entire framework is deprecated anyway. - -pub mod help_commands; -pub mod macros { - pub use command_attr::{check, command, group, help, hook}; -} - -mod args; -mod configuration; -mod parse; -mod structures; - -use std::collections::HashMap; -use std::sync::Arc; - -pub use args::{Args, Delimiter, Error as ArgError, Iter, RawArguments}; -use async_trait::async_trait; -pub use configuration::{Configuration, WithWhiteSpace}; -use futures::future::BoxFuture; -use parse::map::{CommandMap, GroupMap, Map}; -use parse::{Invoke, ParseError}; -pub use structures::buckets::BucketBuilder; -use structures::buckets::{Bucket, RateLimitAction}; -pub use structures::*; -use tokio::sync::Mutex; -use tokio::time::sleep; -use tracing::instrument; -use uwl::Stream; - -use self::buckets::{RateLimitInfo, RevertBucket}; -use super::Framework; -#[cfg(feature = "cache")] -use crate::cache::Cache; -use crate::client::{Context, FullEvent}; -use crate::model::channel::Message; -#[cfg(feature = "cache")] -use crate::model::guild::Member; -use crate::model::permissions::Permissions; -#[cfg(all(feature = "cache", feature = "http", feature = "model"))] -use crate::model::{guild::Role, id::RoleId}; - -/// An enum representing all possible fail conditions under which a command won't be executed. -#[derive(Debug)] -#[non_exhaustive] -pub enum DispatchError { - /// When a custom function check has failed. - CheckFailed(&'static str, Reason), - /// When the command caller has exceeded a ratelimit bucket. - Ratelimited(RateLimitInfo), - /// When the requested command is disabled in bot configuration. - CommandDisabled, - /// When the user is blocked in bot configuration. - BlockedUser, - /// When the guild or its owner is blocked in bot configuration. - BlockedGuild, - /// When the channel blocked in bot configuration. - BlockedChannel, - /// When the requested command can only be used in a direct message or group - /// channel. - OnlyForDM, - /// When the requested command can only be ran in guilds, or the bot doesn't - /// support DMs. - OnlyForGuilds, - /// When the requested command can only be used by bot owners. - OnlyForOwners, - /// When the requested command requires one role. - LackingRole, - /// When the command requester lacks specific required permissions. - LackingPermissions(Permissions), - /// When there are too few arguments. - NotEnoughArguments { min: u16, given: usize }, - /// When there are too many arguments. - TooManyArguments { max: u16, given: usize }, -} - -type DispatchHook = - for<'fut> fn(&'fut Context, &'fut Message, DispatchError, &'fut str) -> BoxFuture<'fut, ()>; -type BeforeHook = for<'fut> fn(&'fut Context, &'fut Message, &'fut str) -> BoxFuture<'fut, bool>; -type AfterHook = for<'fut> fn( - &'fut Context, - &'fut Message, - &'fut str, - Result<(), CommandError>, -) -> BoxFuture<'fut, ()>; -type UnrecognisedHook = - for<'fut> fn(&'fut Context, &'fut Message, &'fut str) -> BoxFuture<'fut, ()>; -type NormalMessageHook = for<'fut> fn(&'fut Context, &'fut Message) -> BoxFuture<'fut, ()>; -type PrefixOnlyHook = for<'fut> fn(&'fut Context, &'fut Message) -> BoxFuture<'fut, ()>; - -/// A utility for easily managing dispatches to commands. -/// -/// Refer to the [module-level documentation] for more information. -/// -/// [module-level documentation]: self -#[derive(Default)] -pub struct StandardFramework { - groups: Vec<(&'static CommandGroup, Map)>, - buckets: Mutex>, - before: Option, - after: Option, - dispatch: Option, - unrecognised_command: Option, - normal_message: Option, - prefix_only: Option, - config: parking_lot::RwLock, - help: Option<&'static HelpCommand>, - /// Whether the framework has been "initialized". - /// - /// The framework is initialized once one of the following occurs: - /// - configuration has been set; - /// - a command handler has been set; - /// - a command check has been set. - /// - /// This is used internally to determine whether or not - in addition to dispatching to the - /// [`EventHandler::message`] handler - to have the framework check if a - /// [`Event::MessageCreate`] should be processed by itself. - /// - /// [`EventHandler::message`]: crate::client::EventHandler::message - /// [`Event::MessageCreate`]: crate::model::event::Event::MessageCreate - pub initialized: bool, -} - -impl StandardFramework { - #[inline] - #[must_use] - pub fn new() -> Self { - StandardFramework::default() - } - - /// Configures the framework, setting non-default values. - /// - /// This passes a mutable reference to the current configuration, allowing for runtime - /// configuration of the Framework. - /// - /// # Examples - /// - /// Configuring the framework for a [`Client`], [allowing whitespace between prefixes], and - /// setting the [`prefix`] to `"~"`: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// # struct Handler; - /// # impl EventHandler for Handler {} - /// use serenity::framework::standard::{Configuration, StandardFramework}; - /// use serenity::Client; - /// - /// # async fn run() -> Result<(), Box> { - /// let framework = StandardFramework::new(); - /// framework.configure(Configuration::new().with_whitespace(true).prefix("~")); - /// - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()) - /// .event_handler(Handler) - /// .framework(framework) - /// .await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// [`Client`]: crate::Client - /// [`prefix`]: Configuration::prefix - /// [allowing whitespace between prefixes]: Configuration::with_whitespace - pub fn configure(&self, config: Configuration) { - *self.config.write() = config; - } - - /// Defines a bucket with `delay` between each command, and the `limit` of uses per - /// `time_span`. - /// - /// # Examples - /// - /// Create and use a bucket that limits a command to 3 uses per 10 seconds with a 2 second - /// delay in between invocations: - /// - /// ```rust,no_run - /// use serenity::framework::standard::macros::command; - /// use serenity::framework::standard::{BucketBuilder, CommandResult, StandardFramework}; - /// - /// #[command] - /// // Registers the bucket `basic` to this command. - /// #[bucket = "basic"] - /// async fn nothing() -> CommandResult { - /// Ok(()) - /// } - /// - /// # async fn run() { - /// let framework = StandardFramework::new() - /// .bucket("basic", BucketBuilder::default().delay(2).time_span(10).limit(3)) - /// .await; - /// # } - /// ``` - #[inline] - pub async fn bucket(self, name: impl Into, builder: BucketBuilder) -> Self { - self.buckets.lock().await.insert(name.into(), builder.construct()); - self - } - - /// Whether the message should be ignored because it is from a bot or webhook. - fn should_ignore(&self, msg: &Message) -> bool { - let config = self.config.read(); - - (config.ignore_bots && msg.author.bot) - || (config.ignore_webhooks && msg.webhook_id.is_some()) - } - - async fn should_fail<'a>( - &'a self, - ctx: &'a Context, - msg: &'a Message, - args: &'a mut Args, - command: &'static CommandOptions, - group: &'static GroupOptions, - ) -> Option { - if let Some(min) = command.min_args { - if args.len() < min as usize { - return Some(DispatchError::NotEnoughArguments { - min, - given: args.len(), - }); - } - } - - if let Some(max) = command.max_args { - if args.len() > max as usize { - return Some(DispatchError::TooManyArguments { - max, - given: args.len(), - }); - } - } - - { - let config = self.config.read(); - if (group.owner_privilege && command.owner_privilege) - && config.owners.contains(&msg.author.id) - { - return None; - } - - if config.blocked_users.contains(&msg.author.id) { - return Some(DispatchError::BlockedUser); - } - - #[cfg(feature = "cache")] - { - if let Some(channel) = msg.channel_id.to_channel_cached(&ctx.cache) { - let guild_id = channel.guild_id; - - if config.blocked_guilds.contains(&guild_id) { - return Some(DispatchError::BlockedGuild); - } - - if let Some(guild) = ctx.cache.guild(guild_id) { - if config.blocked_users.contains(&guild.owner_id) { - return Some(DispatchError::BlockedGuild); - } - } - } - } - - if !config.allowed_channels.is_empty() - && !config.allowed_channels.contains(&msg.channel_id) - { - return Some(DispatchError::BlockedChannel); - } - } - - // Try passing the command's bucket, exiting the loop if no command ratelimit has been hit - // or early-return when ratelimits cancel the framework invocation. Otherwise, delay and - // loop again to check if we passed the bucket. - loop { - let mut duration = None; - - { - let mut buckets = self.buckets.lock().await; - - if let Some(bucket) = command.bucket.and_then(|b| buckets.get_mut(b)) { - if let Some(rate_limit_info) = bucket.take(ctx, msg).await { - duration = match rate_limit_info.action { - RateLimitAction::Cancelled | RateLimitAction::FailedDelay => { - return Some(DispatchError::Ratelimited(rate_limit_info)) - }, - RateLimitAction::Delayed => Some(rate_limit_info.rate_limit), - }; - } - } - } - - match duration { - Some(duration) => sleep(duration).await, - None => break, - } - } - - for check in group.checks.iter().chain(command.checks.iter()) { - let res = (check.function)(ctx, msg, args, command).await; - - if let Result::Err(reason) = res { - return Some(DispatchError::CheckFailed(check.name, reason)); - } - } - - None - } - - /// Adds a group which can organize several related commands. Groups are taken into account - /// when using [`serenity::framework::standard::help_commands`]. - /// - /// # Examples - /// - /// Add a group with ping and pong commands: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// # use std::error::Error as StdError; - /// # struct Handler; - /// # - /// # impl EventHandler for Handler {} - /// # - /// use serenity::client::{Client, Context}; - /// use serenity::model::channel::Message; - /// use serenity::framework::standard::{ - /// StandardFramework, - /// CommandResult, - /// macros::{command, group}, - /// }; - /// - /// // For information regarding this macro, learn more about it in its documentation in `command_attr`. - /// #[command] - /// async fn ping(ctx: &Context, msg: &Message) -> CommandResult { - /// msg.channel_id.say(&ctx.http, "pong!").await?; - /// - /// Ok(()) - /// } - /// - /// #[command] - /// async fn pong(ctx: &Context, msg: &Message) -> CommandResult { - /// msg.channel_id.say(&ctx.http, "ping!").await?; - /// - /// Ok(()) - /// } - /// - /// #[group("bingbong")] - /// #[commands(ping, pong)] - /// struct BingBong; - /// - /// let framework = StandardFramework::new() - /// // Groups' names are changed to all uppercase, plus appended with `_GROUP`. - /// .group(&BINGBONG_GROUP); - /// ``` - /// - /// [`serenity::framework::standard::help_commands`]: crate::framework::standard::help_commands - #[must_use] - pub fn group(mut self, group: &'static CommandGroup) -> Self { - self.group_add(group); - self.initialized = true; - - self - } - - /// Adds a group to be used by the framework. Primary use-case is runtime modification of - /// groups in the framework; will _not_ mark the framework as initialized. Refer to - /// [`Self::group`] for adding groups in initial configuration. - /// - /// Note: does _not_ return [`Self`] like many other commands. This is because it's not - /// intended to be chained as the other commands are. - pub fn group_add(&mut self, group: &'static CommandGroup) { - let config = self.config.read(); - let map = if group.options.prefixes.is_empty() { - Map::Prefixless( - GroupMap::new(group.options.sub_groups, &config), - CommandMap::new(group.options.commands, &config), - ) - } else { - Map::WithPrefixes(GroupMap::new(&[group], &config)) - }; - - self.groups.push((group, map)); - } - - /// Removes a group from being used in the framework. Primary use-case is runtime modification - /// of groups in the framework. - /// - /// Note: does _not_ return [`Self`] like many other commands. This is because it's not - /// intended to be chained as the other commands are. - pub fn group_remove(&mut self, group: &'static CommandGroup) { - // Iterates through the vector and if a given group _doesn't_ match, we retain it - self.groups.retain(|&(g, _)| g != group); - } - - /// Specify the function that's called in case a command wasn't executed for one reason or - /// another. - /// - /// DispatchError represents all possible fail conditions. - /// - /// # Examples - /// - /// Making a simple argument error responder: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// # use serenity::model::prelude::*; - /// use serenity::framework::standard::macros::hook; - /// use serenity::framework::standard::DispatchError; - /// use serenity::framework::StandardFramework; - /// - /// #[hook] - /// async fn dispatch_error_hook( - /// context: &Context, - /// msg: &Message, - /// error: DispatchError, - /// command_name: &str, - /// ) { - /// match error { - /// DispatchError::NotEnoughArguments { - /// min, - /// given, - /// } => { - /// let s = format!("Need {} arguments, but only got {}.", min, given); - /// - /// let _ = msg.channel_id.say(&context, &s).await; - /// }, - /// DispatchError::TooManyArguments { - /// max, - /// given, - /// } => { - /// let s = format!("Max arguments allowed is {}, but got {}.", max, given); - /// - /// let _ = msg.channel_id.say(&context, &s).await; - /// }, - /// _ => println!("Unhandled dispatch error in {}.", command_name), - /// } - /// } - /// - /// let framework = StandardFramework::new().on_dispatch_error(dispatch_error_hook); - /// ``` - #[must_use] - pub fn on_dispatch_error(mut self, f: DispatchHook) -> Self { - self.dispatch = Some(f); - - self - } - - /// Specify the function to be called on messages comprised of only the prefix. - #[must_use] - pub fn prefix_only(mut self, f: PrefixOnlyHook) -> Self { - self.prefix_only = Some(f); - - self - } - - /// Specify the function to be called prior to every command's execution. If that function - /// returns true, the command will be executed. - /// - /// # Examples - /// - /// Using [`Self::before`] to log command usage: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// # use serenity::model::prelude::*; - /// use serenity::framework::standard::macros::hook; - /// use serenity::framework::StandardFramework; - /// - /// #[hook] - /// async fn before_hook(_: &Context, _: &Message, cmd_name: &str) -> bool { - /// println!("Running command {}", cmd_name); - /// true - /// } - /// let framework = StandardFramework::new().before(before_hook); - /// ``` - /// - /// Using before to prevent command usage: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// # use serenity::model::prelude::*; - /// use serenity::framework::standard::macros::hook; - /// use serenity::framework::StandardFramework; - /// - /// #[hook] - /// async fn before_hook(ctx: &Context, msg: &Message, cmd_name: &str) -> bool { - /// if let Ok(channel) = msg.channel_id.to_channel(ctx).await { - /// // Don't run unless in nsfw channel - /// if !channel.is_nsfw() { - /// return false; - /// } - /// } - /// - /// println!("Running command {}", cmd_name); - /// - /// true - /// } - /// - /// let framework = StandardFramework::new().before(before_hook); - /// ``` - #[must_use] - pub fn before(mut self, f: BeforeHook) -> Self { - self.before = Some(f); - - self - } - - /// Specify the function to be called after every command's execution. Fourth argument exists - /// if command returned an error which you can handle. - /// - /// # Examples - /// - /// Using [`Self::after`] to log command usage: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// # use serenity::model::prelude::*; - /// use serenity::framework::standard::macros::hook; - /// use serenity::framework::standard::CommandError; - /// use serenity::framework::StandardFramework; - /// - /// #[hook] - /// async fn after_hook(_: &Context, _: &Message, cmd_name: &str, error: Result<(), CommandError>) { - /// // Print out an error if it happened - /// if let Err(why) = error { - /// println!("Error in {}: {:?}", cmd_name, why); - /// } - /// } - /// - /// let framework = StandardFramework::new().after(after_hook); - /// ``` - #[must_use] - pub fn after(mut self, f: AfterHook) -> Self { - self.after = Some(f); - - self - } - - /// Specify the function to be called if no command could be dispatched. - /// - /// # Examples - /// - /// Using [`Self::unrecognised_command`]: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// # use serenity::model::prelude::*; - /// use serenity::framework::standard::macros::hook; - /// use serenity::framework::StandardFramework; - /// - /// #[hook] - /// async fn unrecognised_command_hook( - /// _: &Context, - /// msg: &Message, - /// unrecognised_command_name: &str, - /// ) { - /// println!( - /// "A user named {:?} tried to execute an unknown command: {}", - /// msg.author.name, unrecognised_command_name - /// ); - /// } - /// - /// let framework = StandardFramework::new().unrecognised_command(unrecognised_command_hook); - /// ``` - #[must_use] - pub fn unrecognised_command(mut self, f: UnrecognisedHook) -> Self { - self.unrecognised_command = Some(f); - - self - } - - /// Specify the function to be called if a message contains no command. - /// - /// # Examples - /// - /// Using [`Self::normal_message`]: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// # use serenity::model::prelude::*; - /// use serenity::framework::standard::macros::hook; - /// use serenity::framework::StandardFramework; - /// - /// #[hook] - /// async fn normal_message_hook(_: &Context, msg: &Message) { - /// println!("Received a generic message: {:?}", msg.content); - /// } - /// - /// let framework = StandardFramework::new().normal_message(normal_message_hook); - /// ``` - #[must_use] - pub fn normal_message(mut self, f: NormalMessageHook) -> Self { - self.normal_message = Some(f); - - self - } - - /// Sets what code should be executed when a user sends `(prefix)help`. - /// - /// If a command named `help` in a group was set, then this takes precedence first. - #[must_use] - pub fn help(mut self, h: &'static HelpCommand) -> Self { - self.help = Some(h); - - self - } -} - -#[async_trait] -impl Framework for StandardFramework { - #[instrument(skip(self, event))] - async fn dispatch(&self, mut ctx: Context, event: FullEvent) { - let FullEvent::Message { - new_message: msg, - } = event - else { - return; - }; - - if self.should_ignore(&msg) { - return; - } - - let mut stream = Stream::new(&msg.content); - - stream.take_while_char(char::is_whitespace); - - let config = self.config.read().clone(); - - let prefix = parse::prefix(&ctx, &msg, &mut stream, &config).await; - - if prefix.is_some() && stream.rest().is_empty() { - if let Some(prefix_only) = &self.prefix_only { - prefix_only(&mut ctx, &msg).await; - } - - return; - } - - if prefix.is_none() && !(config.no_dm_prefix && msg.is_private()) { - if let Some(normal) = &self.normal_message { - normal(&mut ctx, &msg).await; - } - - return; - } - - let invocation = parse::command( - &ctx, - &msg, - &mut stream, - &self.groups, - &config, - self.help.map(|h| h.options.names), - ) - .await; - - let invoke = match invocation { - Ok(i) => i, - Err(ParseError::UnrecognisedCommand(unreg)) => { - if let Some(unreg) = unreg { - if let Some(unrecognised_command) = &self.unrecognised_command { - unrecognised_command(&mut ctx, &msg, &unreg).await; - } - } - - if let Some(normal) = &self.normal_message { - normal(&mut ctx, &msg).await; - } - - return; - }, - Err(ParseError::Dispatch { - error, - command_name, - }) => { - if let Some(dispatch) = &self.dispatch { - dispatch(&mut ctx, &msg, error, &command_name).await; - } - - return; - }, - }; - - match invoke { - Invoke::Help(name) => { - if !config.allow_dm && msg.is_private() { - return; - } - - let args = Args::new(stream.rest(), &config.delimiters); - - let groups = self.groups.iter().map(|(g, _)| *g).collect::>(); - - // `parse_command` promises to never return a help invocation if - // `StandardFramework::help` is `None`. - #[allow(clippy::unwrap_used)] - let help = self.help.unwrap(); - - if let Some(before) = &self.before { - if !before(&mut ctx, &msg, name).await { - return; - } - } - - let res = - (help.fun)(&mut ctx, &msg, args, help.options, &groups, config.owners).await; - - if let Some(after) = &self.after { - after(&mut ctx, &msg, name, res).await; - } - }, - Invoke::Command { - command, - group, - } => { - let mut args = { - use std::borrow::Cow; - - let mut delims = Cow::Borrowed(&config.delimiters); - - // If user has configured the command's own delimiters, use those instead. - if !command.options.delimiters.is_empty() { - // FIXME: Get rid of this allocation. - let mut v = Vec::with_capacity(command.options.delimiters.len()); - - for delim in command.options.delimiters { - if delim.len() == 1 { - // Should always be Some() in this case - #[allow(clippy::unwrap_used)] - v.push(Delimiter::Single(delim.chars().next().unwrap())); - } else { - // This too. - v.push(Delimiter::Multiple((*delim).to_string())); - } - } - - delims = Cow::Owned(v); - } - - Args::new(stream.rest(), &delims) - }; - - if let Some(error) = - self.should_fail(&ctx, &msg, &mut args, command.options, group.options).await - { - if let Some(dispatch) = &self.dispatch { - let command_name = command.options.names[0]; - dispatch(&mut ctx, &msg, error, command_name).await; - } - - return; - } - - let name = command.options.names[0]; - - if let Some(before) = &self.before { - if !before(&mut ctx, &msg, name).await { - return; - } - } - - let res = (command.fun)(&mut ctx, &msg, args).await; - - // Check if the command wants to revert the bucket by giving back a ticket. - if matches!(&res, Err(e) if e.is::()) { - let mut buckets = self.buckets.lock().await; - - if let Some(bucket) = command.options.bucket.and_then(|b| buckets.get_mut(b)) { - bucket.give(&ctx, &msg).await; - } - } - - if let Some(after) = &self.after { - after(&mut ctx, &msg, name, res).await; - } - }, - } - } -} - -pub trait CommonOptions { - fn required_permissions(&self) -> &Permissions; - fn allowed_roles(&self) -> &'static [&'static str]; - fn checks(&self) -> &'static [&'static Check]; - fn only_in(&self) -> OnlyIn; - fn help_available(&self) -> bool; - fn owners_only(&self) -> bool; - fn owner_privilege(&self) -> bool; -} - -impl CommonOptions for &GroupOptions { - fn required_permissions(&self) -> &Permissions { - &self.required_permissions - } - - fn allowed_roles(&self) -> &'static [&'static str] { - self.allowed_roles - } - - fn checks(&self) -> &'static [&'static Check] { - self.checks - } - - fn only_in(&self) -> OnlyIn { - self.only_in - } - - fn help_available(&self) -> bool { - self.help_available - } - - fn owners_only(&self) -> bool { - self.owners_only - } - - fn owner_privilege(&self) -> bool { - self.owner_privilege - } -} - -impl CommonOptions for &CommandOptions { - fn required_permissions(&self) -> &Permissions { - &self.required_permissions - } - - fn allowed_roles(&self) -> &'static [&'static str] { - self.allowed_roles - } - - fn checks(&self) -> &'static [&'static Check] { - self.checks - } - - fn only_in(&self) -> OnlyIn { - self.only_in - } - - fn help_available(&self) -> bool { - self.help_available - } - - fn owners_only(&self) -> bool { - self.owners_only - } - - fn owner_privilege(&self) -> bool { - self.owner_privilege - } -} - -#[cfg(feature = "cache")] -pub(crate) fn has_correct_permissions( - cache: impl AsRef, - options: &impl CommonOptions, - message: &Message, -) -> bool { - if options.required_permissions().is_empty() { - true - } else { - message.guild(cache.as_ref()).is_some_and(|guild| { - let Some(channel) = guild.channels.get(&message.channel_id) else { return false }; - let Some(member) = guild.members.get(&message.author.id) else { return false }; - - guild.user_permissions_in(channel, member).contains(*options.required_permissions()) - }) - } -} - -#[cfg(all(feature = "cache", feature = "http"))] -pub(crate) fn has_correct_roles( - options: &impl CommonOptions, - roles: &HashMap, - member: &Member, -) -> bool { - if options.allowed_roles().is_empty() { - true - } else { - options - .allowed_roles() - .iter() - .filter_map(|r| roles.values().find(|role| *r == role.name)) - .any(|g| member.roles.contains(&g.id)) - } -} diff --git a/src/framework/standard/parse/map.rs b/src/framework/standard/parse/map.rs deleted file mode 100644 index 96309e5772a..00000000000 --- a/src/framework/standard/parse/map.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::framework::standard::*; - -#[derive(Debug)] -pub enum Map { - WithPrefixes(GroupMap), - Prefixless(GroupMap, CommandMap), -} - -pub trait ParseMap { - type Storage; - - fn get(&self, n: &str) -> Option; - fn min_length(&self) -> usize; - fn max_length(&self) -> usize; - fn is_empty(&self) -> bool; -} - -#[derive(Debug, Default)] -pub struct CommandMap { - cmds: HashMap)>, - min_length: usize, - max_length: usize, -} - -impl CommandMap { - pub fn new(cmds: &[&'static Command], conf: &Configuration) -> Self { - let mut map = Self::default(); - - for cmd in cmds { - let sub_map = Arc::new(Self::new(cmd.options.sub_commands, conf)); - - for name in cmd.options.names { - let len = name.chars().count(); - map.min_length = std::cmp::min(len, map.min_length); - map.max_length = std::cmp::max(len, map.max_length); - - let name = - if conf.case_insensitive { name.to_lowercase() } else { (*name).to_string() }; - - map.cmds.insert(name, (*cmd, Arc::clone(&sub_map))); - } - } - - map - } -} - -impl ParseMap for CommandMap { - type Storage = (&'static Command, Arc); - - #[inline] - fn min_length(&self) -> usize { - self.min_length - } - - #[inline] - fn max_length(&self) -> usize { - self.max_length - } - - #[inline] - fn get(&self, name: &str) -> Option { - self.cmds.get(name).cloned() - } - - #[inline] - fn is_empty(&self) -> bool { - self.cmds.is_empty() - } -} - -#[derive(Debug, Default)] -pub struct GroupMap { - groups: HashMap<&'static str, (&'static CommandGroup, Arc, Arc)>, - min_length: usize, - max_length: usize, -} - -impl GroupMap { - pub fn new(groups: &[&'static CommandGroup], conf: &Configuration) -> Self { - let mut map = Self::default(); - - for group in groups { - let subgroups_map = Arc::new(Self::new(group.options.sub_groups, conf)); - let commands_map = Arc::new(CommandMap::new(group.options.commands, conf)); - - for prefix in group.options.prefixes { - let len = prefix.chars().count(); - map.min_length = std::cmp::min(len, map.min_length); - map.max_length = std::cmp::max(len, map.max_length); - - map.groups.insert( - *prefix, - (*group, Arc::clone(&subgroups_map), Arc::clone(&commands_map)), - ); - } - } - - map - } -} - -impl ParseMap for GroupMap { - type Storage = (&'static CommandGroup, Arc, Arc); - - #[inline] - fn min_length(&self) -> usize { - self.min_length - } - - #[inline] - fn max_length(&self) -> usize { - self.max_length - } - - #[inline] - fn get(&self, name: &str) -> Option { - self.groups.get(&name).cloned() - } - - #[inline] - fn is_empty(&self) -> bool { - self.groups.is_empty() - } -} diff --git a/src/framework/standard/parse/mod.rs b/src/framework/standard/parse/mod.rs deleted file mode 100644 index 158ebee9038..00000000000 --- a/src/framework/standard/parse/mod.rs +++ /dev/null @@ -1,517 +0,0 @@ -use super::{Command, *}; -use crate::model::prelude::*; - -pub mod map; - -use std::borrow::Cow; - -use futures::FutureExt; -use map::ParseMap; - -// FIXME: Add the `http` parameter to `Guild::user_permissions_in`. -// -// Trying to shove the parameter to the original method results in several errors and interface -// changes to methods using `Guild::user_permissions_in` that are not worthwhile to resolve. As a -// compromise, the method has been copied with the parameter added in to the place where the -// *problem* occurs. -// -// When a bot's command is invoked in a large guild (e.g., 250k+ members), the method fails to -// retrieve the member data of the author that invoked the command, and instead defaults to -// `@everyone`'s permissions. This is because Discord does not send data of all members past 250, -// resulting in the problem to meet permissions of a command even if the author does possess them. -// To avoid defaulting to permissions of everyone, we fetch the member from HTTP if it is missing -// in the guild's members list. -#[cfg(feature = "cache")] -fn permissions_in( - ctx: &Context, - guild_id: GuildId, - channel_id: ChannelId, - member: &Member, - roles: &HashMap, -) -> Permissions { - let guild = ctx.cache.guild(guild_id); - if guild.as_ref().map(|guild| member.user.id == guild.owner_id) == Some(true) { - return Permissions::all(); - } - - let Some(everyone) = roles.get(&RoleId::new(guild_id.get())) else { - tracing::error!("@everyone role is missing in guild {}", guild_id); - - return Permissions::empty(); - }; - - let mut permissions = everyone.permissions; - - for &role in &member.roles { - if let Some(role) = roles.get(&role) { - permissions |= role.permissions; - } else { - tracing::warn!("{} on {} has non-existent role {:?}", member.user.id, guild_id, role); - } - } - - if permissions.contains(Permissions::ADMINISTRATOR) { - return Permissions::all(); - } - - if let Some(channel) = guild.and_then(|guild| guild.channels.get(&channel_id).cloned()) { - let mut data = Vec::with_capacity(member.roles.len()); - - for overwrite in &channel.permission_overwrites { - if let PermissionOverwriteType::Role(role) = overwrite.kind { - if role.get() != guild_id.get() && !member.roles.contains(&role) { - continue; - } - - if let Some(role) = roles.get(&role) { - data.push((role.position, overwrite.deny, overwrite.allow)); - } - } - } - - data.sort_by(|a, b| a.0.cmp(&b.0)); - - for overwrite in data { - permissions = (permissions & !overwrite.1) | overwrite.2; - } - - for overwrite in &channel.permission_overwrites { - if PermissionOverwriteType::Member(member.user.id) != overwrite.kind { - continue; - } - - permissions = (permissions & !overwrite.deny) | overwrite.allow; - } - } else { - tracing::warn!("Guild {} does not contain channel {}", guild_id, channel_id); - } - - if channel_id.get() == guild_id.get() { - permissions |= Permissions::VIEW_CHANNEL; - } - - permissions -} - -#[inline] -fn to_lowercase<'a>(config: &Configuration, s: &'a str) -> Cow<'a, str> { - if config.case_insensitive { - Cow::Owned(s.to_lowercase()) - } else { - Cow::Borrowed(s) - } -} - -/// Parse a mention in the message that is of either the direct (`<@id>`) or nickname (`<@!id>`) -/// syntax, and compare the encoded `id` with the id from [`Configuration::on_mention`] for a -/// match. Returns `Some()` on success, [`None`] otherwise. -pub fn mention<'a>(stream: &mut Stream<'a>, config: &Configuration) -> Option<&'a str> { - let on_mention = config.on_mention.as_deref()?; - - let start = stream.offset(); - - if !stream.eat("<@") { - return None; - } - - // Optional. - stream.eat("!"); - - let id = stream.take_while(|b| b.is_ascii_digit()); - - if !stream.eat(">") { - // Backtrack to where we were. - stream.set(start); - - return None; - } - - if id == on_mention { - Some(id) - } else { - stream.set(start); - - None - } -} - -async fn find_prefix<'a>( - ctx: &Context, - msg: &Message, - config: &Configuration, - stream: &Stream<'a>, -) -> Option> { - let try_match = |prefix: &str| { - let peeked = stream.peek_for_char(prefix.chars().count()); - let peeked = to_lowercase(config, peeked); - (prefix == peeked).then_some(peeked) - }; - - for f in &config.dynamic_prefixes { - if let Some(p) = f(ctx, msg).await { - let p = to_lowercase(config, &p); - if let Some(p) = try_match(&p) { - return Some(p); - } - } - } - - config.prefixes.iter().find_map(|p| try_match(p)) -} - -/// Parse a prefix in the message. -/// -/// The "prefix" may be one of the following: -/// - A mention (`<@id>`/`<@!id>`) -/// - A dynamically constructed prefix ([`Configuration::dynamic_prefix`]) -/// - A static prefix ([`Configuration::prefix`]) -/// - Nothing -/// -/// In all cases, whitespace after the prefix is cleared. -#[allow(clippy::needless_lifetimes)] // Clippy and the compiler disagree -pub async fn prefix<'a>( - ctx: &Context, - msg: &Message, - stream: &mut Stream<'a>, - config: &Configuration, -) -> Option> { - if let Some(id) = mention(stream, config) { - stream.take_while_char(char::is_whitespace); - - return Some(Cow::Borrowed(id)); - } - - let prefix = find_prefix(ctx, msg, config, stream).await; - - if let Some(prefix) = &prefix { - stream.increment(prefix.len()); - } - - if config.with_whitespace.prefixes { - stream.take_while_char(char::is_whitespace); - } - - prefix -} - -/// Checked per valid group or command in the message. -async fn check_discrepancy( - #[allow(unused_variables)] ctx: &Context, - msg: &Message, - config: &Configuration, - options: &impl CommonOptions, -) -> Result<(), DispatchError> { - if options.owners_only() && !config.owners.contains(&msg.author.id) { - return Err(DispatchError::OnlyForOwners); - } - - if options.only_in() == OnlyIn::Dm && !msg.is_private() { - return Err(DispatchError::OnlyForDM); - } - - if (!config.allow_dm || options.only_in() == OnlyIn::Guild) && msg.is_private() { - return Err(DispatchError::OnlyForGuilds); - } - - #[cfg(feature = "cache")] - { - if let Some(guild_id) = msg.guild_id { - let roles = match ctx.cache.guild(guild_id) { - Some(guild) => guild.roles.clone(), - None => return Ok(()), - }; - - let Ok(member) = guild_id.member(ctx, msg.author.id).await else { return Ok(()) }; - let perms = permissions_in(ctx, guild_id, msg.channel_id, &member, &roles); - - if !(perms.contains(*options.required_permissions()) - || options.owner_privilege() && config.owners.contains(&msg.author.id)) - { - return Err(DispatchError::LackingPermissions(*options.required_permissions())); - } - - if !perms.administrator() && !has_correct_roles(options, &roles, &member) { - return Err(DispatchError::LackingRole); - } - } - } - - Ok(()) -} - -fn try_parse( - stream: &Stream<'_>, - map: &M, - by_space: bool, - f: impl Fn(&str) -> String, -) -> (String, Option) { - if by_space { - let n = f(stream.peek_until_char(char::is_whitespace)); - - let o = map.get(&n); - - (n, o) - } else { - let mut n = f(stream.peek_for_char(map.max_length())); - let mut o = None; - - for _ in 0..(map.max_length() - map.min_length()) { - o = map.get(&n); - - if o.is_some() { - break; - } - - n.pop(); - } - - (n, o) - } -} - -fn parse_cmd<'a>( - stream: &'a mut Stream<'_>, - ctx: &'a Context, - msg: &'a Message, - config: &'a Configuration, - map: &'a CommandMap, -) -> BoxFuture<'a, Result<&'static Command, ParseError>> { - async move { - let (n, r) = - try_parse(stream, map, config.by_space, |s| to_lowercase(config, s).into_owned()); - - if config.disabled_commands.contains(&n) { - return Err(ParseError::Dispatch { - error: DispatchError::CommandDisabled, - command_name: n, - }); - } - - if let Some((cmd, map)) = r { - stream.increment(n.len()); - - if config.with_whitespace.commands { - stream.take_while_char(char::is_whitespace); - } - - check_discrepancy(ctx, msg, config, &cmd.options).await.map_err(|e| { - ParseError::Dispatch { - error: e, - command_name: n, - } - })?; - - if map.is_empty() { - return Ok(cmd); - } - - return match parse_cmd(stream, ctx, msg, config, &map).await { - Err(ParseError::UnrecognisedCommand(Some(_))) => Ok(cmd), - res => res, - }; - } - - Err(ParseError::UnrecognisedCommand(Some(n.to_string()))) - } - .boxed() -} - -fn parse_group<'a>( - stream: &'a mut Stream<'_>, - ctx: &'a Context, - msg: &'a Message, - config: &'a Configuration, - map: &'a GroupMap, -) -> BoxFuture<'a, Result<(&'static CommandGroup, Arc), ParseError>> { - async move { - let (n, o) = try_parse(stream, map, config.by_space, ToString::to_string); - - if let Some((group, map, commands)) = o { - stream.increment(n.len()); - - if config.with_whitespace.groups { - stream.take_while_char(char::is_whitespace); - } - - check_discrepancy(ctx, msg, config, &group.options).await.map_err(|e| { - ParseError::Dispatch { - error: e, - command_name: n, - } - })?; - - if map.is_empty() { - return Ok((group, commands)); - } - - return match parse_group(stream, ctx, msg, config, &map).await { - Err(ParseError::UnrecognisedCommand(None)) => Ok((group, commands)), - res => res, - }; - } - - Err(ParseError::UnrecognisedCommand(None)) - } - .boxed() -} - -#[inline] -async fn handle_command<'a>( - stream: &'a mut Stream<'_>, - ctx: &'a Context, - msg: &'a Message, - config: &'a Configuration, - map: &'a CommandMap, - group: &'static CommandGroup, -) -> Result { - match parse_cmd(stream, ctx, msg, config, map).await { - Ok(command) => Ok(Invoke::Command { - group, - command, - }), - Err(err) => match group.options.default_command { - Some(command) => { - check_discrepancy(ctx, msg, config, &command.options).await.map_err(|e| { - ParseError::Dispatch { - error: e, - command_name: command.options.names[0].to_string(), - } - })?; - - Ok(Invoke::Command { - group, - command, - }) - }, - None => Err(err), - }, - } -} - -#[inline] -async fn handle_group<'a>( - stream: &mut Stream<'_>, - ctx: &'a Context, - msg: &'a Message, - config: &'a Configuration, - map: &'a GroupMap, -) -> Result { - match parse_group(stream, ctx, msg, config, map).await { - Ok((group, map)) => handle_command(stream, ctx, msg, config, &map, group).await, - Err(error) => Err(error), - } -} - -#[derive(Debug)] -pub enum ParseError { - UnrecognisedCommand(Option), - Dispatch { error: DispatchError, command_name: String }, -} - -fn is_unrecognised(res: &Result) -> bool { - matches!(res, Err(ParseError::UnrecognisedCommand(_))) -} - -/// Parse a command from the message. -/// -/// The "command" may be: -/// 1. A *help command* that provides a friendly browsing interface of all groups and commands, -/// explaining what each of them are, how they are laid out and how to invoke them. There can -/// only one help command registered, but might have many names defined for invocation of itself. -/// -/// 2. A command defined under another command or a group, which may also belong to another group -/// and so on. To invoke this command, all names and prefixes of its parent commands and groups -/// must be specified before it. -pub async fn command( - ctx: &Context, - msg: &Message, - stream: &mut Stream<'_>, - groups: &[(&'static CommandGroup, Map)], - config: &Configuration, - help_was_set: Option<&[&'static str]>, -) -> Result { - // Precedence is taken over commands named as one of the help names. - if let Some(names) = help_was_set { - for name in names { - let n = to_lowercase(config, stream.peek_for_char(name.chars().count())); - - if name == &n { - stream.increment(n.len()); - - stream.take_while_char(char::is_whitespace); - - return Ok(Invoke::Help(name)); - } - } - } - - let mut last = Err::(ParseError::UnrecognisedCommand(None)); - let mut is_prefixless = false; - - for (group, map) in groups { - match map { - // Includes [group] itself. - Map::WithPrefixes(map) => { - let res = handle_group(stream, ctx, msg, config, map).await; - - if !is_unrecognised(&res) { - return res; - } - - if !is_prefixless { - last = res; - } - }, - Map::Prefixless(subgroups, commands) => { - fn command_name_if_recognised(res: &Result) -> Option<&str> { - match res { - Ok(Invoke::Command { - command, .. - }) => Some(command.options.names[0]), - Ok(Invoke::Help(name)) => Some(name), // unreachable; fallback just in case - Err(ParseError::UnrecognisedCommand(_)) => None, - Err(ParseError::Dispatch { - command_name, .. - }) => Some(command_name), - } - } - - is_prefixless = true; - - let res = handle_group(stream, ctx, msg, config, subgroups).await; - - if let Some(command_name) = command_name_if_recognised(&res) { - check_discrepancy(ctx, msg, config, &group.options).await.map_err(|e| { - ParseError::Dispatch { - error: e, - command_name: command_name.to_owned(), - } - })?; - return res; - } - - let res = handle_command(stream, ctx, msg, config, commands, group).await; - - if let Some(command_name) = command_name_if_recognised(&res) { - check_discrepancy(ctx, msg, config, &group.options).await.map_err(|e| { - ParseError::Dispatch { - error: e, - command_name: command_name.to_owned(), - } - })?; - return res; - } - - last = res; - }, - } - } - - last -} - -#[derive(Debug)] -pub enum Invoke { - Command { group: &'static CommandGroup, command: &'static Command }, - Help(&'static str), -} diff --git a/src/framework/standard/structures/buckets.rs b/src/framework/standard/structures/buckets.rs deleted file mode 100644 index 8676a3fc4bc..00000000000 --- a/src/framework/standard/structures/buckets.rs +++ /dev/null @@ -1,569 +0,0 @@ -use std::collections::HashMap; -use std::fmt; -use std::time::{Duration, Instant}; - -use futures::future::BoxFuture; - -use crate::client::Context; -use crate::internal::tokio::spawn_named; -use crate::model::channel::Message; - -type Check = for<'fut> fn(&'fut Context, &'fut Message) -> BoxFuture<'fut, bool>; - -type DelayHook = for<'fut> fn(&'fut Context, &'fut Message) -> BoxFuture<'fut, ()>; - -pub(crate) struct Ratelimit { - pub delay: Duration, - pub limit: Option<(Duration, u32)>, -} -pub(crate) struct UnitRatelimit { - pub last_time: Option, - pub set_time: Instant, - pub tickets: u32, - pub awaiting: u32, - pub is_first_try: bool, -} - -impl UnitRatelimit { - fn new(creation_time: Instant) -> Self { - Self { - last_time: None, - set_time: creation_time, - tickets: 0, - awaiting: 0, - is_first_try: true, - } - } -} - -/// A bucket offers fine-grained control over the execution of commands. -pub(crate) enum Bucket { - /// The bucket will collect tickets for every invocation of a command. - Global(TicketCounter), - /// The bucket will collect tickets per user. - User(TicketCounter), - /// The bucket will collect tickets per guild. - Guild(TicketCounter), - /// The bucket will collect tickets per channel. - Channel(TicketCounter), - /// The bucket will collect tickets per category. - /// - /// This requires the cache, as messages do not contain their channel's category and retrieving - /// channel data via HTTP is costly. - #[cfg(feature = "cache")] - Category(TicketCounter), -} - -impl Bucket { - #[inline] - pub async fn take(&mut self, ctx: &Context, msg: &Message) -> Option { - match self { - Self::Global(counter) => counter.take(ctx, msg, 0).await, - Self::User(counter) => counter.take(ctx, msg, msg.author.id.get()).await, - Self::Guild(counter) => { - if let Some(guild_id) = msg.guild_id { - counter.take(ctx, msg, guild_id.get()).await - } else { - None - } - }, - Self::Channel(counter) => counter.take(ctx, msg, msg.channel_id.get()).await, - // This requires the cache, as messages do not contain their channel's category. - #[cfg(feature = "cache")] - Self::Category(counter) => { - if let Some(category_id) = msg.category_id(ctx).await { - counter.take(ctx, msg, category_id.get()).await - } else { - None - } - }, - } - } - - #[inline] - pub async fn give(&mut self, ctx: &Context, msg: &Message) { - match self { - Self::Global(counter) => counter.give(ctx, msg, 0).await, - Self::User(counter) => counter.give(ctx, msg, msg.author.id.get()).await, - Self::Guild(counter) => { - if let Some(guild_id) = msg.guild_id { - counter.give(ctx, msg, guild_id.get()).await; - } - }, - Self::Channel(counter) => counter.give(ctx, msg, msg.channel_id.get()).await, - // This requires the cache, as messages do not contain their channel's category. - #[cfg(feature = "cache")] - Self::Category(counter) => { - if let Some(category_id) = msg.category_id(ctx).await { - counter.give(ctx, msg, category_id.get()).await; - } - }, - } - } -} - -/// Keeps track of who owns how many tickets and when they accessed the last time. -pub(crate) struct TicketCounter { - pub ratelimit: Ratelimit, - pub tickets_for: HashMap, - pub check: Option, - pub delay_action: Option, - pub await_ratelimits: u32, -} - -/// Contains information about a rate limit. -#[derive(Debug)] -pub struct RateLimitInfo { - /// Time to elapse in order to invoke a command again. - pub rate_limit: Duration, - /// Amount of active delays by this target. - pub active_delays: u32, - /// Maximum delays that this target can invoke. - pub max_delays: u32, - /// Whether this is the first time the rate limit info has been returned for this target - /// without the rate limit to elapse. - pub is_first_try: bool, - /// How the command invocation has been treated by the framework. - pub action: RateLimitAction, -} - -/// Action taken for the command invocation. -#[derive(Debug)] -pub enum RateLimitAction { - /// Invocation has been delayed. - Delayed, - /// Tried to delay invocation but maximum of delays reached. - FailedDelay, - /// Cancelled the invocation due to time or ticket reasons. - Cancelled, -} - -impl RateLimitInfo { - /// Gets the duration of the rate limit in seconds. - #[inline] - #[must_use] - pub fn as_secs(&self) -> u64 { - self.rate_limit.as_secs() - } - - /// Gets the duration of the rate limit in milliseconds. - #[inline] - #[must_use] - pub fn as_millis(&self) -> u128 { - self.rate_limit.as_millis() - } - - /// Gets the duration of the rate limit in microseconds. - #[inline] - #[must_use] - pub fn as_micros(&self) -> u128 { - self.rate_limit.as_micros() - } -} - -impl TicketCounter { - /// Tries to check whether the invocation is permitted by the ticket counter and if a ticket - /// can be taken; it does not return a a ticket but a duration until a ticket can be taken. - /// - /// The duration will be wrapped in an action for the caller to perform if wanted. This may - /// inform them to directly cancel trying to take a ticket or delay the take until later. - /// - /// However there is no contract: It does not matter what the caller ends up doing, receiving - /// some action eventually means no ticket can be taken and the duration must elapse. - pub async fn take(&mut self, ctx: &Context, msg: &Message, id: u64) -> Option { - if let Some(check) = &self.check { - if !(check)(ctx, msg).await { - return None; - } - } - - let now = Instant::now(); - let Self { - tickets_for, - ratelimit, - .. - } = self; - - let ticket_owner = tickets_for.entry(id).or_insert_with(|| UnitRatelimit::new(now)); - - // Check if too many tickets have been taken already. - // If all tickets are exhausted, return the needed delay for this invocation. - if let Some((timespan, limit)) = ratelimit.limit { - if (ticket_owner.tickets + 1) > limit { - if let Some(ratelimit) = - (ticket_owner.set_time + timespan).checked_duration_since(now) - { - let was_first_try = ticket_owner.is_first_try; - - // Are delay limits left? - let action = if self.await_ratelimits > ticket_owner.awaiting { - ticket_owner.awaiting += 1; - - if let Some(delay_action) = self.delay_action { - let ctx = ctx.clone(); - let msg = msg.clone(); - - spawn_named("buckets::delay_action", async move { - delay_action(&ctx, &msg).await; - }); - } - - RateLimitAction::Delayed - // Is this bucket utilising delay limits? - } else if self.await_ratelimits > 0 { - ticket_owner.is_first_try = false; - - RateLimitAction::FailedDelay - } else { - ticket_owner.is_first_try = false; - - RateLimitAction::Cancelled - }; - - return Some(RateLimitInfo { - rate_limit: ratelimit, - active_delays: ticket_owner.awaiting, - max_delays: self.await_ratelimits, - action, - is_first_try: was_first_try, - }); - } - ticket_owner.tickets = 0; - ticket_owner.set_time = now; - } - } - - // Check if `ratelimit.delay`-time passed between the last and the current invocation - // If the time did not pass, return the needed delay for this invocation. - if let Some(ratelimit) = - ticket_owner.last_time.and_then(|x| (x + ratelimit.delay).checked_duration_since(now)) - { - let was_first_try = ticket_owner.is_first_try; - - // Are delay limits left? - let action = if self.await_ratelimits > ticket_owner.awaiting { - ticket_owner.awaiting += 1; - - if let Some(delay_action) = self.delay_action { - let ctx = ctx.clone(); - let msg = msg.clone(); - - spawn_named("buckets::delay_action", async move { - delay_action(&ctx, &msg).await; - }); - } - - RateLimitAction::Delayed - // Is this bucket utilising delay limits? - } else if self.await_ratelimits > 0 { - ticket_owner.is_first_try = false; - - RateLimitAction::FailedDelay - } else { - RateLimitAction::Cancelled - }; - - return Some(RateLimitInfo { - rate_limit: ratelimit, - active_delays: ticket_owner.awaiting, - max_delays: self.await_ratelimits, - action, - is_first_try: was_first_try, - }); - } - ticket_owner.awaiting = ticket_owner.awaiting.saturating_sub(1); - ticket_owner.tickets += 1; - ticket_owner.is_first_try = true; - ticket_owner.last_time = Some(now); - - None - } - - /// Reverts the last ticket step performed by returning a ticket for the matching ticket - /// holder. Only call this if the mutable owner already took a ticket in this atomic execution - /// of calling `take` and `give`. - pub async fn give(&mut self, ctx: &Context, msg: &Message, id: u64) { - if let Some(check) = &self.check { - if !(check)(ctx, msg).await { - return; - } - } - - if let Some(ticket_owner) = self.tickets_for.get_mut(&id) { - // Remove a ticket if one is available. - if ticket_owner.tickets > 0 { - ticket_owner.tickets -= 1; - } - - let delay = self.ratelimit.delay; - // Subtract one step of time that would have to pass. - // This tries to bypass a problem of keeping track of when tickets were taken. - // When a ticket is taken, the bucket sets `last_time`, by subtracting the delay, once - // a ticket is allowed to be taken. - // If the value is set to `None` this could possibly reset the bucket. - ticket_owner.last_time = ticket_owner.last_time.and_then(|i| i.checked_sub(delay)); - } - } -} - -/// An error struct that can be returned from a command to set the bucket one step back. -#[derive(Debug)] -pub struct RevertBucket; - -impl fmt::Display for RevertBucket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("RevertBucket") - } -} - -impl std::error::Error for RevertBucket {} - -/// Decides what a bucket will use to collect tickets for. -#[derive(Debug)] -pub enum LimitedFor { - /// The bucket will collect tickets for every invocation of a command. - Global, - /// The bucket will collect tickets per user. - User, - /// The bucket will collect tickets per guild. - Guild, - /// The bucket will collect tickets per channel. - Channel, - /// The bucket will collect tickets per category. - /// - /// This requires the cache, as messages do not contain their channel's category. - #[cfg(feature = "cache")] - Category, -} - -impl Default for LimitedFor { - /// We use the previous behaviour of buckets as default. - fn default() -> Self { - Self::User - } -} - -pub struct BucketBuilder { - pub(crate) delay: Duration, - pub(crate) time_span: Duration, - pub(crate) limit: u32, - pub(crate) check: Option, - pub(crate) delay_action: Option, - pub(crate) limited_for: LimitedFor, - pub(crate) await_ratelimits: u32, -} - -impl Default for BucketBuilder { - fn default() -> Self { - Self { - delay: Duration::default(), - time_span: Duration::default(), - limit: 1, - check: None, - delay_action: None, - limited_for: LimitedFor::default(), - await_ratelimits: 0, - } - } -} - -impl BucketBuilder { - /// A bucket collecting tickets per command invocation. - #[must_use] - pub fn new_global() -> Self { - Self { - limited_for: LimitedFor::Global, - ..Default::default() - } - } - - /// A bucket collecting tickets per user. - #[must_use] - pub fn new_user() -> Self { - Self { - limited_for: LimitedFor::User, - ..Default::default() - } - } - - /// A bucket collecting tickets per guild. - #[must_use] - pub fn new_guild() -> Self { - Self { - limited_for: LimitedFor::Guild, - ..Default::default() - } - } - - /// A bucket collecting tickets per channel. - #[must_use] - pub fn new_channel() -> Self { - Self { - limited_for: LimitedFor::Channel, - ..Default::default() - } - } - - /// A bucket collecting tickets per channel category. - /// - /// This requires the cache, as messages do not contain their channel's category. - #[cfg(feature = "cache")] - #[must_use] - pub fn new_category() -> Self { - Self { - limited_for: LimitedFor::Category, - ..Default::default() - } - } - - /// The "break" time between invocations of a command. - /// - /// Expressed in seconds. - #[inline] - #[must_use] - pub fn delay(mut self, secs: u64) -> Self { - self.delay = Duration::from_secs(secs); - self - } - - /// How long the bucket will apply for. - /// - /// Expressed in seconds. - #[inline] - #[must_use] - pub fn time_span(mut self, secs: u64) -> Self { - self.time_span = Duration::from_secs(secs); - self - } - - /// Number of invocations allowed per [`Self::time_span`]. - #[inline] - #[must_use] - pub fn limit(mut self, n: u32) -> Self { - self.limit = n; - self - } - - /// Middleware confirming (or denying) that the bucket is eligible to apply. For instance, to - /// limit the bucket to just one user. - #[inline] - #[must_use] - pub fn check(mut self, check: Check) -> Self { - self.check = Some(check); - self - } - - /// This function is called when a user's command invocation is delayed when: - /// 1. `await_ratelimits` is set to a non zero value (the default is 0). - /// 2. user's message rests comfortably within `await_ratelimits` (ex. if you set it to 1 then - /// it will only respond once when the delay is first exceeded). - /// - /// For convenience, this function will automatically raise `await_ratelimits` to at least 1. - /// - /// You can use this to, for example, send a custom response when someone exceeds the amount of - /// commands they're allowed to make. - /// - /// # Examples - /// - /// ```rust - /// # async fn run() -> Result<(), Box> { - /// use serenity::framework::standard::macros::{command, group}; - /// use serenity::framework::standard::{BucketBuilder, Configuration, CommandResult, StandardFramework}; - /// use serenity::model::channel::Message; - /// use serenity::prelude::*; - /// - /// #[command] - /// #[bucket = "example_bucket"] - /// async fn example_command(ctx: &Context, msg: &Message) -> CommandResult { - /// msg.reply(ctx, "Example message, You can only repeat this once every 10 seconds").await?; - /// - /// Ok(()) - /// } - /// - /// async fn example_overuse_response(ctx: &Context, msg: &Message) { - /// msg.reply(ctx, "I told you that you can't call this command less than every 10 seconds!").await.unwrap(); - /// } - /// - /// #[group] - /// #[commands(example_command)] - /// struct General; - /// - /// let token = std::env::var("DISCORD_TOKEN")?; - /// - /// let framework = StandardFramework::new() - /// .bucket("example_bucket", BucketBuilder::default() - /// // We initialise the bucket with the function we want to run - /// .delay_action(|ctx, msg| { - /// Box::pin(example_overuse_response(ctx, msg)) - /// }) - /// .delay(10) // We set the delay to 10 seconds - /// .await_ratelimits(1) // We override the default behavior so that the function actually gets run - /// ) - /// .await - /// .group(&GENERAL_GROUP); - /// - /// framework.configure(Configuration::new().prefix("~")); - /// - /// let mut client = Client::builder(&token, GatewayIntents::default()) - /// .framework(framework) - /// .await?; - /// - /// client.start().await?; - /// # Ok(()) - /// # } - /// ``` - #[inline] - #[must_use] - pub fn delay_action(mut self, action: DelayHook) -> Self { - self.delay_action = Some(action); - if self.await_ratelimits == 0 { - self.await_ratelimits = 1; - } - - self - } - - /// Limit the bucket for a specific type of `target`. - #[inline] - #[must_use] - pub fn limit_for(mut self, target: LimitedFor) -> Self { - self.limited_for = target; - self - } - - /// If this is set to an `amount` greater than `0`, the invocation of the command will be - /// delayed `amount` times instead of stopping command dispatch. - /// - /// By default this value is `0` and rate limits will cancel instead. - #[inline] - #[must_use] - pub fn await_ratelimits(mut self, amount: u32) -> Self { - self.await_ratelimits = amount; - self - } - - /// Constructs the bucket. - #[inline] - pub(crate) fn construct(self) -> Bucket { - let counter = TicketCounter { - ratelimit: Ratelimit { - delay: self.delay, - limit: Some((self.time_span, self.limit)), - }, - tickets_for: HashMap::new(), - check: self.check, - delay_action: self.delay_action, - await_ratelimits: self.await_ratelimits, - }; - - match self.limited_for { - LimitedFor::User => Bucket::User(counter), - LimitedFor::Guild => Bucket::Guild(counter), - LimitedFor::Channel => Bucket::Channel(counter), - // This requires the cache, as messages do not contain their channel's category. - #[cfg(feature = "cache")] - LimitedFor::Category => Bucket::Category(counter), - LimitedFor::Global => Bucket::Global(counter), - } - } -} diff --git a/src/framework/standard/structures/check.rs b/src/framework/standard/structures/check.rs deleted file mode 100644 index 73599ca56bc..00000000000 --- a/src/framework/standard/structures/check.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::error::Error; -use std::fmt; - -use futures::future::BoxFuture; - -use crate::client::Context; -use crate::framework::standard::{Args, CommandOptions}; -use crate::model::channel::Message; - -/// This type describes why a check has failed. -/// -/// **Note**: The bot-developer is supposed to process this `enum` as the framework is not. It -/// solely serves as a way to inform a user about why a check has failed and for the developer to -/// log given failure (e.g. bugs or statistics) occurring in [`Check`]s. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub enum Reason { - /// No information on the failure. - Unknown, - /// Information dedicated to the user. - User(String), - /// Information purely for logging purposes. - Log(String), - /// Information for the user but also for logging purposes. - UserAndLog { user: String, log: String }, -} - -impl Error for Reason {} - -pub type CheckFunction = for<'fut> fn( - &'fut Context, - &'fut Message, - &'fut mut Args, - &'fut CommandOptions, -) -> BoxFuture<'fut, Result<(), Reason>>; - -/// A check can be part of a command or group and will be executed to determine whether a user is -/// permitted to use related item. -/// -/// Additionally, a check may hold additional settings. -pub struct Check { - /// Name listed in help-system. - pub name: &'static str, - /// Function that will be executed. - pub function: CheckFunction, - /// Whether a check should be evaluated in the help-system. `false` will ignore check and won't - /// fail execution. - pub check_in_help: bool, - /// Whether a check shall be listed in the help-system. `false` won't affect whether the check - /// will be evaluated help, solely [`Self::check_in_help`] sets this. - pub display_in_help: bool, -} - -impl fmt::Debug for Check { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Check") - .field("name", &self.name) - .field("function", &"") - .field("check_in_help", &self.check_in_help) - .field("display_in_help", &self.display_in_help) - .finish() - } -} - -impl fmt::Display for Reason { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Unknown => f.write_str("Unknown"), - Self::User(reason) => write!(f, "User {reason}"), - Self::Log(reason) => write!(f, "Log {reason}"), - Self::UserAndLog { - user, - log, - } => { - write!(f, "UserAndLog {{user: {user}, log: {log}}}") - }, - } - } -} - -impl PartialEq for Check { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - } -} diff --git a/src/framework/standard/structures/mod.rs b/src/framework/standard/structures/mod.rs deleted file mode 100644 index 864ec076667..00000000000 --- a/src/framework/standard/structures/mod.rs +++ /dev/null @@ -1,284 +0,0 @@ -use std::collections::HashSet; -use std::error::Error as StdError; -use std::fmt; - -use futures::future::BoxFuture; - -use super::Args; -use crate::client::Context; -use crate::model::channel::Message; -use crate::model::id::UserId; -use crate::model::permissions::Permissions; -use crate::model::Colour; - -pub mod buckets; -mod check; - -pub use self::check::*; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[non_exhaustive] -pub enum OnlyIn { - Dm, - Guild, - None, -} - -impl Default for OnlyIn { - fn default() -> Self { - Self::None - } -} - -#[derive(Debug, Default, PartialEq)] -pub struct CommandOptions { - /// A set of checks to be called prior to executing the command. The checks will short-circuit - /// on the first check that returns `false`. - pub checks: &'static [&'static Check], - /// Ratelimit bucket. - pub bucket: Option<&'static str>, - /// Names that the command can be referred to. - pub names: &'static [&'static str], - /// Command description, used by other commands. - pub desc: Option<&'static str>, - /// Delimiters used to split the arguments of the command by. If empty, the [global delimiters] - /// are used. - /// - /// [global delimiters]: super::Configuration::delimiters - pub delimiters: &'static [&'static str], - /// Command usage schema, used by other commands. - pub usage: Option<&'static str>, - /// Example arguments, used by other commands. - pub examples: &'static [&'static str], - /// Minimum amount of arguments that should be passed. - pub min_args: Option, - /// Maximum amount of arguments that can be passed. - pub max_args: Option, - /// Roles allowed to use this command. - pub allowed_roles: &'static [&'static str], - /// Permissions required to use this command. - pub required_permissions: Permissions, - /// Whether the command should be displayed in help list or not, used by other commands. - pub help_available: bool, - /// Whether the command can only be used in dms or guilds; or both. - pub only_in: OnlyIn, - /// Whether the command can only be used by owners or not. - pub owners_only: bool, - /// Whether the command treats owners as normal users. - pub owner_privilege: bool, - /// Other commands belonging to this command. - pub sub_commands: &'static [&'static Command], -} - -pub type CommandError = Box; -pub type CommandResult = std::result::Result; -pub type CommandFn = - for<'fut> fn(&'fut Context, &'fut Message, Args) -> BoxFuture<'fut, CommandResult>; - -pub struct Command { - pub fun: CommandFn, - pub options: &'static CommandOptions, -} - -impl fmt::Debug for Command { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Command").field("options", &self.options).finish_non_exhaustive() - } -} - -impl PartialEq for Command { - #[inline] - fn eq(&self, other: &Command) -> bool { - (self.fun as usize == other.fun as usize) && (self.options == other.options) - } -} - -pub type HelpCommandFn = for<'fut> fn( - &'fut Context, - &'fut Message, - Args, - &'fut HelpOptions, - &'fut [&'static CommandGroup], - HashSet, -) -> BoxFuture<'fut, CommandResult>; - -pub struct HelpCommand { - pub fun: HelpCommandFn, - pub options: &'static HelpOptions, -} - -impl fmt::Debug for HelpCommand { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("HelpCommand") - .field("fun", &"") - .field("options", &self.options) - .finish() - } -} - -impl PartialEq for HelpCommand { - #[inline] - fn eq(&self, other: &HelpCommand) -> bool { - (self.fun as usize == other.fun as usize) && (self.options == other.options) - } -} - -/// Describes the behaviour the help-command shall execute once it encounters a command which the -/// user or command fails to meet following criteria : -/// - Lacking required permissions to execute the command. -/// - Lacking required roles to execute the command. -/// - The command can't be used in the current channel (as in `DM only` or `guild only`). -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -#[non_exhaustive] -pub enum HelpBehaviour { - /// The command will be displayed, hence nothing will be done. - Nothing, - /// Strikes a command by applying `~~{command_name}~~`. - Strike, - /// Does not list a command in the help-menu. - Hide, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HelpOptions { - /// Which names should the help command use for dispatching. - /// Defaults to `["help"]` - pub names: &'static [&'static str], - /// Suggests a command's name. - pub suggestion_text: &'static str, - /// If no help is available, this text will be displayed. - pub no_help_available_text: &'static str, - /// How to use a command, `{usage_label}: {command_name} {args}` - pub usage_label: &'static str, - /// Actual sample label, `{usage_sample_label}: {command_name} {args}` - pub usage_sample_label: &'static str, - /// Text labeling ungrouped commands, `{ungrouped_label}: ...` - pub ungrouped_label: &'static str, - /// Text labeling the start of the description. - pub description_label: &'static str, - /// Text labeling grouped commands, `{grouped_label} {group_name}: ...` - pub grouped_label: &'static str, - /// Text labeling a command's alternative names (aliases). - pub aliases_label: &'static str, - /// Text specifying that a command is only usable in a guild. - pub guild_only_text: &'static str, - /// Text labelling a command's names of checks. - pub checks_label: &'static str, - /// Text labelling a command's subcommands - pub sub_commands_label: &'static str, - /// Text specifying that a command is only usable in via DM. - pub dm_only_text: &'static str, - /// Text specifying that a command can be used via DM and in guilds. - pub dm_and_guild_text: &'static str, - /// Text expressing that a command is available. - pub available_text: &'static str, - /// Error-message once a command could not be found. - /// - /// Output-example (without whitespace between both substitutions: - /// `{command_not_found_text}{command_name}` `{command_name}` describes user's input as in: - /// `{prefix}help {command_name}`. - pub command_not_found_text: &'static str, - /// Explains the user on how to use access a single command's details. - pub individual_command_tip: &'static str, - /// Explains reasoning behind strikethrough-commands, see fields requiring [`HelpBehaviour`] - /// for further information. If [`HelpBehaviour::Strike`] is unused, this field will evaluate - /// to [`None`] during creation inside of the help macro. - /// - /// **Note**: Text is only used in direct messages. - pub strikethrough_commands_tip_in_dm: Option<&'static str>, - /// Explains reasoning behind strikethrough-commands, see fields requiring [`HelpBehaviour`] - /// for further information. If [`HelpBehaviour::Strike`] is unused, this field will evaluate - /// to [`None`] during creation inside of the help macro. - /// - /// **Note**: Text is only used in guilds. - pub strikethrough_commands_tip_in_guild: Option<&'static str>, - /// Announcing a group's prefix as in: {group_prefix} {prefix}. - pub group_prefix: &'static str, - /// If a user lacks required roles, this will treat how these commands will be displayed. - pub lacking_role: HelpBehaviour, - /// If a user lacks permissions, this will treat how these commands will be displayed. - pub lacking_permissions: HelpBehaviour, - /// If a user lacks ownership, this will treat how these commands will be displayed. - pub lacking_ownership: HelpBehaviour, - /// If conditions (of a check) may be lacking by the user, this will treat how these commands - /// will be displayed. - pub lacking_conditions: HelpBehaviour, - /// If a user is using the help-command in a channel where a command is not available, - /// this behaviour will be executed. - pub wrong_channel: HelpBehaviour, - /// Colour help-embed will use upon encountering an error. - pub embed_error_colour: Colour, - /// Colour help-embed will use if no error occurred. - pub embed_success_colour: Colour, - /// If not 0, help will check whether a command is similar to searched named. - pub max_levenshtein_distance: usize, - /// Help will use this as prefix to express how deeply nested a command or - /// group is. - pub indention_prefix: &'static str, -} - -#[derive(Debug, Default, PartialEq)] -pub struct GroupOptions { - pub prefixes: &'static [&'static str], - pub only_in: OnlyIn, - pub owners_only: bool, - pub owner_privilege: bool, - pub help_available: bool, - pub allowed_roles: &'static [&'static str], - pub required_permissions: Permissions, - pub checks: &'static [&'static Check], - pub default_command: Option<&'static Command>, - pub description: Option<&'static str>, - pub summary: Option<&'static str>, - pub commands: &'static [&'static Command], - pub sub_groups: &'static [&'static CommandGroup], -} - -#[derive(Debug, PartialEq)] -pub struct CommandGroup { - pub name: &'static str, - pub options: &'static GroupOptions, -} - -#[cfg(test)] -#[cfg(all(feature = "cache", feature = "http"))] -mod levenshtein_tests { - use super::HelpBehaviour; - - #[test] - fn help_behaviour_eq() { - assert_eq!(HelpBehaviour::Hide, std::cmp::max(HelpBehaviour::Hide, HelpBehaviour::Hide)); - assert_eq!( - HelpBehaviour::Strike, - std::cmp::max(HelpBehaviour::Strike, HelpBehaviour::Strike) - ); - assert_eq!( - HelpBehaviour::Nothing, - std::cmp::max(HelpBehaviour::Nothing, HelpBehaviour::Nothing) - ); - } - - #[test] - fn help_behaviour_hide() { - assert_eq!(HelpBehaviour::Hide, std::cmp::max(HelpBehaviour::Hide, HelpBehaviour::Nothing)); - assert_eq!(HelpBehaviour::Hide, std::cmp::max(HelpBehaviour::Hide, HelpBehaviour::Strike)); - } - - #[test] - fn help_behaviour_strike() { - assert_eq!( - HelpBehaviour::Strike, - std::cmp::max(HelpBehaviour::Strike, HelpBehaviour::Nothing) - ); - assert_eq!(HelpBehaviour::Hide, std::cmp::max(HelpBehaviour::Strike, HelpBehaviour::Hide)); - } - - #[test] - fn help_behaviour_nothing() { - assert_eq!( - HelpBehaviour::Strike, - std::cmp::max(HelpBehaviour::Nothing, HelpBehaviour::Strike) - ); - assert_eq!(HelpBehaviour::Hide, std::cmp::max(HelpBehaviour::Nothing, HelpBehaviour::Hide)); - } -} diff --git a/src/gateway/bridge/mod.rs b/src/gateway/bridge/mod.rs index 45543d26711..f5b2bbe845c 100644 --- a/src/gateway/bridge/mod.rs +++ b/src/gateway/bridge/mod.rs @@ -50,12 +50,14 @@ mod shard_runner_message; mod voice; use std::fmt; +use std::num::NonZeroU16; +use std::sync::Arc; use std::time::Duration as StdDuration; pub use self::event::ShardStageUpdateEvent; pub use self::shard_manager::{ShardManager, ShardManagerOptions}; pub use self::shard_messenger::ShardMessenger; -pub use self::shard_queuer::ShardQueuer; +pub use self::shard_queuer::{ShardQueue, ShardQueuer}; pub use self::shard_runner::{ShardRunner, ShardRunnerOptions}; pub use self::shard_runner_message::ShardRunnerMessage; #[cfg(feature = "voice")] @@ -68,13 +70,14 @@ use crate::model::id::ShardId; /// A message to be sent to the [`ShardQueuer`]. #[derive(Clone, Debug)] pub enum ShardQueuerMessage { - /// Message to start a shard, where the 0-index element is the ID of the Shard to start and the - /// 1-index element is the total shards in use. - Start(ShardId, ShardId), + /// Message to set the shard total. + SetShardTotal(NonZeroU16), + /// Message to start a shard. + Start { shard_id: ShardId, concurrent: bool }, /// Message to shutdown the shard queuer. Shutdown, /// Message to dequeue/shutdown a shard. - ShutdownShard(ShardId, u16), + ShutdownShard { shard_id: ShardId, code: u16 }, } /// Information about a [`ShardRunner`]. @@ -92,18 +95,20 @@ pub struct ShardRunnerInfo { pub stage: ConnectionStage, } -impl AsRef for ShardRunnerInfo { - fn as_ref(&self) -> &ShardMessenger { - &self.runner_tx - } -} - /// Newtype around a callback that will be called on every incoming request. As long as this /// collector should still receive events, it should return `true`. Once it returns `false`, it is /// removed. -pub struct CollectorCallback(pub Box bool + Send + Sync>); +#[derive(Clone)] +pub struct CollectorCallback(pub Arc bool + Send + Sync>); + impl std::fmt::Debug for CollectorCallback { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("CollectorCallback").finish() } } + +impl PartialEq for CollectorCallback { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} diff --git a/src/gateway/bridge/shard_manager.rs b/src/gateway/bridge/shard_manager.rs index d20ef98244f..0064f5cb428 100644 --- a/src/gateway/bridge/shard_manager.rs +++ b/src/gateway/bridge/shard_manager.rs @@ -1,5 +1,5 @@ -use std::collections::{HashMap, VecDeque}; -use std::sync::atomic::{AtomicU32, Ordering}; +use std::collections::HashMap; +use std::num::NonZeroU16; use std::sync::Arc; #[cfg(feature = "framework")] use std::sync::OnceLock; @@ -7,17 +7,16 @@ use std::time::Duration; use futures::channel::mpsc::{self, UnboundedReceiver as Receiver, UnboundedSender as Sender}; use futures::{SinkExt, StreamExt}; -use tokio::sync::{Mutex, RwLock}; +use tokio::sync::Mutex; use tokio::time::timeout; -use tracing::{info, instrument, warn}; -use typemap_rev::TypeMap; +use tracing::{info, warn}; #[cfg(feature = "voice")] use super::VoiceGatewayManager; -use super::{ShardId, ShardQueuer, ShardQueuerMessage, ShardRunnerInfo}; +use super::{ShardId, ShardQueue, ShardQueuer, ShardQueuerMessage, ShardRunnerInfo}; #[cfg(feature = "cache")] use crate::cache::Cache; -use crate::client::{EventHandler, RawEventHandler}; +use crate::client::InternalEventHandler; #[cfg(feature = "framework")] use crate::framework::Framework; use crate::gateway::{ConnectionStage, GatewayError, PresenceData}; @@ -34,8 +33,7 @@ use crate::model::gateway::GatewayIntents; /// /// # Examples /// -/// Initialize a shard manager with a framework responsible for shards 0 through 2, of 5 total -/// shards: +/// Initialize a shard manager for shards 0 through 2, of 5 total shards: /// /// ```rust,no_run /// # use std::error::Error; @@ -51,8 +49,7 @@ use crate::model::gateway::GatewayIntents; /// use std::env; /// use std::sync::{Arc, OnceLock}; /// -/// use serenity::client::{EventHandler, RawEventHandler}; -/// use serenity::framework::{Framework, StandardFramework}; +/// use serenity::client::{EventHandler, InternalEventHandler, RawEventHandler}; /// use serenity::gateway::{ShardManager, ShardManagerOptions}; /// use serenity::http::Http; /// use serenity::model::gateway::GatewayIntents; @@ -62,33 +59,30 @@ use crate::model::gateway::GatewayIntents; /// struct Handler; /// /// impl EventHandler for Handler {} -/// impl RawEventHandler for Handler {} /// /// # let http: Arc = unimplemented!(); -/// let ws_url = Arc::new(Mutex::new(http.get_gateway().await?.url)); -/// let data = Arc::new(RwLock::new(TypeMap::new())); +/// let gateway_info = http.get_bot_gateway().await?; +/// +/// let data = Arc::new(()); +/// let shard_total = gateway_info.shards; +/// let ws_url = Arc::from(gateway_info.url); /// let event_handler = Arc::new(Handler) as Arc; -/// let framework = Arc::new(StandardFramework::new()) as Arc; +/// let max_concurrency = std::num::NonZeroU16::MIN; /// /// ShardManager::new(ShardManagerOptions { /// data, -/// event_handlers: vec![event_handler], -/// raw_event_handlers: vec![], -/// framework: Arc::new(OnceLock::from(framework)), -/// // the shard index to start initiating from -/// shard_index: 0, -/// // the number of shards to initiate (this initiates 0, 1, and 2) -/// shard_init: 3, -/// // the total number of shards in use -/// shard_total: 5, +/// event_handler: Some(InternalEventHandler::Normal(event_handler)), +/// framework: Arc::new(OnceLock::new()), /// # #[cfg(feature = "voice")] /// # voice_manager: None, /// ws_url, +/// shard_total, /// # #[cfg(feature = "cache")] /// # cache: unimplemented!(), /// # http, /// intents: GatewayIntents::non_privileged(), /// presence: None, +/// max_concurrency, /// }); /// # Ok(()) /// # } @@ -103,13 +97,6 @@ pub struct ShardManager { /// **Note**: It is highly unrecommended to mutate this yourself unless you need to. Instead /// prefer to use methods on this struct that are provided where possible. pub runners: Arc>>, - /// The index of the first shard to initialize, 0-indexed. - // Atomics are used here to allow for mutation without requiring a mutable reference to self. - shard_index: AtomicU32, - /// The number of shards to initialize. - shard_init: AtomicU32, - /// The total shards in use, 1-indexed. - shard_total: AtomicU32, shard_queuer: Sender, // We can safely use a Mutex for this field, as it is only ever used in one single place // and only is ever used to receive a single message @@ -131,10 +118,7 @@ impl ShardManager { let manager = Arc::new(Self { return_value_tx: Mutex::new(return_value_tx), - shard_index: AtomicU32::new(opt.shard_index), - shard_init: AtomicU32::new(opt.shard_init), shard_queuer: shard_queue_tx, - shard_total: AtomicU32::new(opt.shard_total), shard_shutdown: Mutex::new(shutdown_recv), shard_shutdown_send: shutdown_send, runners: Arc::clone(&runners), @@ -143,18 +127,18 @@ impl ShardManager { let mut shard_queuer = ShardQueuer { data: opt.data, - event_handlers: opt.event_handlers, - raw_event_handlers: opt.raw_event_handlers, + event_handler: opt.event_handler, #[cfg(feature = "framework")] framework: opt.framework, last_start: None, manager: Arc::clone(&manager), - queue: VecDeque::new(), + queue: ShardQueue::new(opt.max_concurrency), runners, rx: shard_queue_rx, #[cfg(feature = "voice")] voice_manager: opt.voice_manager, ws_url: opt.ws_url, + shard_total: opt.shard_total, #[cfg(feature = "cache")] cache: opt.cache, http: opt.http, @@ -181,33 +165,14 @@ impl ShardManager { /// /// This will communicate shard boots with the [`ShardQueuer`] so that they are properly /// queued. - #[instrument(skip(self))] - pub fn initialize(&self) -> Result<()> { - let shard_index = self.shard_index.load(Ordering::Relaxed); - let shard_init = self.shard_init.load(Ordering::Relaxed); - let shard_total = self.shard_total.load(Ordering::Relaxed); - + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + pub fn initialize(&self, shard_index: u16, shard_init: u16, shard_total: NonZeroU16) { let shard_to = shard_index + shard_init; + self.set_shard_total(shard_total); for shard_id in shard_index..shard_to { - self.boot([ShardId(shard_id), ShardId(shard_total)]); + self.boot(ShardId(shard_id), true); } - - Ok(()) - } - - /// Sets the new sharding information for the manager. - /// - /// This will shutdown all existing shards. - /// - /// This will _not_ instantiate the new shards. - #[instrument(skip(self))] - pub async fn set_shards(&self, index: u32, init: u32, total: u32) { - self.shutdown_all().await; - - self.shard_index.store(index, Ordering::Relaxed); - self.shard_init.store(init, Ordering::Relaxed); - self.shard_total.store(total, Ordering::Relaxed); } /// Restarts a shard runner. @@ -230,21 +195,18 @@ impl ShardManager { /// ``` /// /// [`ShardRunner`]: super::ShardRunner - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn restart(&self, shard_id: ShardId) { - info!("Restarting shard {}", shard_id); + info!("Restarting shard {shard_id}"); self.shutdown(shard_id, 4000).await; - - let shard_total = self.shard_total.load(Ordering::Relaxed); - - self.boot([shard_id, ShardId(shard_total)]); + self.boot(shard_id, false); } /// Returns the [`ShardId`]s of the shards that have been instantiated and currently have a /// valid [`ShardRunner`]. /// /// [`ShardRunner`]: super::ShardRunner - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn shards_instantiated(&self) -> Vec { self.runners.lock().await.keys().copied().collect() } @@ -257,7 +219,7 @@ impl ShardManager { /// **Note**: If the receiving end of an mpsc channel - owned by the shard runner - no longer /// exists, then the shard runner will not know it should shut down. This _should never happen_. /// It may already be stopped. - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn shutdown(&self, shard_id: ShardId, code: u16) { const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(5); @@ -266,9 +228,10 @@ impl ShardManager { { let mut shard_shutdown = self.shard_shutdown.lock().await; - drop( - self.shard_queuer.unbounded_send(ShardQueuerMessage::ShutdownShard(shard_id, code)), - ); + drop(self.shard_queuer.unbounded_send(ShardQueuerMessage::ShutdownShard { + shard_id, + code, + })); match timeout(TIMEOUT, shard_shutdown.next()).await { Ok(Some(shutdown_shard_id)) => { if shutdown_shard_id != shard_id { @@ -299,7 +262,7 @@ impl ShardManager { /// /// If you only need to shutdown a select number of shards, prefer looping over the /// [`Self::shutdown`] method. - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn shutdown_all(&self) { let keys = { let runners = self.runners.lock().await; @@ -324,15 +287,23 @@ impl ShardManager { drop(self.return_value_tx.lock().await.unbounded_send(Ok(()))); } - #[instrument(skip(self))] - fn boot(&self, shard_info: [ShardId; 2]) { - info!("Telling shard queuer to start shard {}", shard_info[0]); - - let msg = ShardQueuerMessage::Start(shard_info[0], shard_info[1]); + fn set_shard_total(&self, shard_total: NonZeroU16) { + info!("Setting shard total to {shard_total}"); + let msg = ShardQueuerMessage::SetShardTotal(shard_total); drop(self.shard_queuer.unbounded_send(msg)); } + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + fn boot(&self, shard_id: ShardId, concurrent: bool) { + info!("Telling shard queuer to start shard {shard_id}"); + + drop(self.shard_queuer.unbounded_send(ShardQueuerMessage::Start { + shard_id, + concurrent, + })); + } + /// Returns the gateway intents used for this gateway connection. #[must_use] pub fn intents(&self) -> GatewayIntents { @@ -351,10 +322,10 @@ impl ShardManager { } } - pub async fn restart_shard(&self, id: ShardId) { - self.restart(id).await; - if let Err(e) = self.shard_shutdown_send.unbounded_send(id) { - tracing::warn!("failed to notify about finished shutdown: {}", e); + pub async fn restart_shard(&self, shard_id: ShardId) { + self.restart(shard_id).await; + if let Err(e) = self.shard_shutdown_send.unbounded_send(shard_id) { + tracing::warn!("failed to notify about finished shutdown: {e}"); } } @@ -384,20 +355,18 @@ impl Drop for ShardManager { } pub struct ShardManagerOptions { - pub data: Arc>, - pub event_handlers: Vec>, - pub raw_event_handlers: Vec>, + pub data: Arc, + pub event_handler: Option, #[cfg(feature = "framework")] pub framework: Arc>>, - pub shard_index: u32, - pub shard_init: u32, - pub shard_total: u32, #[cfg(feature = "voice")] pub voice_manager: Option>, - pub ws_url: Arc>, + pub ws_url: Arc, + pub shard_total: NonZeroU16, #[cfg(feature = "cache")] pub cache: Arc, pub http: Arc, pub intents: GatewayIntents, pub presence: Option, + pub max_concurrency: NonZeroU16, } diff --git a/src/gateway/bridge/shard_messenger.rs b/src/gateway/bridge/shard_messenger.rs index 685cc7eb0ed..39bb10fb82f 100644 --- a/src/gateway/bridge/shard_messenger.rs +++ b/src/gateway/bridge/shard_messenger.rs @@ -21,7 +21,7 @@ use crate::model::prelude::*; pub struct ShardMessenger { pub(crate) tx: Sender, #[cfg(feature = "collector")] - pub(crate) collectors: Arc>>, + pub(crate) collectors: Arc>>, } impl ShardMessenger { @@ -30,7 +30,6 @@ impl ShardMessenger { /// If you are using the [`Client`], you do not need to do this. /// /// [`Client`]: crate::Client - #[inline] #[must_use] pub fn new(shard: &ShardRunner) -> Self { Self { @@ -58,24 +57,17 @@ impl ShardMessenger { /// parameter: /// /// ```rust,no_run - /// # use tokio::sync::Mutex; - /// # use serenity::model::gateway::{GatewayIntents, ShardInfo}; - /// # use serenity::model::id::ShardId; /// # use serenity::gateway::{ChunkGuildFilter, Shard}; - /// # use std::sync::Arc; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let mutex = Arc::new(Mutex::new("".to_string())); - /// # - /// # let shard_info = ShardInfo { - /// # id: ShardId(0), - /// # total: 1, - /// # }; - /// # let mut shard = Shard::new(mutex.clone(), "", shard_info, GatewayIntents::all(), None).await?; - /// # + /// # async fn run(mut shard: Shard) -> Result<(), Box> { /// use serenity::model::id::GuildId; /// - /// shard.chunk_guild(GuildId::new(81384788765712384), Some(2000), false, ChunkGuildFilter::None, None); + /// shard.chunk_guild( + /// GuildId::new(81384788765712384), + /// Some(2000), + /// false, + /// ChunkGuildFilter::None, + /// None, + /// ); /// # Ok(()) /// # } /// ``` @@ -84,22 +76,8 @@ impl ShardMessenger { /// and a nonce of `"request"`: /// /// ```rust,no_run - /// # use tokio::sync::Mutex; - /// # use serenity::model::gateway::{GatewayIntents, ShardInfo}; - /// # use serenity::model::id::ShardId; /// # use serenity::gateway::{ChunkGuildFilter, Shard}; - /// # use std::sync::Arc; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let mutex = Arc::new(Mutex::new("".to_string())); - /// # - /// # let shard_info = ShardInfo { - /// # id: ShardId(0), - /// # total: 1, - /// # }; - /// # - /// # let mut shard = Shard::new(mutex.clone(), "", shard_info, GatewayIntents::all(), None).await?;; - /// # + /// # async fn run(mut shard: Shard) -> Result<(), Box> { /// use serenity::model::id::GuildId; /// /// shard.chunk_guild( @@ -138,21 +116,8 @@ impl ShardMessenger { /// Setting the current activity to playing `"Heroes of the Storm"`: /// /// ```rust,no_run - /// # use tokio::sync::Mutex; - /// # use serenity::gateway::{Shard}; - /// # use serenity::model::id::ShardId; - /// # use serenity::model::gateway::{GatewayIntents, ShardInfo}; - /// # use std::sync::Arc; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let mutex = Arc::new(Mutex::new("".to_string())); - /// # - /// # let shard_info = ShardInfo { - /// # id: ShardId(0), - /// # total: 1, - /// # }; - /// # - /// # let mut shard = Shard::new(mutex.clone(), "", shard_info, GatewayIntents::all(), None).await?; + /// # use serenity::gateway::Shard; + /// # async fn run(mut shard: Shard) -> Result<(), Box> { /// use serenity::gateway::ActivityData; /// /// shard.set_activity(Some(ActivityData::playing("Heroes of the Storm"))); @@ -172,20 +137,8 @@ impl ShardMessenger { /// Set the current user as playing `"Heroes of the Storm"` and being online: /// /// ```rust,ignore - /// # use tokio::sync::Mutex; /// # use serenity::gateway::Shard; - /// # use std::sync::Arc; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let mutex = Arc::new(Mutex::new("".to_string())); - /// # - /// # let shard_info = ShardInfo { - /// # id: 0, - /// # total: 1, - /// # }; - /// # - /// # let mut shard = Shard::new(mutex.clone(), "", shard_info, None).await?; - /// # + /// # async fn run(shard: Shard) -> Result<(), Box> { /// use serenity::gateway::ActivityData; /// use serenity::model::user::OnlineStatus; /// @@ -214,21 +167,8 @@ impl ShardMessenger { /// Setting the current online status for the shard to [`DoNotDisturb`]. /// /// ```rust,no_run - /// # use tokio::sync::Mutex; - /// # use serenity::gateway::{Shard}; - /// # use serenity::model::id::ShardId; - /// # use serenity::model::gateway::{GatewayIntents, ShardInfo}; - /// # use std::sync::Arc; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let mutex = Arc::new(Mutex::new("".to_string())); - /// # let shard_info = ShardInfo { - /// # id: ShardId(0), - /// # total: 1, - /// # }; - /// # - /// # let mut shard = Shard::new(mutex.clone(), "", shard_info, GatewayIntents::all(), None).await?; - /// # + /// # use serenity::gateway::Shard; + /// # async fn run(mut shard: Shard) -> Result<(), Box> { /// use serenity::model::user::OnlineStatus; /// /// shard.set_status(OnlineStatus::DoNotDisturb); @@ -263,7 +203,6 @@ impl ShardMessenger { } /// Sends a message to the shard. - #[inline] pub fn send_to_shard(&self, msg: ShardRunnerMessage) { if let Err(e) = self.tx.unbounded_send(msg) { tracing::warn!("failed to send ShardRunnerMessage to shard: {}", e); @@ -272,12 +211,6 @@ impl ShardMessenger { #[cfg(feature = "collector")] pub fn add_collector(&self, collector: CollectorCallback) { - self.collectors.lock().expect("poison").push(collector); - } -} - -impl AsRef for ShardMessenger { - fn as_ref(&self) -> &ShardMessenger { - self + self.collectors.write().push(collector); } } diff --git a/src/gateway/bridge/shard_queuer.rs b/src/gateway/bridge/shard_queuer.rs index 2c0571a8fa5..7d8e2d98d3c 100644 --- a/src/gateway/bridge/shard_queuer.rs +++ b/src/gateway/bridge/shard_queuer.rs @@ -1,14 +1,14 @@ use std::collections::{HashMap, VecDeque}; +use std::num::NonZeroU16; use std::sync::Arc; #[cfg(feature = "framework")] use std::sync::OnceLock; use futures::channel::mpsc::UnboundedReceiver as Receiver; use futures::StreamExt; -use tokio::sync::{Mutex, RwLock}; +use tokio::sync::Mutex; use tokio::time::{sleep, timeout, Duration, Instant}; -use tracing::{debug, info, instrument, warn}; -use typemap_rev::TypeMap; +use tracing::{debug, info, warn}; #[cfg(feature = "voice")] use super::VoiceGatewayManager; @@ -23,7 +23,7 @@ use super::{ }; #[cfg(feature = "cache")] use crate::cache::Cache; -use crate::client::{EventHandler, RawEventHandler}; +use crate::client::InternalEventHandler; #[cfg(feature = "framework")] use crate::framework::Framework; use crate::gateway::{ConnectionStage, PresenceData, Shard, ShardRunnerMessage}; @@ -42,15 +42,12 @@ pub struct ShardQueuer { /// A copy of [`Client::data`] to be given to runners for contextual dispatching. /// /// [`Client::data`]: crate::Client::data - pub data: Arc>, - /// A reference to an [`EventHandler`], such as the one given to the [`Client`]. + pub data: Arc, + /// A reference to [`EventHandler`] or [`RawEventHandler`]. /// - /// [`Client`]: crate::Client - pub event_handlers: Vec>, - /// A reference to an [`RawEventHandler`], such as the one given to the [`Client`]. - /// - /// [`Client`]: crate::Client - pub raw_event_handlers: Vec>, + /// [`EventHandler`]: crate::client::EventHandler + /// [`RawEventHandler`]: crate::client::RawEventHandler + pub event_handler: Option, /// A copy of the framework #[cfg(feature = "framework")] pub framework: Arc>>, @@ -61,9 +58,7 @@ pub struct ShardQueuer { /// A copy of the [`ShardManager`] to communicate with it. pub manager: Arc, /// The shards that are queued for booting. - /// - /// This will typically be filled with previously failed boots. - pub queue: VecDeque, + pub queue: ShardQueue, /// A copy of the map of shard runners. pub runners: Arc>>, /// A receiver channel for the shard queuer to be told to start shards. @@ -72,7 +67,9 @@ pub struct ShardQueuer { #[cfg(feature = "voice")] pub voice_manager: Option>, /// A copy of the URL to use to connect to the gateway. - pub ws_url: Arc>, + pub ws_url: Arc, + /// The total amount of shards to start. + pub shard_total: NonZeroU16, #[cfg(feature = "cache")] pub cache: Arc, pub http: Arc, @@ -97,40 +94,67 @@ impl ShardQueuer { /// over. /// /// **Note**: This should be run in its own thread due to the blocking nature of the loop. - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn run(&mut self) { - // The duration to timeout from reads over the Rx channel. This can be done in a loop, and - // if the read times out then a shard can be started if one is presently waiting in the - // queue. + // We read from the Rx channel in a loop, and use a timeout of 5 seconds so that we don't + // hang forever. When we receive a command to start a shard, we append it to our queue. The + // queue is popped in batches of shards, which are started in parallel. A batch is fired + // every 5 seconds at minimum in order to avoid being ratelimited. const TIMEOUT: Duration = Duration::from_secs(WAIT_BETWEEN_BOOTS_IN_SECONDS); loop { - match timeout(TIMEOUT, self.rx.next()).await { - Ok(Some(ShardQueuerMessage::Shutdown)) => { - debug!("[Shard Queuer] Received to shutdown."); - self.shutdown_runners().await; - - break; - }, - Ok(Some(ShardQueuerMessage::ShutdownShard(shard, code))) => { - debug!("[Shard Queuer] Received to shutdown shard {} with {}.", shard.0, code); - self.shutdown(shard, code).await; - }, - Ok(Some(ShardQueuerMessage::Start(id, total))) => { - debug!("[Shard Queuer] Received to start shard {} of {}.", id.0, total.0); - self.checked_start(id, total.0).await; - }, - Ok(None) => break, - Err(_) => { - if let Some(shard) = self.queue.pop_front() { - self.checked_start(shard.id, shard.total).await; - } - }, + if let Ok(msg) = timeout(TIMEOUT, self.rx.next()).await { + match msg { + Some(ShardQueuerMessage::SetShardTotal(shard_total)) => { + self.shard_total = shard_total; + }, + Some(ShardQueuerMessage::Start { + shard_id, + concurrent, + }) => { + if concurrent { + // If we're starting multiple shards, we can start them concurrently + // according to `max_concurrency`, and want our batches to be of + // maximal size. + self.queue.push_back(shard_id); + if self.queue.buckets_filled() { + let batch = self.queue.pop_batch(); + self.checked_start_batch(batch).await; + } + } else { + // In cases where we're only starting a single shard (e.g. if we're + // restarting a shard), we assume the queue will never fill up and skip + // using it so that we don't incur a 5 second timeout. + self.checked_start(shard_id).await; + } + }, + Some(ShardQueuerMessage::ShutdownShard { + shard_id, + code, + }) => { + debug!( + "[Shard Queuer] Received to shutdown shard {} with code {}", + shard_id.0, code + ); + self.shutdown(shard_id, code).await; + }, + Some(ShardQueuerMessage::Shutdown) => { + debug!("[Shard Queuer] Received to shutdown all shards"); + self.shutdown_runners().await; + break; + }, + None => break, + } + } else { + // Once we've stopped receiving `Start` commands, we no longer care about the size + // of our batches being maximal. + let batch = self.queue.pop_batch(); + self.checked_start_batch(batch).await; } } } - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] async fn check_last_start(&mut self) { let Some(instant) = self.last_start else { return }; @@ -147,29 +171,48 @@ impl ShardQueuer { sleep(to_sleep).await; } - #[instrument(skip(self))] - async fn checked_start(&mut self, id: ShardId, total: u32) { - debug!("[Shard Queuer] Checked start for shard {} out of {}", id, total); + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + async fn checked_start(&mut self, shard_id: ShardId) { + debug!("[Shard Queuer] Checked start for shard {shard_id}"); + self.check_last_start().await; + self.try_start(shard_id).await; - if let Err(why) = self.start(id, total).await { - warn!("[Shard Queuer] Err starting shard {}: {:?}", id, why); - info!("[Shard Queuer] Re-queueing start of shard {}", id); + self.last_start = Some(Instant::now()); + } - self.queue.push_back(ShardInfo::new(id, total)); + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + async fn checked_start_batch(&mut self, shard_ids: Vec) { + if shard_ids.is_empty() { + return; } + debug!("[Shard Queuer] Starting batch of {} shards", shard_ids.len()); + self.check_last_start().await; + for shard_id in shard_ids { + debug!("[Shard Queuer] Starting shard {shard_id}"); + self.try_start(shard_id).await; + } self.last_start = Some(Instant::now()); } - #[instrument(skip(self))] - async fn start(&mut self, id: ShardId, total: u32) -> Result<()> { - let shard_info = ShardInfo::new(id, total); + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + async fn try_start(&mut self, shard_id: ShardId) { + if let Err(why) = self.start(shard_id).await { + warn!("[Shard Queuer] Err starting shard {shard_id}: {why:?}"); + info!("[Shard Queuer] Re-queueing start of shard {shard_id}"); + + // Try again in the next batch. + self.queue.push_front(shard_id); + } + } + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + async fn start(&mut self, shard_id: ShardId) -> Result<()> { let mut shard = Shard::new( Arc::clone(&self.ws_url), - self.http.token(), - shard_info, + Arc::clone(self.http.token()), + ShardInfo::new(shard_id, self.shard_total), self.intents, self.presence.clone(), ) @@ -180,8 +223,7 @@ impl ShardQueuer { let mut runner = ShardRunner::new(ShardRunnerOptions { data: Arc::clone(&self.data), - event_handlers: self.event_handlers.clone(), - raw_event_handlers: self.raw_event_handlers.clone(), + event_handler: self.event_handler.clone(), #[cfg(feature = "framework")] framework: self.framework.get().cloned(), manager: Arc::clone(&self.manager), @@ -204,12 +246,12 @@ impl ShardQueuer { debug!("[ShardRunner {:?}] Stopping", runner.shard.shard_info()); }); - self.runners.lock().await.insert(id, runner_info); + self.runners.lock().await.insert(shard_id, runner_info); Ok(()) } - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] async fn shutdown_runners(&mut self) { let keys = { let runners = self.runners.lock().await; @@ -233,7 +275,7 @@ impl ShardQueuer { /// **Note**: If the receiving end of an mpsc channel - owned by the shard runner - no longer /// exists, then the shard runner will not know it should shut down. This _should never happen_. /// It may already be stopped. - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn shutdown(&mut self, shard_id: ShardId, code: u16) { info!("Shutting down shard {}", shard_id); @@ -250,3 +292,48 @@ impl ShardQueuer { } } } + +/// A queue of [`ShardId`]s that is split up into multiple buckets according to the value of +/// [`max_concurrency`](crate::model::gateway::SessionStartLimit::max_concurrency). +#[must_use] +pub struct ShardQueue { + buckets: FixedArray, u16>, +} + +impl ShardQueue { + pub fn new(max_concurrency: NonZeroU16) -> Self { + let buckets = vec![VecDeque::new(); max_concurrency.get() as usize].into_boxed_slice(); + let buckets = FixedArray::try_from(buckets).expect("should fit without truncation"); + + Self { + buckets, + } + } + + fn calculate_bucket(&self, shard_id: ShardId) -> u16 { + shard_id.0 % self.buckets.len() + } + + /// Calculates the corresponding bucket for the given `ShardId` and **appends** to it. + pub fn push_back(&mut self, shard_id: ShardId) { + let bucket = self.calculate_bucket(shard_id); + self.buckets[bucket].push_back(shard_id); + } + + /// Calculates the corresponding bucket for the given `ShardId` and **prepends** to it. + pub fn push_front(&mut self, shard_id: ShardId) { + let bucket = self.calculate_bucket(shard_id); + self.buckets[bucket].push_front(shard_id); + } + + /// Pops a `ShardId` from every bucket containing at least one and returns them all as a `Vec`. + pub fn pop_batch(&mut self) -> Vec { + self.buckets.iter_mut().filter_map(VecDeque::pop_front).collect() + } + + /// Returns `true` if every bucket contains at least one `ShardId`. + #[must_use] + pub fn buckets_filled(&self) -> bool { + self.buckets.iter().all(|b| !b.is_empty()) + } +} diff --git a/src/gateway/bridge/shard_runner.rs b/src/gateway/bridge/shard_runner.rs index 91420b1447c..f384a4e1ecb 100644 --- a/src/gateway/bridge/shard_runner.rs +++ b/src/gateway/bridge/shard_runner.rs @@ -2,12 +2,10 @@ use std::borrow::Cow; use std::sync::Arc; use futures::channel::mpsc::{self, UnboundedReceiver as Receiver, UnboundedSender as Sender}; -use tokio::sync::RwLock; use tokio_tungstenite::tungstenite; use tokio_tungstenite::tungstenite::error::Error as TungsteniteError; use tokio_tungstenite::tungstenite::protocol::frame::CloseFrame; -use tracing::{debug, error, info, instrument, trace, warn}; -use typemap_rev::TypeMap; +use tracing::{debug, error, info, trace, warn}; use super::event::ShardStageUpdateEvent; #[cfg(feature = "collector")] @@ -18,7 +16,7 @@ use super::{ShardId, ShardManager, ShardRunnerMessage}; #[cfg(feature = "cache")] use crate::cache::Cache; use crate::client::dispatch::dispatch_model; -use crate::client::{Context, EventHandler, RawEventHandler}; +use crate::client::{Context, InternalEventHandler}; #[cfg(feature = "framework")] use crate::framework::Framework; use crate::gateway::{GatewayError, ReconnectType, Shard, ShardAction}; @@ -29,9 +27,8 @@ use crate::model::event::{Event, GatewayEvent}; /// A runner for managing a [`Shard`] and its respective WebSocket client. pub struct ShardRunner { - data: Arc>, - event_handlers: Vec>, - raw_event_handlers: Vec>, + data: Arc, + event_handler: Option, #[cfg(feature = "framework")] framework: Option>, manager: Arc, @@ -46,7 +43,7 @@ pub struct ShardRunner { pub cache: Arc, pub http: Arc, #[cfg(feature = "collector")] - pub(crate) collectors: Arc>>, + pub(crate) collectors: Arc>>, } impl ShardRunner { @@ -58,8 +55,7 @@ impl ShardRunner { runner_rx: rx, runner_tx: tx, data: opt.data, - event_handlers: opt.event_handlers, - raw_event_handlers: opt.raw_event_handlers, + event_handler: opt.event_handler, #[cfg(feature = "framework")] framework: opt.framework, manager: opt.manager, @@ -70,7 +66,7 @@ impl ShardRunner { cache: opt.cache, http: opt.http, #[cfg(feature = "collector")] - collectors: Arc::new(std::sync::Mutex::new(vec![])), + collectors: Arc::new(parking_lot::RwLock::new(vec![])), } } @@ -93,14 +89,17 @@ impl ShardRunner { /// /// 6. Go back to 1. /// + /// # Errors + /// Returns errors if the internal WS connection drops in a non-recoverable way. + /// /// [`ShardManager`]: super::ShardManager - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn run(&mut self) -> Result<()> { info!("[ShardRunner {:?}] Running", self.shard.shard_info()); loop { trace!("[ShardRunner {:?}] loop iteration started.", self.shard.shard_info()); - if !self.recv().await? { + if !self.recv().await { return Ok(()); } @@ -108,7 +107,8 @@ impl ShardRunner { if !self.shard.do_heartbeat().await { warn!("[ShardRunner {:?}] Error heartbeating", self.shard.shard_info(),); - return self.request_restart().await; + self.request_restart().await; + return Ok(()); } let pre = self.shard.stage(); @@ -118,13 +118,15 @@ impl ShardRunner { if post != pre { self.update_manager().await; - for event_handler in self.event_handlers.clone() { + if let Some(InternalEventHandler::Normal(event_handler)) = &self.event_handler { + let event_handler = Arc::clone(event_handler); let context = self.make_context(); let event = ShardStageUpdateEvent { new: post, old: pre, shard_id: self.shard.shard_info().id, }; + spawn_named("dispatch::event_handler::shard_stage_update", async move { event_handler.shard_stage_update(context, event).await; }); @@ -133,7 +135,8 @@ impl ShardRunner { match action { Some(ShardAction::Reconnect(ReconnectType::Reidentify)) => { - return self.request_restart().await; + self.request_restart().await; + return Ok(()); }, Some(other) => { if let Err(e) = self.action(&other).await { @@ -144,7 +147,10 @@ impl ShardRunner { e ); match self.shard.reconnection_type() { - ReconnectType::Reidentify => return self.request_restart().await, + ReconnectType::Reidentify => { + self.request_restart().await; + return Ok(()); + }, ReconnectType::Resume => { if let Err(why) = self.shard.resume().await { warn!( @@ -153,7 +159,8 @@ impl ShardRunner { why ); - return self.request_restart().await; + self.request_restart().await; + return Ok(()); } }, }; @@ -163,21 +170,50 @@ impl ShardRunner { } if let Some(event) = event { - #[cfg(feature = "collector")] - self.collectors.lock().expect("poison").retain_mut(|callback| (callback.0)(&event)); - - dispatch_model( - event, - &self.make_context(), - #[cfg(feature = "framework")] - self.framework.clone(), - self.event_handlers.clone(), - self.raw_event_handlers.clone(), - ); + let context = self.make_context(); + let can_dispatch = match &self.event_handler { + Some(InternalEventHandler::Normal(handler)) => { + handler.filter_event(&context, &event) + }, + Some(InternalEventHandler::Raw(handler)) => { + handler.filter_event(&context, &event) + }, + None => true, + }; + + if can_dispatch { + #[cfg(feature = "collector")] + { + let read_lock = self.collectors.read(); + // search all collectors to be removed and clone the Arcs + let to_remove: Vec<_> = read_lock + .iter() + .filter(|callback| !callback.0(&event)) + .cloned() + .collect(); + drop(read_lock); + // remove all found arcs from the collection + // this compares the inner pointer of the Arc + if !to_remove.is_empty() { + self.collectors.write().retain(|f| !to_remove.contains(f)); + } + } + spawn_named( + "shard_runner::dispatch", + dispatch_model( + event, + context, + #[cfg(feature = "framework")] + self.framework.clone(), + self.event_handler.clone(), + ), + ); + } } if !successful && !self.shard.stage().is_connecting() { - return self.request_restart().await; + self.request_restart().await; + return Ok(()); } trace!("[ShardRunner {:?}] loop iteration reached the end.", self.shard.shard_info()); } @@ -196,10 +232,13 @@ impl ShardRunner { /// # Errors /// /// Returns - #[instrument(skip(self, action))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self, action)))] async fn action(&mut self, action: &ShardAction) -> Result<()> { match *action { - ShardAction::Reconnect(ReconnectType::Reidentify) => self.request_restart().await, + ShardAction::Reconnect(ReconnectType::Reidentify) => { + self.request_restart().await; + Ok(()) + }, ShardAction::Reconnect(ReconnectType::Resume) => self.shard.resume().await, ShardAction::Heartbeat => self.shard.heartbeat().await, ShardAction::Identify => self.shard.identify().await, @@ -212,7 +251,7 @@ impl ShardRunner { // Returns whether the WebSocket client is still active. // // If true, the WebSocket client was _not_ shutdown. If false, it was. - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] async fn checked_shutdown(&mut self, id: ShardId, close_code: u16) -> bool { // First verify the ID so we know for certain this runner is to shutdown. if id != self.shard.shard_info().id { @@ -269,7 +308,7 @@ impl ShardRunner { // // This always returns true, except in the case that the shard manager asked the runner to // shutdown. - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] async fn handle_rx_value(&mut self, msg: ShardRunnerMessage) -> bool { match msg { ShardRunnerMessage::Restart(id) => self.checked_shutdown(id, 4000).await, @@ -310,7 +349,7 @@ impl ShardRunner { } #[cfg(feature = "voice")] - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] async fn handle_voice_event(&self, event: &Event) { if let Some(voice_manager) = &self.voice_manager { match event { @@ -320,9 +359,9 @@ impl ShardRunner { .await; }, Event::VoiceServerUpdate(event) => { - if let Some(guild_id) = event.guild_id { - voice_manager.server_update(guild_id, &event.endpoint, &event.token).await; - } + voice_manager + .server_update(event.guild_id, event.endpoint.as_deref(), &event.token) + .await; }, Event::VoiceStateUpdate(event) => { if let Some(guild_id) = event.voice_state.guild_id { @@ -341,13 +380,13 @@ impl ShardRunner { // Requests a restart if the sending half of the channel disconnects. This should _never_ // happen, as the sending half is kept on the runner. // Returns whether the shard runner is in a state that can continue. - #[instrument(skip(self))] - async fn recv(&mut self) -> Result { + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + async fn recv(&mut self) -> bool { loop { match self.runner_rx.try_next() { Ok(Some(value)) => { if !self.handle_rx_value(value).await { - return Ok(false); + return false; } }, Ok(None) => { @@ -356,25 +395,27 @@ impl ShardRunner { self.shard.shard_info(), ); - drop(self.request_restart().await); - return Ok(false); + self.request_restart().await; + return false; }, Err(_) => break, } } // There are no longer any values available. - - Ok(true) + true } /// Returns a received event, as well as whether reading the potentially present event was /// successful. - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] async fn recv_event(&mut self) -> Result<(Option, Option, bool)> { - let gw_event = match self.shard.client.recv_json().await { - Ok(inner) => Ok(inner), - Err(Error::Tungstenite(TungsteniteError::Io(_))) => { + let gateway_event = match self.shard.client.recv_json().await { + Ok(Some(inner)) => Ok(inner), + Ok(None) => { + return Ok((None, None, true)); + }, + Err(Error::Tungstenite(tung_err)) if matches!(*tung_err, TungsteniteError::Io(_)) => { debug!("Attempting to auto-reconnect"); match self.shard.reconnection_type() { @@ -396,54 +437,42 @@ impl ShardRunner { Err(why) => Err(why), }; - let event = match gw_event { - Ok(Some(event)) => Ok(event), - Ok(None) => return Ok((None, None, true)), - Err(why) => Err(why), - }; - - let action = match self.shard.handle_event(&event) { - Ok(Some(action)) => Some(action), - Ok(None) => None, + let is_ack = matches!(gateway_event, Ok(GatewayEvent::HeartbeatAck)); + let (action, event) = match self.shard.handle_event(gateway_event) { + Ok((action, event)) => (action, event), + Err(Error::Gateway( + why @ (GatewayError::InvalidAuthentication + | GatewayError::InvalidGatewayIntents + | GatewayError::DisallowedGatewayIntents), + )) => { + error!("Shard handler received fatal err: {why:?}"); + + self.manager.return_with_value(Err(why.clone())).await; + return Err(Error::Gateway(why)); + }, + Err(Error::Json(_)) => return Ok((None, None, true)), Err(why) => { - error!("Shard handler received err: {:?}", why); - - match &why { - Error::Gateway( - error @ (GatewayError::InvalidAuthentication - | GatewayError::InvalidGatewayIntents - | GatewayError::DisallowedGatewayIntents), - ) => { - self.manager.return_with_value(Err(error.clone())).await; - - return Err(why); - }, - _ => return Ok((None, None, true)), - } + error!("Shard handler recieved err: {why:?}"); + return Ok((None, None, true)); }, }; - if let Ok(GatewayEvent::HeartbeatAck) = event { + if is_ack { self.update_manager().await; } #[cfg(feature = "voice")] { - if let Ok(GatewayEvent::Dispatch(_, ref event)) = event { + if let Some(event) = &event { self.handle_voice_event(event).await; } } - let event = match event { - Ok(GatewayEvent::Dispatch(_, event)) => Some(event), - _ => None, - }; - Ok((event, action, true)) } - #[instrument(skip(self))] - async fn request_restart(&mut self) -> Result<()> { + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + async fn request_restart(&mut self) { debug!("[ShardRunner {:?}] Requesting restart", self.shard.shard_info()); self.update_manager().await; @@ -455,11 +484,9 @@ impl ShardRunner { if let Some(voice_manager) = &self.voice_manager { voice_manager.deregister_shard(shard_id.0).await; } - - Ok(()) } - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] async fn update_manager(&self) { self.manager .update_shard_latency_and_stage( @@ -473,9 +500,8 @@ impl ShardRunner { /// Options to be passed to [`ShardRunner::new`]. pub struct ShardRunnerOptions { - pub data: Arc>, - pub event_handlers: Vec>, - pub raw_event_handlers: Vec>, + pub data: Arc, + pub event_handler: Option, #[cfg(feature = "framework")] pub framework: Option>, pub manager: Arc, diff --git a/src/gateway/bridge/voice.rs b/src/gateway/bridge/voice.rs index 7f03113070f..8f3bc6ce7c2 100644 --- a/src/gateway/bridge/voice.rs +++ b/src/gateway/bridge/voice.rs @@ -1,3 +1,5 @@ +use std::num::NonZeroU16; + use async_trait::async_trait; use futures::channel::mpsc::UnboundedSender as Sender; @@ -14,7 +16,7 @@ pub trait VoiceGatewayManager: Send + Sync { /// Performs initial setup at the start of a connection to Discord. /// /// This will only occur once, and provides the bot's ID and shard count. - async fn initialise(&self, shard_count: u32, user_id: UserId); + async fn initialise(&self, shard_count: NonZeroU16, user_id: UserId); /// Handler fired in response to a [`Ready`] event. /// @@ -22,19 +24,19 @@ pub trait VoiceGatewayManager: Send + Sync { /// active shard. /// /// [`Ready`]: crate::model::event::Event - async fn register_shard(&self, shard_id: u32, sender: Sender); + async fn register_shard(&self, shard_id: u16, sender: Sender); /// Handler fired in response to a disconnect, reconnection, or rebalance. /// /// This event invalidates the last sender associated with `shard_id`. Unless the bot is fully /// disconnecting, this is often followed by a call to [`Self::register_shard`]. Users may wish /// to buffer manually any gateway messages sent between these calls. - async fn deregister_shard(&self, shard_id: u32); + async fn deregister_shard(&self, shard_id: u16); /// Handler for VOICE_SERVER_UPDATE messages. /// /// These contain the endpoint and token needed to form a voice connection session. - async fn server_update(&self, guild_id: GuildId, endpoint: &Option, token: &str); + async fn server_update(&self, guild_id: GuildId, endpoint: Option<&str>, token: &str); /// Handler for VOICE_STATE_UPDATE messages. /// diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 3b31c752877..792292b7709 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -54,7 +54,6 @@ pub use self::bridge::*; pub use self::error::Error as GatewayError; pub use self::shard::Shard; pub use self::ws::WsClient; -#[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::gateway::{Activity, ActivityType}; use crate::model::id::UserId; @@ -73,12 +72,12 @@ pub struct PresenceData { #[derive(Clone, Debug, Serialize)] pub struct ActivityData { /// The name of the activity - pub name: String, + pub name: FixedString, /// The type of the activity #[serde(rename = "type")] pub kind: ActivityType, /// The state of the activity, if the type is [`ActivityType::Custom`] - pub state: Option, + pub state: Option, /// The url of the activity, if the type is [`ActivityType::Streaming`] pub url: Option, } @@ -88,7 +87,7 @@ impl ActivityData { #[must_use] pub fn playing(name: impl Into) -> Self { Self { - name: name.into(), + name: name.into().trunc_into(), kind: ActivityType::Playing, state: None, url: None, @@ -103,7 +102,7 @@ impl ActivityData { #[cfg(feature = "http")] pub fn streaming(name: impl Into, url: impl IntoUrl) -> Result { Ok(Self { - name: name.into(), + name: name.into().trunc_into(), kind: ActivityType::Streaming, state: None, url: Some(url.into_url()?), @@ -114,7 +113,7 @@ impl ActivityData { #[must_use] pub fn listening(name: impl Into) -> Self { Self { - name: name.into(), + name: name.into().trunc_into(), kind: ActivityType::Listening, state: None, url: None, @@ -125,7 +124,7 @@ impl ActivityData { #[must_use] pub fn watching(name: impl Into) -> Self { Self { - name: name.into(), + name: name.into().trunc_into(), kind: ActivityType::Watching, state: None, url: None, @@ -136,7 +135,7 @@ impl ActivityData { #[must_use] pub fn competing(name: impl Into) -> Self { Self { - name: name.into(), + name: name.into().trunc_into(), kind: ActivityType::Competing, state: None, url: None, @@ -149,9 +148,9 @@ impl ActivityData { Self { // discord seems to require a name for custom activities // even though it's not displayed - name: "~".to_string(), + name: FixedString::from_static_trunc("~"), kind: ActivityType::Custom, - state: Some(state.into()), + state: Some(state.into().trunc_into()), url: None, } } diff --git a/src/gateway/shard.rs b/src/gateway/shard.rs index a60a85fd7a5..5f9fb0814ab 100644 --- a/src/gateway/shard.rs +++ b/src/gateway/shard.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use std::time::{Duration as StdDuration, Instant}; -use tokio::sync::Mutex; +use secrecy::{ExposeSecret as _, Secret}; use tokio_tungstenite::tungstenite::error::Error as TungsteniteError; use tokio_tungstenite::tungstenite::protocol::frame::CloseFrame; -use tracing::{debug, error, info, instrument, trace, warn}; +use tracing::{debug, error, info, trace, warn}; use url::Url; use super::{ @@ -18,6 +18,7 @@ use super::{ WsClient, }; use crate::constants::{self, close_codes}; +use crate::http::Token; use crate::internal::prelude::*; use crate::model::event::{Event, GatewayEvent}; use crate::model::gateway::{GatewayIntents, ShardInfo}; @@ -65,15 +66,16 @@ pub struct Shard { // This must be set to `true` in `Shard::handle_event`'s `Ok(GatewayEvent::HeartbeatAck)` arm. last_heartbeat_acknowledged: bool, seq: u64, - session_id: Option, + session_id: Option, shard_info: ShardInfo, stage: ConnectionStage, /// Instant of when the shard was started. // This acts as a timeout to determine if the shard has - for some reason - not started within // a decent amount of time. pub started: Instant, - pub token: String, - ws_url: Arc>, + token: Secret, + ws_url: Arc, + resume_ws_url: Option, pub intents: GatewayIntents, } @@ -87,6 +89,7 @@ impl Shard { /// Instantiating a new Shard manually for a bot with no shards, and then listening for events: /// /// ```rust,no_run + /// use std::num::NonZeroU16; /// use std::sync::Arc; /// /// use serenity::gateway::Shard; @@ -98,15 +101,15 @@ impl Shard { /// # /// # async fn run() -> Result<(), Box> { /// # let http: Arc = unimplemented!(); - /// let token = std::env::var("DISCORD_BOT_TOKEN")?; + /// let token = Arc::from(std::env::var("DISCORD_BOT_TOKEN")?); /// let shard_info = ShardInfo { /// id: ShardId(0), - /// total: 1, + /// total: NonZeroU16::MIN, /// }; /// /// // retrieve the gateway response, which contains the URL to connect to - /// let gateway = Arc::new(Mutex::new(http.get_gateway().await?.url)); - /// let shard = Shard::new(gateway, &token, shard_info, GatewayIntents::all(), None).await?; + /// let gateway = Arc::from(http.get_gateway().await?.url); + /// let shard = Shard::new(gateway, token, shard_info, GatewayIntents::all(), None).await?; /// /// // at this point, you can create a `loop`, and receive events and match /// // their variants @@ -119,14 +122,13 @@ impl Shard { /// On Error, will return either [`Error::Gateway`], [`Error::Tungstenite`] or a Rustls/native /// TLS error. pub async fn new( - ws_url: Arc>, - token: &str, + ws_url: Arc, + token: Arc, shard_info: ShardInfo, intents: GatewayIntents, presence: Option, ) -> Result { - let url = ws_url.lock().await.clone(); - let client = connect(&url).await?; + let client = connect(&ws_url).await?; let presence = presence.unwrap_or_default(); let last_heartbeat_sent = None; @@ -148,10 +150,11 @@ impl Shard { seq, stage, started: Instant::now(), - token: token.to_string(), + token: Token::new(token), session_id, shard_info, ws_url, + resume_ws_url: None, intents, }) } @@ -167,19 +170,16 @@ impl Shard { } /// Retrieves the current presence of the shard. - #[inline] pub fn presence(&self) -> &PresenceData { &self.presence } /// Retrieves the value of when the last heartbeat was sent. - #[inline] pub fn last_heartbeat_sent(&self) -> Option { self.last_heartbeat_sent } /// Retrieves the value of when the last heartbeat ack was received. - #[inline] pub fn last_heartbeat_ack(&self) -> Option { self.last_heartbeat_ack } @@ -192,7 +192,7 @@ impl Shard { /// # Errors /// /// Returns [`GatewayError::HeartbeatFailed`] if there was an error sending a heartbeat. - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn heartbeat(&mut self) -> Result<()> { match self.client.send_heartbeat(&self.shard_info, Some(self.seq)).await { Ok(()) => { @@ -202,58 +202,50 @@ impl Shard { Ok(()) }, Err(why) => { - match why { - Error::Tungstenite(TungsteniteError::Io(err)) => { + if let Error::Tungstenite(err) = &why { + if let TungsteniteError::Io(err) = &**err { if err.raw_os_error() != Some(32) { debug!("[{:?}] Err heartbeating: {:?}", self.shard_info, err); + return Err(Error::Gateway(GatewayError::HeartbeatFailed)); } - }, - other => { - warn!("[{:?}] Other err w/ keepalive: {:?}", self.shard_info, other); - }, + } } + warn!("[{:?}] Other err w/ keepalive: {:?}", self.shard_info, why); Err(Error::Gateway(GatewayError::HeartbeatFailed)) }, } } /// Returns the heartbeat interval dictated by Discord, if the Hello packet has been received. - #[inline] pub fn heartbeat_interval(&self) -> Option { self.heartbeat_interval } - #[inline] pub fn last_heartbeat_acknowledged(&self) -> bool { self.last_heartbeat_acknowledged } - #[inline] pub fn seq(&self) -> u64 { self.seq } - #[inline] - pub fn session_id(&self) -> Option<&String> { - self.session_id.as_ref() + pub fn session_id(&self) -> Option<&str> { + self.session_id.as_deref() } - #[inline] - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub fn set_activity(&mut self, activity: Option) { self.presence.activity = activity; } - #[inline] - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub fn set_presence(&mut self, activity: Option, status: OnlineStatus) { self.set_activity(activity); self.set_status(status); } - #[inline] - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub fn set_status(&mut self, mut status: OnlineStatus) { if status == OnlineStatus::Offline { status = OnlineStatus::Invisible; @@ -275,16 +267,25 @@ impl Shard { self.stage } - #[instrument(skip(self))] - fn handle_gateway_dispatch(&mut self, seq: u64, event: &Event) -> Option { + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + fn handle_gateway_dispatch( + &mut self, + seq: u64, + event: JsonMap, + original_str: &str, + ) -> Result<(Option, Option)> { if seq > self.seq + 1 { warn!("[{:?}] Sequence off; them: {}, us: {}", self.shard_info, seq, self.seq); } + self.seq = seq; + let event = Event::deserialize_and_log(event, original_str)?; + match &event { Event::Ready(ready) => { debug!("[{:?}] Received Ready", self.shard_info); + self.resume_ws_url = Some(ready.ready.resume_gateway_url.clone()); self.session_id = Some(ready.ready.session_id.clone()); self.stage = ConnectionStage::Connected; @@ -303,12 +304,10 @@ impl Shard { _ => {}, } - self.seq = seq; - - None + Ok((None, Some(event))) } - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] fn handle_heartbeat_event(&mut self, s: u64) -> ShardAction { info!("[{:?}] Received shard heartbeat", self.shard_info); @@ -332,7 +331,7 @@ impl Shard { ShardAction::Heartbeat } - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] fn handle_gateway_closed( &mut self, data: Option<&CloseFrame<'static>>, @@ -437,11 +436,20 @@ impl Shard { /// /// Returns a [`GatewayError::OverloadedShard`] if the shard would have too many guilds /// assigned to it. - #[instrument(skip(self))] - pub fn handle_event(&mut self, event: &Result) -> Result> { - match event { - Ok(GatewayEvent::Dispatch(seq, event)) => Ok(self.handle_gateway_dispatch(*seq, event)), - Ok(GatewayEvent::Heartbeat(s)) => Ok(Some(self.handle_heartbeat_event(*s))), + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + pub fn handle_event( + &mut self, + event: Result, + ) -> Result<(Option, Option)> { + let action = match event { + Ok(GatewayEvent::Dispatch { + seq, + data, + original_str, + }) => { + return self.handle_gateway_dispatch(seq, data, &original_str); + }, + Ok(GatewayEvent::Heartbeat(s)) => Ok(Some(self.handle_heartbeat_event(s))), Ok(GatewayEvent::HeartbeatAck) => { self.last_heartbeat_ack = Some(Instant::now()); self.last_heartbeat_acknowledged = true; @@ -450,11 +458,11 @@ impl Shard { Ok(None) }, - &Ok(GatewayEvent::Hello(interval)) => { + Ok(GatewayEvent::Hello(interval)) => { debug!("[{:?}] Received a Hello; interval: {}", self.shard_info, interval); if self.stage == ConnectionStage::Resuming { - return Ok(None); + return Ok((None, None)); } self.heartbeat_interval = Some(std::time::Duration::from_millis(interval)); @@ -467,7 +475,7 @@ impl Shard { ShardAction::Reconnect(self.reconnection_type()) })) }, - &Ok(GatewayEvent::InvalidateSession(resumable)) => { + Ok(GatewayEvent::InvalidateSession(resumable)) => { info!("[{:?}] Received session invalidation", self.shard_info); Ok(Some(if resumable { @@ -488,10 +496,11 @@ impl Shard { }, Err(why) => { warn!("[{:?}] Unhandled error: {:?}", self.shard_info, why); - Ok(None) }, - } + }; + + action.map(|a| (a, None)) } /// Does a heartbeat if needed. Returns false if something went wrong and the shard should be @@ -505,7 +514,7 @@ impl Shard { /// `false` is returned under one of the following conditions: /// - a heartbeat acknowledgement was not received in time /// - an error occurred while heartbeating - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn do_heartbeat(&mut self) -> bool { let Some(heartbeat_interval) = self.heartbeat_interval else { // No Hello received yet @@ -542,7 +551,7 @@ impl Shard { /// Calculates the heartbeat latency between the shard and the gateway. // Shamelessly stolen from brayzure's commit in eris: // - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub fn latency(&self) -> Option { if let (Some(sent), Some(received)) = (self.last_heartbeat_sent, self.last_heartbeat_ack) { if received > sent { @@ -595,24 +604,19 @@ impl Shard { /// specifying a query parameter: /// /// ```rust,no_run - /// # use tokio::sync::Mutex; /// # use serenity::gateway::{ChunkGuildFilter, Shard}; - /// # use serenity::model::gateway::{GatewayIntents, ShardInfo}; - /// # use serenity::model::id::ShardId; - /// # use std::sync::Arc; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let mutex = Arc::new(Mutex::new("".to_string())); - /// # let shard_info = ShardInfo { - /// # id: ShardId(0), - /// # total: 1, - /// # }; - /// # - /// # let mut shard = Shard::new(mutex.clone(), "", shard_info, GatewayIntents::all(), None).await?; - /// # + /// # async fn run(mut shard: Shard) -> Result<(), Box> { /// use serenity::model::id::GuildId; /// - /// shard.chunk_guild(GuildId::new(81384788765712384), Some(2000), false, ChunkGuildFilter::None, None).await?; + /// shard + /// .chunk_guild( + /// GuildId::new(81384788765712384), + /// Some(2000), + /// false, + /// ChunkGuildFilter::None, + /// None, + /// ) + /// .await?; /// # Ok(()) /// # } /// ``` @@ -621,22 +625,8 @@ impl Shard { /// `"do"` and a nonce of `"request"`: /// /// ```rust,no_run - /// # use tokio::sync::Mutex; - /// # use serenity::model::gateway::{GatewayIntents, ShardInfo}; /// # use serenity::gateway::{ChunkGuildFilter, Shard}; - /// # use serenity::model::id::ShardId; - /// # use std::error::Error; - /// # use std::sync::Arc; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let mutex = Arc::new(Mutex::new("".to_string())); - /// # - /// # let shard_info = ShardInfo { - /// # id: ShardId(0), - /// # total: 1, - /// # }; - /// # let mut shard = Shard::new(mutex.clone(), "", shard_info, GatewayIntents::all(), None).await?; - /// # + /// # async fn run(mut shard: Shard) -> Result<(), Box> { /// use serenity::model::id::GuildId; /// /// shard @@ -652,10 +642,13 @@ impl Shard { /// # } /// ``` /// + /// # Errors + /// Errors if there is a problem with the WS connection. + /// /// [`Event::GuildMembersChunk`]: crate::model::event::Event::GuildMembersChunk /// [`Guild`]: crate::model::guild::Guild /// [`Member`]: crate::model::guild::Member - #[instrument(skip(self))] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn chunk_guild( &mut self, guild_id: GuildId, @@ -674,10 +667,18 @@ impl Shard { /// Sets the shard as going into identifying stage, which sets: /// - the time that the last heartbeat sent as being now /// - the `stage` to [`ConnectionStage::Identifying`] - #[instrument(skip(self))] + /// + /// # Errors + /// Errors if there is a problem with the WS connection. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn identify(&mut self) -> Result<()> { self.client - .send_identify(&self.shard_info, &self.token, self.intents, &self.presence) + .send_identify( + &self.shard_info, + self.token.expose_secret(), + self.intents, + &self.presence, + ) .await?; self.last_heartbeat_sent = Some(Instant::now()); @@ -686,13 +687,20 @@ impl Shard { Ok(()) } - /// Initializes a new WebSocket client. + /// Reinitializes an existing WebSocket client, replacing it. /// /// This will set the stage of the shard before and after instantiation of the client. - #[instrument(skip(self))] - pub async fn initialize(&mut self) -> Result { + /// + /// # Errors + /// + /// Errors if unable to establish a websocket connection. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + pub async fn reinitialize(&mut self) -> Result { debug!("[{:?}] Initializing.", self.shard_info); + // Reconnect to the resume URL if possible, otherwise use the generic URL. + let ws_url = self.resume_ws_url.as_deref().unwrap_or(&self.ws_url); + // We need to do two, sort of three things here: // - set the stage of the shard as opening the websocket connection // - open the websocket connection @@ -702,15 +710,14 @@ impl Shard { // Hello is received. self.stage = ConnectionStage::Connecting; self.started = Instant::now(); - let url = &self.ws_url.lock().await.clone(); - let client = connect(url).await?; + let client = connect(ws_url).await?; self.stage = ConnectionStage::Handshake; Ok(client) } - #[instrument(skip(self))] - pub async fn reset(&mut self) { + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + pub fn reset(&mut self) { self.last_heartbeat_sent = Some(Instant::now()); self.last_heartbeat_ack = None; self.heartbeat_interval = None; @@ -720,32 +727,43 @@ impl Shard { self.seq = 0; } - #[instrument(skip(self))] + /// # Errors + /// + /// Errors if unable to re-establish a websocket connection. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn resume(&mut self) -> Result<()> { debug!("[{:?}] Attempting to resume", self.shard_info); - self.client = self.initialize().await?; + self.client = self.reinitialize().await?; self.stage = ConnectionStage::Resuming; match &self.session_id { Some(session_id) => { - self.client.send_resume(&self.shard_info, session_id, self.seq, &self.token).await + self.client + .send_resume(&self.shard_info, session_id, self.seq, self.token.expose_secret()) + .await }, None => Err(Error::Gateway(GatewayError::NoSessionId)), } } - #[instrument(skip(self))] + /// # Errors + /// + /// Errors if unable to re-establish a websocket connection. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn reconnect(&mut self) -> Result<()> { info!("[{:?}] Attempting to reconnect", self.shard_info()); - self.reset().await; - self.client = self.initialize().await?; + self.reset(); + self.client = self.reinitialize().await?; Ok(()) } - #[instrument(skip(self))] + /// # Errors + /// + /// Errors if there is a problem with the WS connection. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn update_presence(&mut self) -> Result<()> { self.client.send_presence_update(&self.shard_info, &self.presence).await } diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index 3f378dd367a..d479d8b6962 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -8,6 +8,8 @@ use flate2::read::ZlibDecoder; use futures::SinkExt; #[cfg(feature = "client")] use futures::StreamExt; +#[cfg(feature = "client")] +use small_fixed_array::FixedString; use tokio::net::TcpStream; #[cfg(feature = "client")] use tokio::time::{timeout, Duration}; @@ -20,7 +22,7 @@ use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::{connect_async_with_config, MaybeTlsStream, WebSocketStream}; #[cfg(feature = "client")] use tracing::warn; -use tracing::{debug, instrument, trace}; +use tracing::{debug, trace}; use url::Url; use super::{ActivityData, ChunkGuildFilter, PresenceData}; @@ -28,9 +30,6 @@ use crate::constants::{self, Opcode}; #[cfg(feature = "client")] use crate::gateway::GatewayError; #[cfg(feature = "client")] -use crate::json::from_str; -use crate::json::to_string; -#[cfg(feature = "client")] use crate::model::event::GatewayEvent; use crate::model::gateway::{GatewayIntents, ShardInfo}; use crate::model::id::{GuildId, UserId}; @@ -62,7 +61,7 @@ struct PresenceUpdateMessage<'a> { afk: bool, status: &'a str, since: SystemTime, - activities: &'a [&'a ActivityData], + activities: &'a [ActivityData], } #[derive(Serialize)] @@ -120,7 +119,8 @@ impl WsClient { Ok(None) | Err(_) => return Ok(None), }; - let value = match message { + let json_str = match message { + Message::Text(payload) => payload, Message::Binary(bytes) => { let mut decompressed = String::with_capacity(bytes.len() * DECOMPRESSION_MULTIPLIER); @@ -132,29 +132,34 @@ impl WsClient { why })?; - from_str(&decompressed).map_err(|why| { - warn!("Err deserializing bytes: {why:?}"); - debug!("Failing bytes: {bytes:?}"); - - why - })? + decompressed }, - Message::Text(payload) => from_str(&payload).map_err(|why| { - warn!("Err deserializing text: {why:?}; text: {payload}"); - - why - })?, Message::Close(Some(frame)) => { return Err(Error::Gateway(GatewayError::Closed(Some(frame)))); }, _ => return Ok(None), }; - Ok(Some(value)) + match serde_json::from_str(&json_str) { + Ok(mut event) => { + if let GatewayEvent::Dispatch { + original_str, .. + } = &mut event + { + *original_str = FixedString::from_string_trunc(json_str); + } + + Ok(Some(event)) + }, + Err(err) => { + debug!("Failing text: {json_str}"); + Err(Error::Json(err)) + }, + } } pub(crate) async fn send_json(&mut self, value: &impl serde::Serialize) -> Result<()> { - let message = to_string(value).map(Message::Text)?; + let message = serde_json::to_string(value).map(Message::Text)?; self.0.send(message).await?; Ok(()) @@ -212,7 +217,10 @@ impl WsClient { .await } - #[instrument(skip(self))] + /// # Errors + /// + /// Errors if there is a problem with the WS connection. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn send_heartbeat(&mut self, shard_info: &ShardInfo, seq: Option) -> Result<()> { trace!("[{:?}] Sending heartbeat d: {:?}", shard_info, seq); @@ -223,7 +231,10 @@ impl WsClient { .await } - #[instrument(skip(self, token))] + /// # Errors + /// + /// Errors if there is a problem with the WS connection. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self, token)))] pub async fn send_identify( &mut self, shard: &ShardInfo, @@ -231,8 +242,8 @@ impl WsClient { intents: GatewayIntents, presence: &PresenceData, ) -> Result<()> { - let activities: Vec<_> = presence.activity.iter().collect(); let now = SystemTime::now(); + let activities = presence.activity.as_ref().map(std::slice::from_ref).unwrap_or_default(); debug!("[{:?}] Identifying", shard); @@ -253,7 +264,7 @@ impl WsClient { afk: false, since: now, status: presence.status.name(), - activities: &activities, + activities, }, }, }; @@ -261,30 +272,36 @@ impl WsClient { self.send_json(&msg).await } - #[instrument(skip(self))] + /// # Errors + /// + /// Errors if there is a problem with the WS connection. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] pub async fn send_presence_update( &mut self, shard_info: &ShardInfo, presence: &PresenceData, ) -> Result<()> { - let activities: Vec<_> = presence.activity.iter().collect(); let now = SystemTime::now(); + let activities = presence.activity.as_ref().map(std::slice::from_ref).unwrap_or_default(); - debug!("[{:?}] Sending presence update", shard_info); + debug!("[{shard_info:?}] Sending presence update"); self.send_json(&WebSocketMessage { op: Opcode::PresenceUpdate, d: WebSocketMessageData::PresenceUpdate(PresenceUpdateMessage { afk: false, since: now, + activities, status: presence.status.name(), - activities: &activities, }), }) .await } - #[instrument(skip(self, token))] + /// # Errors + /// + /// Errors if there is a problem with the WS connection. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self, token)))] pub async fn send_resume( &mut self, shard_info: &ShardInfo, diff --git a/src/http/client.rs b/src/http/client.rs index 4157b804fd5..fd758438cc7 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -1,24 +1,26 @@ #![allow(clippy::missing_errors_doc)] use std::borrow::Cow; -use std::num::NonZeroU64; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use arrayvec::ArrayVec; +use nonmax::{NonMaxU16, NonMaxU8}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use reqwest::header::{HeaderMap as Headers, HeaderValue}; #[cfg(feature = "utils")] use reqwest::Url; use reqwest::{Client, ClientBuilder, Response as ReqwestResponse, StatusCode}; -use secrecy::{ExposeSecret, SecretString}; +use secrecy::{ExposeSecret as _, Secret}; use serde::de::DeserializeOwned; -use tracing::{debug, instrument, trace}; +use serde_json::{from_value, json, to_string, to_vec}; +use to_arraystring::ToArrayString as _; +use tracing::{debug, trace}; use super::multipart::{Multipart, MultipartUpload}; use super::ratelimiting::Ratelimiter; use super::request::Request; use super::routing::Route; -use super::typing::Typing; use super::{ ErrorResponse, GuildPagination, @@ -30,9 +32,40 @@ use super::{ use crate::builder::{CreateAllowedMentions, CreateAttachment}; use crate::constants; use crate::internal::prelude::*; -use crate::json::*; use crate::model::prelude::*; +#[derive(Clone)] +pub(crate) struct Token(Arc); + +impl Token { + pub fn new(inner: Arc) -> Secret { + Secret::new(Self(inner)) + } + + pub fn get_inner(&self) -> &Arc { + &self.0 + } +} + +impl std::ops::Deref for Token { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl secrecy::Zeroize for Token { + fn zeroize(&mut self) { + if let Some(string) = Arc::get_mut(&mut self.0) { + string.zeroize(); + } + } +} + +impl secrecy::CloneableSecret for Token {} +impl secrecy::DebugSecret for Token {} + /// A builder for the underlying [`Http`] client that performs requests to Discord's HTTP API. If /// you do not need to use a proxy or do not need to disable the rate limiter, you can use /// [`Http::new`] instead. @@ -53,21 +86,21 @@ pub struct HttpBuilder { client: Option, ratelimiter: Option, ratelimiter_disabled: bool, - token: SecretString, - proxy: Option, + token: Arc, + proxy: Option>, application_id: Option, - default_allowed_mentions: Option, + default_allowed_mentions: Option>, } impl HttpBuilder { /// Construct a new builder to call methods on for the HTTP construction. The `token` will /// automatically be prefixed "Bot " if not already. - pub fn new(token: impl AsRef) -> Self { + pub fn new(token: &str) -> Self { Self { client: None, ratelimiter: None, ratelimiter_disabled: false, - token: SecretString::new(parse_token(token)), + token: parse_token(token), proxy: None, application_id: None, default_allowed_mentions: None, @@ -82,8 +115,8 @@ impl HttpBuilder { /// Sets a token for the bot. If the token is not prefixed "Bot ", this method will /// automatically do so. - pub fn token(mut self, token: impl AsRef) -> Self { - self.token = SecretString::new(parse_token(token)); + pub fn token(mut self, token: &str) -> Self { + self.token = parse_token(token); self } @@ -122,10 +155,22 @@ impl HttpBuilder { /// proxy's behavior where it will tunnel requests that use TLS via [`HTTP CONNECT`] method /// (e.g. using [`reqwest::Proxy`]). /// + /// # Panics + /// + /// Panics if the proxy URL is larger than u16::MAX characters... what are you doing? + /// /// [`twilight-http-proxy`]: https://github.com/twilight-rs/http-proxy /// [`HTTP CONNECT`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT - pub fn proxy(mut self, proxy: impl Into) -> Self { - self.proxy = Some(proxy.into()); + pub fn proxy<'a>(mut self, proxy: impl Into>) -> Self { + let proxy = proxy.into(); + u16::try_from(proxy.len()).expect("Proxy URL should be less than u16::MAX characters"); + + let proxy = match proxy { + Cow::Owned(proxy) => FixedString::from_string_trunc(proxy), + Cow::Borrowed(proxy) => FixedString::from_str_trunc(proxy), + }; + + self.proxy = Some(proxy); self } @@ -133,7 +178,10 @@ impl HttpBuilder { /// /// This only takes effect if you are calling through the model or builder methods, not directly /// calling [`Http`] methods, as [`Http`] is simply used as a convenient storage for these. - pub fn default_allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { + pub fn default_allowed_mentions( + mut self, + allowed_mentions: CreateAllowedMentions<'static>, + ) -> Self { self.default_allowed_mentions = Some(allowed_mentions); self } @@ -141,7 +189,8 @@ impl HttpBuilder { /// Use the given configuration to build the `Http` client. #[must_use] pub fn build(self) -> Http { - let application_id = AtomicU64::new(self.application_id.map_or(0, ApplicationId::get)); + let application_id = + AtomicU64::new(self.application_id.map_or(u64::MAX, ApplicationId::get)); let client = self.client.unwrap_or_else(|| { let builder = configure_client_backend(Client::builder()); @@ -150,27 +199,27 @@ impl HttpBuilder { let ratelimiter = (!self.ratelimiter_disabled).then(|| { self.ratelimiter - .unwrap_or_else(|| Ratelimiter::new(client.clone(), self.token.expose_secret())) + .unwrap_or_else(|| Ratelimiter::new(client.clone(), Arc::clone(&self.token))) }); Http { client, ratelimiter, proxy: self.proxy, - token: self.token, + token: Token::new(self.token), application_id, default_allowed_mentions: self.default_allowed_mentions, } } } -fn parse_token(token: impl AsRef) -> String { - let token = token.as_ref().trim(); +fn parse_token(token: &str) -> Arc { + let token = token.trim(); if token.starts_with("Bot ") || token.starts_with("Bearer ") { - token.to_string() + Arc::from(token) } else { - format!("Bot {token}") + Arc::from(format!("Bot {token}")) } } @@ -195,10 +244,10 @@ fn reason_into_header(reason: &str) -> Headers { pub struct Http { pub(crate) client: Client, pub ratelimiter: Option, - pub proxy: Option, - token: SecretString, + pub proxy: Option>, + token: Secret, application_id: AtomicU64, - pub default_allowed_mentions: Option, + pub default_allowed_mentions: Option>, } impl Http { @@ -209,7 +258,11 @@ impl Http { pub fn application_id(&self) -> Option { let application_id = self.application_id.load(Ordering::Relaxed); - NonZeroU64::new(application_id).map(ApplicationId::from) + if application_id == u64::MAX { + None + } else { + Some(ApplicationId::new(application_id)) + } } fn try_application_id(&self) -> Result { @@ -220,8 +273,8 @@ impl Http { self.application_id.store(application_id.get(), Ordering::Relaxed); } - pub fn token(&self) -> &str { - self.token.expose_secret() + pub(crate) fn token(&self) -> &Arc { + self.token.expose_secret().get_inner() } /// Adds a [`User`] to a [`Guild`] with a valid OAuth2 access token. @@ -252,15 +305,11 @@ impl Http { if response.status() == 204 { Ok(None) } else { - Ok(Some(decode_resp(response).await?)) + Ok(Some(response.json().await?)) } } /// Adds a single [`Role`] to a [`Member`] in a [`Guild`]. - /// - /// **Note**: Requires the [Manage Roles] permission and respect of role hierarchy. - /// - /// [Manage Roles]: Permissions::MANAGE_ROLES pub async fn add_member_role( &self, guild_id: GuildId, @@ -288,10 +337,6 @@ impl Http { /// /// Passing a `delete_message_days` of `0` is equivalent to not removing any messages. Up to /// `7` days' worth of messages may be deleted. - /// - /// **Note**: Requires that you have the [Ban Members] permission. - /// - /// [Ban Members]: Permissions::BAN_MEMBERS pub async fn ban_user( &self, guild_id: GuildId, @@ -310,7 +355,7 @@ impl Http { guild_id, user_id, }, - params: Some(vec![("delete_message_seconds", delete_message_seconds.to_string())]), + params: Some(&[("delete_message_seconds", &delete_message_seconds.to_arraystring())]), }) .await } @@ -338,12 +383,6 @@ impl Http { } /// Broadcasts that the current user is typing in the given [`Channel`]. - /// - /// This lasts for about 10 seconds, and will then need to be renewed to indicate that the - /// current user is still typing. - /// - /// This should rarely be used for bots, although it is a good indicator that a long-running - /// command is still being processed. pub async fn broadcast_typing(&self, channel_id: ChannelId) -> Result<()> { self.wind(204, Request { body: None, @@ -359,13 +398,6 @@ impl Http { } /// Creates a [`GuildChannel`] in the [`Guild`] given its Id. - /// - /// Refer to the Discord's [docs] for information on what fields this requires. - /// - /// **Note**: Requires the [Manage Channels] permission. - /// - /// [docs]: https://discord.com/developers/docs/resources/guild#create-guild-channel - /// [Manage Channels]: Permissions::MANAGE_CHANNELS pub async fn create_channel( &self, guild_id: GuildId, @@ -450,28 +482,18 @@ impl Http { .await } - /// Shortcut for [`Self::create_forum_post_with_attachments`] - pub async fn create_forum_post( - &self, - channel_id: ChannelId, - map: &impl serde::Serialize, - audit_log_reason: Option<&str>, - ) -> Result { - self.create_forum_post_with_attachments(channel_id, map, vec![], audit_log_reason).await - } - /// Creates a forum post channel in the [`GuildChannel`] given its Id. - pub async fn create_forum_post_with_attachments( + pub async fn create_forum_post( &self, channel_id: ChannelId, map: &impl serde::Serialize, - files: Vec, + files: Vec>, audit_log_reason: Option<&str>, ) -> Result { self.fire(Request { body: None, multipart: Some(Multipart { - upload: MultipartUpload::Attachments(files.into_iter().collect()), + upload: MultipartUpload::Attachments(files), payload_json: Some(to_string(map)?), fields: vec![], }), @@ -486,16 +508,10 @@ impl Http { } /// Creates an emoji in the given [`Guild`] with the given data. - /// - /// View the source code for [`Guild::create_emoji`] method to see what fields this requires. - /// - /// **Note**: Requires the [Create Guild Expressions] permission. - /// - /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS pub async fn create_emoji( &self, guild_id: GuildId, - map: &Value, + map: &impl serde::Serialize, audit_log_reason: Option<&str>, ) -> Result { self.fire(Request { @@ -518,7 +534,7 @@ impl Http { &self, interaction_token: &str, map: &impl serde::Serialize, - files: Vec, + files: Vec>, ) -> Result { let mut request = Request { body: None, @@ -546,15 +562,6 @@ impl Http { } /// Creates a new global command. - /// - /// New global commands will be available in all guilds after 1 hour. - /// - /// Refer to Discord's [docs] for field information. - /// - /// **Note**: Creating a command with the same name as an existing command for your application - /// will overwrite the old command. - /// - /// [docs]: https://discord.com/developers/docs/interactions/slash-commands#create-global-application-command pub async fn create_global_command(&self, map: &impl serde::Serialize) -> Result { self.fire(Request { body: Some(to_vec(map)?), @@ -612,35 +619,8 @@ impl Http { /// Only a [`PartialGuild`] will be immediately returned, and a full [`Guild`] will be received /// over a [`Shard`], if at least one is running. /// - /// **Note**: This endpoint is currently limited to 10 active guilds. The limits are raised for - /// whitelisted [GameBridge] applications. See the [documentation on this endpoint] for more - /// info. - /// - /// # Examples - /// - /// Create a guild called `"test"` in the [US West region]: - /// - /// ```rust,no_run - /// use serenity::http::Http; - /// use serenity::json::json; - /// - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let map = json!({ - /// "name": "test", - /// }); - /// - /// let _result = http.create_guild(&map).await?; - /// # Ok(()) - /// # } - /// ``` - /// /// [`Shard`]: crate::gateway::Shard - /// [GameBridge]: https://discord.com/developers/docs/topics/gamebridge - /// [documentation on this endpoint]: - /// https://discord.com/developers/docs/resources/guild#create-guild - /// [whitelist]: https://discord.com/developers/docs/resources/guild#create-guild - pub async fn create_guild(&self, map: &Value) -> Result { + pub async fn create_guild(&self, map: &impl serde::Serialize) -> Result { self.fire(Request { body: Some(to_vec(map)?), multipart: None, @@ -655,10 +635,6 @@ impl Http { /// Creates a new guild command. /// /// New guild commands will be available in the guild immediately. - /// - /// Refer to Discord's [docs] for field information. - /// - /// [docs]: https://discord.com/developers/docs/interactions/slash-commands#create-guild-application-command pub async fn create_guild_command( &self, guild_id: GuildId, @@ -679,18 +655,11 @@ impl Http { } /// Creates an [`Integration`] for a [`Guild`]. - /// - /// Refer to Discord's [docs] for field information. - /// - /// **Note**: Requires the [Manage Guild] permission. - /// - /// [Manage Guild]: Permissions::MANAGE_GUILD - /// [docs]: https://discord.com/developers/docs/resources/guild#create-guild-integration pub async fn create_guild_integration( &self, guild_id: GuildId, integration_id: IntegrationId, - map: &Value, + map: &impl serde::Serialize, audit_log_reason: Option<&str>, ) -> Result<()> { self.wind(204, Request { @@ -708,17 +677,12 @@ impl Http { } /// Creates a response to an [`Interaction`] from the gateway. - /// - /// Refer to Discord's [docs] for the object it takes. - /// - /// [`Interaction`]: crate::model::application::Interaction - /// [docs]: https://discord.com/developers/docs/interactions/slash-commands#interaction-interaction-response pub async fn create_interaction_response( &self, interaction_id: InteractionId, interaction_token: &str, map: &impl serde::Serialize, - files: Vec, + files: Vec>, ) -> Result<()> { let mut request = Request { body: None, @@ -746,15 +710,6 @@ impl Http { } /// Creates a [`RichInvite`] for the given [channel][`GuildChannel`]. - /// - /// Refer to Discord's [docs] for field information. - /// - /// All fields are optional. - /// - /// **Note**: Requires the [Create Instant Invite] permission. - /// - /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE - /// [docs]: https://discord.com/developers/docs/resources/channel#create-channel-invite pub async fn create_invite( &self, channel_id: ChannelId, @@ -801,7 +756,10 @@ impl Http { } /// Creates a private channel with a user. - pub async fn create_private_channel(&self, map: &Value) -> Result { + pub async fn create_private_channel( + &self, + map: &impl serde::Serialize, + ) -> Result { let body = to_vec(map)?; self.fire(Request { @@ -832,7 +790,7 @@ impl Http { message_id, reaction: &reaction_type.as_data(), }, - params: Some(vec![("burst", burst.to_string())]), + params: Some(&[("burst", &burst.to_arraystring())]), }) .await } @@ -885,12 +843,6 @@ impl Http { } /// Creates a Guild Scheduled Event. - /// - /// Refer to Discord's docs for field information. - /// - /// **Note**: Requires the [Create Events] permission. - /// - /// [Create Events]: Permissions::CREATE_EVENTS pub async fn create_scheduled_event( &self, guild_id: GuildId, @@ -912,23 +864,19 @@ impl Http { } /// Creates a sticker. - /// - /// **Note**: Requires the [Create Guild Expressions] permission. - /// - /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS pub async fn create_sticker( &self, guild_id: GuildId, - map: impl IntoIterator, - file: CreateAttachment, + fields: Vec<(Cow<'static, str>, Cow<'static, str>)>, + file: CreateAttachment<'_>, audit_log_reason: Option<&str>, ) -> Result { self.fire(Request { body: None, multipart: Some(Multipart { upload: MultipartUpload::File(file), - fields: map.into_iter().map(|(k, v)| (k.into(), v.into())).collect(), payload_json: None, + fields, }), headers: audit_log_reason.map(reason_into_header), method: LightMethod::Post, @@ -970,32 +918,7 @@ impl Http { .await } - /// Creates a webhook for the given [channel][`GuildChannel`]'s Id, passing in the given data. - /// - /// This method requires authentication. - /// - /// The Value is a map with the values of: - /// - **avatar**: base64-encoded 128x128 image for the webhook's default avatar (_optional_); - /// - **name**: the name of the webhook, limited to between 2 and 100 characters long. - /// - /// # Examples - /// - /// Creating a webhook named `test`: - /// - /// ```rust,no_run - /// use serenity::http::Http; - /// use serenity::json::json; - /// use serenity::model::prelude::*; - /// - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let channel_id = ChannelId::new(81384788765712384); - /// let map = json!({"name": "test"}); - /// - /// let webhook = http.create_webhook(channel_id, &map, None).await?; - /// # Ok(()) - /// # } - /// ``` + /// Creates a webhook for the given [`GuildChannel`]'s Id, passing in the given data. pub async fn create_webhook( &self, channel_id: ChannelId, @@ -1056,8 +979,6 @@ impl Http { } /// Deletes an emoji from a server. - /// - /// See [`GuildId::edit_emoji`] for permissions requirements. pub async fn delete_emoji( &self, guild_id: GuildId, @@ -1216,7 +1137,7 @@ impl Http { pub async fn delete_messages( &self, channel_id: ChannelId, - map: &Value, + map: &impl serde::Serialize, audit_log_reason: Option<&str>, ) -> Result<()> { self.wind(204, Request { @@ -1233,23 +1154,6 @@ impl Http { } /// Deletes all of the [`Reaction`]s associated with a [`Message`]. - /// - /// # Examples - /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # - /// use serenity::model::id::{ChannelId, MessageId}; - /// - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let channel_id = ChannelId::new(7); - /// let message_id = MessageId::new(8); - /// - /// http.delete_message_reactions(channel_id, message_id).await?; - /// # Ok(()) - /// # } - /// ``` pub async fn delete_message_reactions( &self, channel_id: ChannelId, @@ -1399,11 +1303,6 @@ impl Http { } /// Deletes a [Scheduled Event] from a server. - /// - /// **Note**: Requires the [Manage Events] permission. - /// - /// [Scheduled Event]: crate::model::guild::ScheduledEvent - /// [Manage Events]: Permissions::MANAGE_EVENTS pub async fn delete_scheduled_event( &self, guild_id: GuildId, @@ -1424,8 +1323,6 @@ impl Http { } /// Deletes a sticker from a server. - /// - /// See [`GuildId::delete_sticker`] for permissions requirements. pub async fn delete_sticker( &self, guild_id: GuildId, @@ -1464,24 +1361,6 @@ impl Http { } /// Deletes a [`Webhook`] given its Id. - /// - /// This method requires authentication, whereas [`Self::delete_webhook_with_token`] does not. - /// - /// # Examples - /// - /// Deletes a webhook given its Id: - /// - /// ```rust,no_run - /// use serenity::http::Http; - /// use serenity::model::prelude::*; - /// - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let webhook_id = WebhookId::new(245037420704169985); - /// http.delete_webhook(webhook_id, None).await?; - /// Ok(()) - /// # } - /// ``` pub async fn delete_webhook( &self, webhook_id: WebhookId, @@ -1503,24 +1382,6 @@ impl Http { /// Deletes a [`Webhook`] given its Id and unique token. /// /// This method does _not_ require authentication. - /// - /// # Examples - /// - /// Deletes a webhook given its Id and unique token: - /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # use serenity::model::prelude::*; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let id = WebhookId::new(245037420704169985); - /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; - /// - /// http.delete_webhook_with_token(id, token, None).await?; - /// # Ok(()) - /// # } - /// ``` pub async fn delete_webhook_with_token( &self, webhook_id: WebhookId, @@ -1584,13 +1445,11 @@ impl Http { } /// Changes emoji information. - /// - /// See [`GuildId::edit_emoji`] for permissions requirements. pub async fn edit_emoji( &self, guild_id: GuildId, emoji_id: EmojiId, - map: &Value, + map: &impl serde::Serialize, audit_log_reason: Option<&str>, ) -> Result { let body = to_vec(map)?; @@ -1610,16 +1469,12 @@ impl Http { } /// Edits a follow-up message for an interaction. - /// - /// Refer to Discord's [docs] for Edit Webhook Message for field information. - /// - /// [docs]: https://discord.com/developers/docs/resources/webhook#edit-webhook-message pub async fn edit_followup_message( &self, interaction_token: &str, message_id: MessageId, map: &impl serde::Serialize, - new_attachments: Vec, + new_attachments: Vec>, ) -> Result { let mut request = Request { body: None, @@ -1648,10 +1503,6 @@ impl Http { } /// Get a follow-up message for an interaction. - /// - /// Refer to Discord's [docs] for Get Webhook Message for field information. - /// - /// [docs]: https://discord.com/developers/docs/resources/webhook#get-webhook-message pub async fn get_followup_message( &self, interaction_token: &str, @@ -1673,12 +1524,6 @@ impl Http { } /// Edits a global command. - /// - /// Updates will be available in all guilds after 1 hour. - /// - /// Refer to Discord's [docs] for field information. - /// - /// [docs]: https://discord.com/developers/docs/interactions/slash-commands#edit-global-application-command pub async fn edit_global_command( &self, command_id: CommandId, @@ -1721,12 +1566,6 @@ impl Http { } /// Edits a guild command. - /// - /// Updates for guild commands will be available immediately. - /// - /// Refer to Discord's [docs] for field information. - /// - /// [docs]: https://discord.com/developers/docs/interactions/slash-commands#edit-guild-application-command pub async fn edit_guild_command( &self, guild_id: GuildId, @@ -1749,12 +1588,6 @@ impl Http { } /// Edits a guild command permissions. - /// - /// Updates for guild commands will be available immediately. - /// - /// Refer to Discord's [documentation] for field information. - /// - /// [documentation]: https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions pub async fn edit_guild_command_permissions( &self, guild_id: GuildId, @@ -1780,7 +1613,7 @@ impl Http { pub async fn edit_guild_channel_positions( &self, guild_id: GuildId, - value: &Value, + value: &impl serde::Serialize, ) -> Result<()> { let body = to_vec(value)?; @@ -1801,7 +1634,7 @@ impl Http { pub async fn edit_guild_mfa_level( &self, guild_id: GuildId, - value: &Value, + value: &impl serde::Serialize, audit_log_reason: Option<&str>, ) -> Result { #[derive(Deserialize)] @@ -1908,7 +1741,7 @@ impl Http { channel_id: ChannelId, message_id: MessageId, map: &impl serde::Serialize, - new_attachments: Vec, + new_attachments: Vec>, ) -> Result { let mut request = Request { body: None, @@ -1985,14 +1818,11 @@ impl Http { pub async fn edit_nickname( &self, guild_id: GuildId, - new_nickname: Option<&str>, + map: &impl serde::Serialize, audit_log_reason: Option<&str>, ) -> Result<()> { - let map = json!({ "nick": new_nickname }); - let body = to_vec(&map)?; - self.wind(200, Request { - body: Some(body), + body: Some(to_vec(&map)?), multipart: None, headers: audit_log_reason.map(reason_into_header), method: LightMethod::Patch, @@ -2008,13 +1838,10 @@ impl Http { pub async fn follow_news_channel( &self, news_channel_id: ChannelId, - target_channel_id: ChannelId, + map: &impl serde::Serialize, ) -> Result { - let map = json!({ "webhook_channel_id": target_channel_id }); - let body = to_vec(&map)?; - self.fire(Request { - body: Some(body), + body: Some(to_vec(&map)?), multipart: None, headers: None, method: LightMethod::Post, @@ -2046,15 +1873,11 @@ impl Http { } /// Edits the initial interaction response. - /// - /// Refer to Discord's [docs] for Edit Webhook Message for field information. - /// - /// [docs]: https://discord.com/developers/docs/resources/webhook#edit-webhook-message pub async fn edit_original_interaction_response( &self, interaction_token: &str, map: &impl serde::Serialize, - new_attachments: Vec, + new_attachments: Vec>, ) -> Result { let mut request = Request { body: None, @@ -2072,7 +1895,7 @@ impl Http { request.body = Some(to_vec(map)?); } else { request.multipart = Some(Multipart { - upload: MultipartUpload::Attachments(new_attachments.into_iter().collect()), + upload: MultipartUpload::Attachments(new_attachments), payload_json: Some(to_string(map)?), fields: vec![], }); @@ -2129,19 +1952,12 @@ impl Http { pub async fn edit_role_position( &self, guild_id: GuildId, - role_id: RoleId, - position: u16, + map: &impl serde::Serialize, audit_log_reason: Option<&str>, ) -> Result> { - let map = json!([{ - "id": role_id, - "position": position, - }]); - let body = to_vec(&map)?; - let mut value: Value = self .fire(Request { - body: Some(body), + body: Some(to_vec(&map)?), multipart: None, headers: audit_log_reason.map(reason_into_header), method: LightMethod::Patch, @@ -2244,35 +2060,6 @@ impl Http { } /// Changes another user's voice state in a stage channel. - /// - /// The Value is a map with values of: - /// - **channel_id**: ID of the channel the user is currently in (**required**) - /// - **suppress**: Bool which toggles user's suppressed state. Setting this to `false` will - /// invite the user to speak. - /// - /// # Example - /// - /// Suppress a user - /// - /// ```rust,no_run - /// use serenity::http::Http; - /// use serenity::json::json; - /// use serenity::model::prelude::*; - /// - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let guild_id = GuildId::new(187450744427773963); - /// let user_id = UserId::new(150443906511667200); - /// let map = json!({ - /// "channel_id": "826929611849334784", - /// "suppress": true, - /// }); - /// - /// // Edit state for another user - /// http.edit_voice_state(guild_id, user_id, &map).await?; - /// # Ok(()) - /// # } - /// ``` pub async fn edit_voice_state( &self, guild_id: GuildId, @@ -2294,38 +2081,6 @@ impl Http { } /// Changes the current user's voice state in a stage channel. - /// - /// The Value is a map with values of: - /// - /// - **channel_id**: ID of the channel the user is currently in (**required**) - /// - **suppress**: Bool which toggles user's suppressed state. Setting this to `false` will - /// invite the user to speak. - /// - **request_to_speak_timestamp**: ISO8601 timestamp to set the user's request to speak. This - /// can be any present or future time. - /// - /// # Example - /// - /// Unsuppress the current bot user - /// - /// ```rust,no_run - /// use serenity::http::Http; - /// use serenity::json::json; - /// use serenity::model::prelude::*; - /// - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let guild_id = GuildId::new(187450744427773963); - /// let map = json!({ - /// "channel_id": "826929611849334784", - /// "suppress": false, - /// "request_to_speak_timestamp": "2021-03-31T18:45:31.297561+00:00" - /// }); - /// - /// // Edit state for current user - /// http.edit_voice_state_me(guild_id, &map).await?; - /// # Ok(()) - /// # } - /// ``` pub async fn edit_voice_state_me( &self, guild_id: GuildId, @@ -2345,37 +2100,6 @@ impl Http { } /// Edits a the webhook with the given data. - /// - /// The Value is a map with optional values of: - /// - **avatar**: base64-encoded 128x128 image for the webhook's default avatar (_optional_); - /// - **name**: the name of the webhook, limited to between 2 and 100 characters long. - /// - /// Note that, unlike with [`Self::create_webhook`], _all_ values are optional. - /// - /// This method requires authentication, whereas [`Self::edit_webhook_with_token`] does not. - /// - /// # Examples - /// - /// Edit the image of a webhook given its Id and unique token: - /// - /// ```rust,no_run - /// use serenity::builder::CreateAttachment; - /// use serenity::http::Http; - /// use serenity::json::json; - /// use serenity::model::prelude::*; - /// - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let id = WebhookId::new(245037420704169985); - /// let image = CreateAttachment::path("./webhook_img.png").await?; - /// let map = json!({ - /// "avatar": image.to_base64(), - /// }); - /// - /// let edited = http.edit_webhook(id, &map, None).await?; - /// # Ok(()) - /// # } - /// ``` pub async fn edit_webhook( &self, webhook_id: WebhookId, @@ -2396,30 +2120,6 @@ impl Http { } /// Edits the webhook with the given data. - /// - /// Refer to the documentation for [`Self::edit_webhook`] for more information. - /// - /// This method does _not_ require authentication. - /// - /// # Examples - /// - /// Edit the name of a webhook given its Id and unique token: - /// - /// ```rust,no_run - /// use serenity::http::Http; - /// use serenity::json::json; - /// use serenity::model::prelude::*; - /// - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let id = WebhookId::new(245037420704169985); - /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; - /// let map = json!({"name": "new name"}); - /// - /// let edited = http.edit_webhook_with_token(id, token, &map, None).await?; - /// # Ok(()) - /// # } - /// ``` pub async fn edit_webhook_with_token( &self, webhook_id: WebhookId, @@ -2444,67 +2144,23 @@ impl Http { } /// Executes a webhook, posting a [`Message`] in the webhook's associated [`Channel`]. - /// - /// This method does _not_ require authentication. - /// - /// If `thread_id` is not `None`, then the message will be sent to the thread in the webhook's - /// associated [`Channel`] with the corresponding Id, which will be automatically unarchived. - /// - /// If `wait` is `false`, this function will return `Ok(None)` on success. Otherwise, it will - /// wait for server confirmation of the message having been sent, and return `Ok(Some(msg))`. - /// From the [Discord docs]: - /// - /// > waits for server confirmation of message send before response, and returns the created - /// > message body (defaults to false; when false a message that is not saved does not return - /// > an error) - /// - /// The map can _optionally_ contain the following data: - /// - `avatar_url`: Override the default avatar of the webhook with a URL. - /// - `tts`: Whether this is a text-to-speech message (defaults to `false`). - /// - `username`: Override the default username of the webhook. - /// - /// Additionally, _at least one_ of the following must be given: - /// - `content`: The content of the message. - /// - `embeds`: An array of rich embeds. - /// - /// **Note**: For embed objects, all fields are registered by Discord except for `height`, - /// `provider`, `proxy_url`, `type` (it will always be `rich`), `video`, and `width`. The rest - /// will be determined by Discord. - /// - /// # Examples - /// - /// Sending a webhook with message content of `test`: - /// - /// ```rust,no_run - /// use serenity::http::Http; - /// use serenity::json::json; - /// use serenity::model::prelude::*; - /// - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let id = WebhookId::new(245037420704169985); - /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; - /// let map = json!({"content": "test"}); - /// let files = vec![]; - /// - /// let message = http.execute_webhook(id, None, token, true, files, &map).await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// [Discord docs]: https://discord.com/developers/docs/resources/webhook#execute-webhook-query-string-params pub async fn execute_webhook( &self, webhook_id: WebhookId, thread_id: Option, token: &str, wait: bool, - files: Vec, + files: Vec>, map: &impl serde::Serialize, ) -> Result> { - let mut params = vec![("wait", wait.to_string())]; + let thread_id_str; + let wait_str = wait.to_arraystring(); + let mut params = ArrayVec::<_, 2>::new(); + + params.push(("wait", wait_str.as_str())); if let Some(thread_id) = thread_id { - params.push(("thread_id", thread_id.to_string())); + thread_id_str = thread_id.to_arraystring(); + params.push(("thread_id", &thread_id_str)); } let mut request = Request { @@ -2516,14 +2172,14 @@ impl Http { webhook_id, token, }, - params: Some(params), + params: Some(¶ms), }; if files.is_empty() { request.body = Some(to_vec(map)?); } else { request.multipart = Some(Multipart { - upload: MultipartUpload::Attachments(files.into_iter().collect()), + upload: MultipartUpload::Attachments(files), payload_json: Some(to_string(map)?), fields: vec![], }); @@ -2531,11 +2187,7 @@ impl Http { let response = self.request(request).await?; - Ok(if response.status() == StatusCode::NO_CONTENT { - None - } else { - decode_resp(response).await? - }) + Ok(if response.status() == StatusCode::NO_CONTENT { None } else { response.json().await? }) } // Gets a webhook's message by Id @@ -2546,6 +2198,14 @@ impl Http { token: &str, message_id: MessageId, ) -> Result { + let thread_id_str; + let mut params = None; + + if let Some(thread_id) = thread_id { + thread_id_str = thread_id.to_arraystring(); + params = Some([("thread_id", thread_id_str.as_str())]); + } + self.fire(Request { body: None, multipart: None, @@ -2556,7 +2216,7 @@ impl Http { token, message_id, }, - params: thread_id.map(|thread_id| vec![("thread_id", thread_id.to_string())]), + params: params.as_ref().map(<[_; 1]>::as_slice), }) .await } @@ -2569,8 +2229,16 @@ impl Http { token: &str, message_id: MessageId, map: &impl serde::Serialize, - new_attachments: Vec, + new_attachments: Vec>, ) -> Result { + let thread_id_str; + let mut params = None; + + if let Some(thread_id) = thread_id { + thread_id_str = thread_id.to_arraystring(); + params = Some([("thread_id", thread_id_str.as_str())]); + } + let mut request = Request { body: None, multipart: None, @@ -2581,7 +2249,7 @@ impl Http { token, message_id, }, - params: thread_id.map(|thread_id| vec![("thread_id", thread_id.to_string())]), + params: params.as_ref().map(<[_; 1]>::as_slice), }; if new_attachments.is_empty() { @@ -2605,6 +2273,14 @@ impl Http { token: &str, message_id: MessageId, ) -> Result<()> { + let thread_id_str; + let mut params = None; + + if let Some(thread_id) = thread_id { + thread_id_str = thread_id.to_arraystring(); + params = Some([("thread_id", thread_id_str.as_str())]); + } + self.wind(204, Request { body: None, multipart: None, @@ -2615,7 +2291,7 @@ impl Http { token, message_id, }, - params: thread_id.map(|thread_id| vec![("thread_id", thread_id.to_string())]), + params: params.as_ref().map(<[_; 1]>::as_slice), }) .await } @@ -2658,19 +2334,25 @@ impl Http { &self, guild_id: GuildId, target: Option, - limit: Option, + limit: Option, ) -> Result> { - let mut params = vec![]; + let id_str; + let limit_str; + let mut params = ArrayVec::<_, 2>::new(); if let Some(limit) = limit { - params.push(("limit", limit.to_string())); + limit_str = limit.get().to_arraystring(); + params.push(("limit", limit_str.as_str())); } if let Some(target) = target { - match target { - UserPagination::After(id) => params.push(("after", id.to_string())), - UserPagination::Before(id) => params.push(("before", id.to_string())), - } + let (name, id) = match target { + UserPagination::After(id) => ("after", id), + UserPagination::Before(id) => ("before", id), + }; + + id_str = id.to_arraystring(); + params.push((name, &id_str)); } self.fire(Request { @@ -2681,7 +2363,7 @@ impl Http { route: Route::GuildBans { guild_id, }, - params: Some(params), + params: Some(¶ms), }) .await } @@ -2693,20 +2375,25 @@ impl Http { action_type: Option, user_id: Option, before: Option, - limit: Option, + limit: Option, ) -> Result { - let mut params = vec![]; + let (action_type_str, before_str, limit_str, user_id_str); + let mut params = ArrayVec::<_, 4>::new(); if let Some(action_type) = action_type { - params.push(("action_type", action_type.num().to_string())); + action_type_str = action_type.num().to_arraystring(); + params.push(("action_type", action_type_str.as_str())); } if let Some(before) = before { - params.push(("before", before.to_string())); + before_str = before.to_arraystring(); + params.push(("before", &before_str)); } if let Some(limit) = limit { - params.push(("limit", limit.to_string())); + limit_str = limit.get().to_arraystring(); + params.push(("limit", &limit_str)); } if let Some(user_id) = user_id { - params.push(("user_id", user_id.to_string())); + user_id_str = user_id.to_arraystring(); + params.push(("user_id", &user_id_str)); } self.fire(Request { @@ -2717,14 +2404,12 @@ impl Http { route: Route::GuildAuditLogs { guild_id, }, - params: Some(params), + params: Some(¶ms), }) .await } /// Retrieves all auto moderation rules in a guild. - /// - /// This method requires `MANAGE_GUILD` permissions. pub async fn get_automod_rules(&self, guild_id: GuildId) -> Result> { self.fire(Request { body: None, @@ -2740,8 +2425,6 @@ impl Http { } /// Retrieves an auto moderation rule in a guild. - /// - /// This method requires `MANAGE_GUILD` permissions. pub async fn get_automod_rule(&self, guild_id: GuildId, rule_id: RuleId) -> Result { self.fire(Request { body: None, @@ -2758,8 +2441,6 @@ impl Http { } /// Creates an auto moderation rule in a guild. - /// - /// This method requires `MANAGE_GUILD` permissions. pub async fn create_automod_rule( &self, guild_id: GuildId, @@ -2782,8 +2463,6 @@ impl Http { } /// Retrieves an auto moderation rule in a guild. - /// - /// This method requires `MANAGE_GUILD` permissions. pub async fn edit_automod_rule( &self, guild_id: GuildId, @@ -2808,8 +2487,6 @@ impl Http { } /// Deletes an auto moderation rule in a guild. - /// - /// This method requires `MANAGE_GUILD` permissions. pub async fn delete_automod_rule( &self, guild_id: GuildId, @@ -2895,15 +2572,18 @@ impl Http { pub async fn get_channel_archived_public_threads( &self, channel_id: ChannelId, - before: Option, + before: Option, limit: Option, ) -> Result { - let mut params = vec![]; + let (before_str, limit_str); + let mut params = ArrayVec::<_, 2>::new(); if let Some(before) = before { - params.push(("before", before.to_string())); + before_str = before.to_string(); + params.push(("before", before_str.as_str())); } if let Some(limit) = limit { - params.push(("limit", limit.to_string())); + limit_str = limit.to_arraystring(); + params.push(("limit", &limit_str)); } self.fire(Request { @@ -2914,7 +2594,7 @@ impl Http { route: Route::ChannelArchivedPublicThreads { channel_id, }, - params: Some(params), + params: Some(¶ms), }) .await } @@ -2923,15 +2603,18 @@ impl Http { pub async fn get_channel_archived_private_threads( &self, channel_id: ChannelId, - before: Option, + before: Option, limit: Option, ) -> Result { - let mut params = vec![]; + let (before_str, limit_str); + let mut params = ArrayVec::<_, 2>::new(); if let Some(before) = before { - params.push(("before", before.to_string())); + before_str = before.to_string(); + params.push(("before", before_str.as_str())); } if let Some(limit) = limit { - params.push(("limit", limit.to_string())); + limit_str = limit.to_arraystring(); + params.push(("limit", &limit_str)); } self.fire(Request { @@ -2942,7 +2625,7 @@ impl Http { route: Route::ChannelArchivedPrivateThreads { channel_id, }, - params: Some(params), + params: Some(¶ms), }) .await } @@ -2951,15 +2634,18 @@ impl Http { pub async fn get_channel_joined_archived_private_threads( &self, channel_id: ChannelId, - before: Option, + before: Option, limit: Option, ) -> Result { - let mut params = vec![]; + let (before_str, limit_str); + let mut params = ArrayVec::<_, 2>::new(); if let Some(before) = before { - params.push(("before", before.to_string())); + before_str = before.to_arraystring(); + params.push(("before", before_str.as_str())); } if let Some(limit) = limit { - params.push(("limit", limit.to_string())); + limit_str = limit.to_arraystring(); + params.push(("limit", &limit_str)); } self.fire(Request { @@ -2970,7 +2656,7 @@ impl Http { route: Route::ChannelJoinedPrivateThreads { channel_id, }, - params: Some(params), + params: Some(¶ms), }) .await } @@ -3060,31 +2746,12 @@ impl Http { channel_id, user_id, }, - params: Some(vec![("with_member", with_member.to_string())]), + params: Some(&[("with_member", &with_member.to_arraystring())]), }) .await } /// Retrieves the webhooks for the given [channel][`GuildChannel`]'s Id. - /// - /// This method requires authentication. - /// - /// # Examples - /// - /// Retrieve all of the webhooks owned by a channel: - /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # use serenity::model::prelude::*; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let channel_id = ChannelId::new(81384788765712384); - /// - /// let webhooks = http.get_channel_webhooks(channel_id).await?; - /// # Ok(()) - /// # } - /// ``` pub async fn get_channel_webhooks(&self, channel_id: ChannelId) -> Result> { self.fire(Request { body: None, @@ -3115,7 +2782,10 @@ impl Http { } /// Gets all channels in a guild. - pub async fn get_channels(&self, guild_id: GuildId) -> Result> { + pub async fn get_channels( + &self, + guild_id: GuildId, + ) -> Result> { self.fire(Request { body: None, multipart: None, @@ -3158,13 +2828,16 @@ impl Http { users: Vec, } + let (after_str, limit_str); let mut params = Vec::with_capacity(2); if let Some(after) = after { - params.push(("after", after.to_string())); + after_str = after.to_arraystring(); + params.push(("after", after_str.as_str())); } if let Some(limit) = limit { - params.push(("limit", limit.to_string())); + limit_str = limit.to_arraystring(); + params.push(("limit", &limit_str)); } let resp: VotersResponse = self @@ -3178,7 +2851,7 @@ impl Http { message_id, answer_id, }, - params: Some(params), + params: Some(¶ms), }) .await?; @@ -3271,34 +2944,40 @@ impl Http { sku_ids: Option>, before: Option, after: Option, - limit: Option, + limit: Option, guild_id: Option, exclude_ended: Option, ) -> Result> { - let mut params = vec![]; + let (user_id_str, sku_ids_str, before_str, after_str, limit_str, guild_id_str, exclude_str); + let mut params = ArrayVec::<_, 7>::new(); + if let Some(user_id) = user_id { - params.push(("user_id", user_id.to_string())); + user_id_str = user_id.to_arraystring(); + params.push(("user_id", user_id_str.as_str())); } if let Some(sku_ids) = sku_ids { - params.push(( - "sku_ids", - sku_ids.iter().map(ToString::to_string).collect::>().join(","), - )); + sku_ids_str = join_to_string(',', sku_ids); + params.push(("sku_ids", &sku_ids_str)); } if let Some(before) = before { - params.push(("before", before.to_string())); + before_str = before.to_arraystring(); + params.push(("before", &before_str)); } if let Some(after) = after { - params.push(("after", after.to_string())); + after_str = after.to_arraystring(); + params.push(("after", &after_str)); } if let Some(limit) = limit { - params.push(("limit", limit.to_string())); + limit_str = limit.get().to_arraystring(); + params.push(("limit", &limit_str)); } if let Some(guild_id) = guild_id { - params.push(("guild_id", guild_id.to_string())); + guild_id_str = guild_id.to_arraystring(); + params.push(("guild_id", &guild_id_str)); } if let Some(exclude_ended) = exclude_ended { - params.push(("exclude_ended", exclude_ended.to_string())); + exclude_str = exclude_ended.to_arraystring(); + params.push(("exclude_ended", &exclude_str)); } self.fire(Request { @@ -3309,7 +2988,7 @@ impl Http { route: Route::Entitlements { application_id: self.try_application_id()?, }, - params: Some(params), + params: Some(¶ms), }) .await } @@ -3352,7 +3031,7 @@ impl Http { route: Route::Commands { application_id: self.try_application_id()?, }, - params: Some(vec![("with_localizations", true.to_string())]), + params: Some(&[("with_localizations", "true")]), }) .await } @@ -3398,7 +3077,7 @@ impl Http { route: Route::Guild { guild_id, }, - params: Some(vec![("with_counts", true.to_string())]), + params: Some(&[("with_counts", "true")]), }) .await } @@ -3434,7 +3113,7 @@ impl Http { application_id: self.try_application_id()?, guild_id, }, - params: Some(vec![("with_localizations", true.to_string())]), + params: Some(&[("with_localizations", "true")]), }) .await } @@ -3584,7 +3263,7 @@ impl Http { code: String, } - self.fire::(Request { + self.fire(Request { body: None, multipart: None, headers: None, @@ -3595,7 +3274,7 @@ impl Http { params: None, }) .await - .map(|x| x.code) + .map(|x: GuildVanityUrl| x.code) } /// Gets the members of a guild. Optionally pass a `limit` and the Id of the user to offset the @@ -3603,19 +3282,18 @@ impl Http { pub async fn get_guild_members( &self, guild_id: GuildId, - limit: Option, - after: Option, + limit: Option, + after: Option, ) -> Result> { - if let Some(l) = limit { - if !(1..=constants::MEMBER_FETCH_LIMIT).contains(&l) { - return Err(Error::NotInRange("limit", l, 1, constants::MEMBER_FETCH_LIMIT)); - } - } + let (limit_str, after_str); + let mut params = ArrayVec::<_, 2>::new(); + + limit_str = limit.unwrap_or(constants::MEMBER_FETCH_LIMIT).get().to_arraystring(); + params.push(("limit", limit_str.as_str())); - let mut params = - vec![("limit", limit.unwrap_or(constants::MEMBER_FETCH_LIMIT).to_string())]; if let Some(after) = after { - params.push(("after", after.to_string())); + after_str = after.to_arraystring(); + params.push(("after", &after_str)); } let mut value: Value = self @@ -3627,7 +3305,7 @@ impl Http { route: Route::GuildMembers { guild_id, }, - params: Some(params), + params: Some(¶ms), }) .await?; @@ -3644,6 +3322,7 @@ impl Http { /// Gets the amount of users that can be pruned. pub async fn get_guild_prune_count(&self, guild_id: GuildId, days: u8) -> Result { + let days_str = days.to_arraystring(); self.fire(Request { body: None, multipart: None, @@ -3652,7 +3331,7 @@ impl Http { route: Route::GuildPrune { guild_id, }, - params: Some(vec![("days", days.to_string())]), + params: Some(&[("days", &days_str)]), }) .await } @@ -3674,7 +3353,7 @@ impl Http { } /// Retrieves a list of roles in a [`Guild`]. - pub async fn get_guild_roles(&self, guild_id: GuildId) -> Result> { + pub async fn get_guild_roles(&self, guild_id: GuildId) -> Result> { let mut value: Value = self .fire(Request { body: None, @@ -3700,16 +3379,13 @@ impl Http { } /// Gets a scheduled event by Id. - /// - /// **Note**: Requires the [View Channel] permission for the channel associated with the event. - /// - /// [View Channel]: Permissions::VIEW_CHANNEL pub async fn get_scheduled_event( &self, guild_id: GuildId, event_id: ScheduledEventId, with_user_count: bool, ) -> Result { + let with_user_count_str = with_user_count.to_arraystring(); self.fire(Request { body: None, multipart: None, @@ -3719,21 +3395,18 @@ impl Http { guild_id, event_id, }, - params: Some(vec![("with_user_count", with_user_count.to_string())]), + params: Some(&[("with_user_count", &with_user_count_str)]), }) .await } /// Gets a list of all scheduled events for the corresponding guild. - /// - /// **Note**: Requires the [View Channel] permission at the guild level. - /// - /// [View Channel]: Permissions::VIEW_CHANNEL pub async fn get_scheduled_events( &self, guild_id: GuildId, with_user_count: bool, ) -> Result> { + let with_user_count_str = with_user_count.to_arraystring(); self.fire(Request { body: None, multipart: None, @@ -3742,47 +3415,39 @@ impl Http { route: Route::GuildScheduledEvents { guild_id, }, - params: Some(vec![("with_user_count", with_user_count.to_string())]), + params: Some(&[("with_user_count", &with_user_count_str)]), }) .await } /// Gets a list of all interested users for the corresponding scheduled event, with additional /// options for filtering. - /// - /// If `limit` is left unset, by default at most 100 users are returned. - /// - /// If `target` is set, then users will be filtered by Id, such that their Id comes before or - /// after the provided [`UserId`] wrapped by the [`UserPagination`]. - /// - /// If `with_member` is set to `Some(true)`, then the [`member`] field of the user struct will - /// be populated with [`Guild Member`] information, if the interested user is a member of the - /// guild the event takes place in. - /// - /// **Note**: Requires the [View Channel] permission for the channel associated with the event. - /// - /// [`member`]: ScheduledEventUser::member - /// [`Guild Member`]: crate::model::guild::Member pub async fn get_scheduled_event_users( &self, guild_id: GuildId, event_id: ScheduledEventId, - limit: Option, + limit: Option, target: Option, with_member: Option, ) -> Result> { - let mut params = vec![]; + let (limit_str, with_member_str, id_str); + let mut params = ArrayVec::<_, 3>::new(); if let Some(limit) = limit { - params.push(("limit", limit.to_string())); + limit_str = limit.get().to_arraystring(); + params.push(("limit", limit_str.as_str())); } if let Some(with_member) = with_member { - params.push(("with_member", with_member.to_string())); + with_member_str = with_member.to_arraystring(); + params.push(("with_member", &with_member_str)); } if let Some(target) = target { - match target { - UserPagination::After(id) => params.push(("after", id.to_string())), - UserPagination::Before(id) => params.push(("before", id.to_string())), - } + let (name, id) = match target { + UserPagination::After(id) => ("after", id), + UserPagination::Before(id) => ("before", id), + }; + + id_str = id.to_arraystring(); + params.push((name, &id_str)); } self.fire(Request { @@ -3794,7 +3459,7 @@ impl Http { guild_id, event_id, }, - params: Some(params), + params: Some(¶ms), }) .await } @@ -3852,26 +3517,7 @@ impl Http { from_value(value).map_err(From::from) } - /// Retrieves the webhooks for the given [guild][`Guild`]'s Id. - /// - /// This method requires authentication. - /// - /// # Examples - /// - /// Retrieve all of the webhooks owned by a guild: - /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # use serenity::model::prelude::*; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let guild_id = GuildId::new(81384788765712384); - /// - /// let webhooks = http.get_guild_webhooks(guild_id).await?; - /// # Ok(()) - /// # } - /// ``` + /// Retrieves the webhooks for the given [`Guild`]'s Id. pub async fn get_guild_webhooks(&self, guild_id: GuildId) -> Result> { self.fire(Request { body: None, @@ -3887,45 +3533,25 @@ impl Http { } /// Gets a paginated list of the current user's guilds. - /// - /// The `limit` has a maximum value of 100. - /// - /// [Discord's documentation][docs] - /// - /// # Examples - /// - /// Get the first 10 guilds after a certain guild's Id: - /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// use serenity::http::GuildPagination; - /// use serenity::model::id::GuildId; - /// - /// let guild_id = GuildId::new(81384788765712384); - /// - /// let guilds = http.get_guilds(Some(GuildPagination::After(guild_id)), Some(10)).await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// [docs]: https://discord.com/developers/docs/resources/user#get-current-user-guilds pub async fn get_guilds( &self, target: Option, - limit: Option, + limit: Option, ) -> Result> { - let mut params = vec![]; + let (limit_str, id_str); + let mut params = ArrayVec::<_, 2>::new(); if let Some(limit) = limit { - params.push(("limit", limit.to_string())); + limit_str = limit.get().to_arraystring(); + params.push(("limit", limit_str.as_str())); } if let Some(target) = target { - match target { - GuildPagination::After(id) => params.push(("after", id.to_string())), - GuildPagination::Before(id) => params.push(("before", id.to_string())), - } + let (name, id) = match target { + GuildPagination::After(id) => ("after", id), + GuildPagination::Before(id) => ("before", id), + }; + + id_str = id.to_arraystring(); + params.push((name, &id_str)); } self.fire(Request { @@ -3934,40 +3560,16 @@ impl Http { headers: None, method: LightMethod::Get, route: Route::UserMeGuilds, - params: Some(params), + params: Some(¶ms), }) .await } /// Returns a guild [`Member`] object for the current user. /// - /// # Authorization - /// /// This method only works for user tokens with the [`GuildsMembersRead`] OAuth2 scope. /// /// [`GuildsMembersRead`]: crate::model::application::Scope::GuildsMembersRead - /// - /// # Examples - /// - /// Get the member object for the current user within the specified guild. - /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// use serenity::model::id::GuildId; - /// - /// let guild_id = GuildId::new(81384788765712384); - /// - /// let member = http.get_current_user_guild_member(guild_id).await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// See the [Discord Developer Portal documentation][docs] for more. - /// - /// [docs]: https://discord.com/developers/docs/resources/user#get-current-user-guild-member pub async fn get_current_user_guild_member(&self, guild_id: GuildId) -> Result { let mut value: Value = self .fire(Request { @@ -3990,16 +3592,6 @@ impl Http { } /// Gets information about a specific invite. - /// - /// # Arguments - /// * `code` - The invite code. - /// * `member_counts` - Whether to include information about the current number of members in - /// the server that the invite belongs to. - /// * `expiration` - Whether to include information about when the invite expires. - /// * `event_id` - An optional server event ID to include with the invite. - /// - /// More information about these arguments can be found on Discord's - /// [API documentation](https://discord.com/developers/docs/resources/invite#get-invite). pub async fn get_invite( &self, code: &str, @@ -4007,15 +3599,21 @@ impl Http { expiration: bool, event_id: Option, ) -> Result { + let (member_counts_str, expiration_str, event_id_str); #[cfg(feature = "utils")] let code = crate::utils::parse_invite(code); - let mut params = vec![ - ("member_counts", member_counts.to_string()), - ("expiration", expiration.to_string()), - ]; + let mut params = ArrayVec::<_, 3>::new(); + + member_counts_str = member_counts.to_arraystring(); + params.push(("member_counts", member_counts_str.as_str())); + + expiration_str = expiration.to_arraystring(); + params.push(("expiration", &expiration_str)); + if let Some(event_id) = event_id { - params.push(("event_id", event_id.to_string())); + event_id_str = event_id.to_arraystring(); + params.push(("event_id", &event_id_str)); } self.fire(Request { @@ -4026,7 +3624,7 @@ impl Http { route: Route::Invite { code, }, - params: Some(params), + params: Some(¶ms), }) .await } @@ -4079,18 +3677,25 @@ impl Http { &self, channel_id: ChannelId, target: Option, - limit: Option, + limit: Option, ) -> Result> { - let mut params = vec![]; + let (limit_str, id_str); + let mut params = ArrayVec::<_, 2>::new(); + if let Some(limit) = limit { - params.push(("limit", limit.to_string())); + limit_str = limit.get().to_arraystring(); + params.push(("limit", limit_str.as_str())); } + if let Some(target) = target { - match target { - MessagePagination::After(id) => params.push(("after", id.to_string())), - MessagePagination::Around(id) => params.push(("around", id.to_string())), - MessagePagination::Before(id) => params.push(("before", id.to_string())), - } + let (name, id) = match target { + MessagePagination::After(id) => ("after", id), + MessagePagination::Around(id) => ("around", id), + MessagePagination::Before(id) => ("before", id), + }; + + id_str = id.to_arraystring(); + params.push((name, &id_str)); } self.fire(Request { @@ -4101,7 +3706,7 @@ impl Http { route: Route::ChannelMessages { channel_id, }, - params: Some(params), + params: Some(¶ms), }) .await } @@ -4113,7 +3718,7 @@ impl Http { sticker_packs: Vec, } - self.fire::(Request { + self.fire(Request { body: None, multipart: None, headers: None, @@ -4122,7 +3727,7 @@ impl Http { params: None, }) .await - .map(|s| s.sticker_packs) + .map(|s: StickerPacks| s.sticker_packs) } /// Gets all pins of a channel. @@ -4147,12 +3752,19 @@ impl Http { message_id: MessageId, reaction_type: &ReactionType, limit: u8, - after: Option, + after: Option, ) -> Result> { - let mut params = vec![("limit", limit.to_string())]; + let (limit_str, after_str); + let mut params = ArrayVec::<_, 2>::new(); + + limit_str = limit.to_arraystring(); + params.push(("limit", limit_str.as_str())); + if let Some(after) = after { - params.push(("after", after.to_string())); + after_str = after.to_arraystring(); + params.push(("after", &after_str)); } + self.fire(Request { body: None, multipart: None, @@ -4163,7 +3775,7 @@ impl Http { message_id, reaction: &reaction_type.as_data(), }, - params: Some(params), + params: Some(¶ms), }) .await } @@ -4308,22 +3920,6 @@ impl Http { /// /// This method requires authentication, whereas [`Http::get_webhook_with_token`] and /// [`Http::get_webhook_from_url`] do not. - /// - /// # Examples - /// - /// Retrieve a webhook by Id: - /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # use serenity::model::prelude::*; - /// - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let id = WebhookId::new(245037420704169985); - /// let webhook = http.get_webhook(id).await?; - /// # Ok(()) - /// # } - /// ``` pub async fn get_webhook(&self, webhook_id: WebhookId) -> Result { self.fire(Request { body: None, @@ -4341,24 +3937,6 @@ impl Http { /// Retrieves a webhook given its Id and unique token. /// /// This method does _not_ require authentication. - /// - /// # Examples - /// - /// Retrieve a webhook by Id and its unique token: - /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # use serenity::model::prelude::*; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let id = WebhookId::new(245037420704169985); - /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; - /// - /// let webhook = http.get_webhook_with_token(id, token).await?; - /// # Ok(()) - /// # } - /// ``` pub async fn get_webhook_with_token( &self, webhook_id: WebhookId, @@ -4381,21 +3959,6 @@ impl Http { /// Retrieves a webhook given its url. /// /// This method does _not_ require authentication - /// - /// # Examples - /// - /// Retrieve a webhook by url: - /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; - /// let webhook = http.get_webhook_from_url(url).await?; - /// # Ok(()) - /// # } - /// ``` #[cfg(feature = "utils")] pub async fn get_webhook_from_url(&self, url: &str) -> Result { let url = Url::parse(url).map_err(HttpError::Url)?; @@ -4452,14 +4015,10 @@ impl Http { } /// Sends a message to a channel. - /// - /// # Errors - /// - /// Returns an [`HttpError::UnsuccessfulRequest`] if the files are too large to send. pub async fn send_message( &self, channel_id: ChannelId, - files: Vec, + files: Vec>, map: &impl serde::Serialize, ) -> Result { let mut request = Request { @@ -4477,7 +4036,7 @@ impl Http { request.body = Some(to_vec(map)?); } else { request.multipart = Some(Multipart { - upload: MultipartUpload::Attachments(files.into_iter().collect()), + upload: MultipartUpload::Attachments(files), payload_json: Some(to_string(map)?), fields: vec![], }); @@ -4529,10 +4088,6 @@ impl Http { } /// Deletes a single [`Role`] from a [`Member`] in a [`Guild`]. - /// - /// **Note**: Requires the [Manage Roles] permission and respect of role hierarchy. - /// - /// [Manage Roles]: Permissions::MANAGE_ROLES pub async fn remove_member_role( &self, guild_id: GuildId, @@ -4561,8 +4116,9 @@ impl Http { &self, guild_id: GuildId, query: &str, - limit: Option, + limit: Option, ) -> Result> { + let limit_str = limit.unwrap_or(constants::MEMBER_FETCH_LIMIT).get().to_arraystring(); let mut value: Value = self .fire(Request { body: None, @@ -4572,10 +4128,7 @@ impl Http { route: Route::GuildMembersSearch { guild_id, }, - params: Some(vec![ - ("query", query.to_string()), - ("limit", limit.unwrap_or(constants::MEMBER_FETCH_LIMIT).to_string()), - ]), + params: Some(&[("query", query), ("limit", &limit_str)]), }) .await?; @@ -4597,6 +4150,7 @@ impl Http { days: u8, audit_log_reason: Option<&str>, ) -> Result { + let days_str = days.to_arraystring(); self.fire(Request { body: None, multipart: None, @@ -4605,7 +4159,7 @@ impl Http { route: Route::GuildPrune { guild_id, }, - params: Some(vec![("days", days.to_string())]), + params: Some(&[("days", &days_str)]), }) .await } @@ -4630,46 +4184,6 @@ impl Http { .await } - /// Starts typing in the specified [`Channel`] for an indefinite period of time. - /// - /// Returns [`Typing`] that is used to trigger the typing. [`Typing::stop`] must be called on - /// the returned struct to stop typing. Note that on some clients, typing may persist for a few - /// seconds after [`Typing::stop`] is called. Typing is also stopped when the struct is - /// dropped. - /// - /// If a message is sent while typing is triggered, the user will stop typing for a brief - /// period of time and then resume again until either [`Typing::stop`] is called or the struct - /// is dropped. - /// - /// This should rarely be used for bots, although it is a good indicator that a long-running - /// command is still being processed. - /// - /// ## Examples - /// - /// ```rust,no_run - /// # use std::sync::Arc; - /// # use serenity::http::{Http, Typing}; - /// # use serenity::Result; - /// # use serenity::model::prelude::*; - /// # - /// # fn long_process() {} - /// # fn main() { - /// # let http: Arc = unimplemented!(); - /// // Initiate typing (assuming http is `Arc`) - /// let channel_id = ChannelId::new(7); - /// let typing = http.start_typing(channel_id); - /// - /// // Run some long-running process - /// long_process(); - /// - /// // Stop typing - /// typing.stop(); - /// # } - /// ``` - pub fn start_typing(self: &Arc, channel_id: ChannelId) -> Typing { - Typing::start(Arc::clone(self), channel_id) - } - /// Unpins a message from a channel. pub async fn unpin_message( &self, @@ -4696,81 +4210,28 @@ impl Http { /// If you don't need to deserialize the response and want the response instance itself, use /// [`Self::request`]. /// - /// # Examples - /// - /// Create a new message and deserialize the response into a [`Message`]: - /// - /// ```rust,no_run - /// # async fn run() -> Result<(), Box> { - /// # use serenity::http::Http; - /// # - /// # let http: Http = unimplemented!(); - /// use serenity::{ - /// http::{LightMethod, Request, Route}, - /// model::prelude::*, - /// }; - /// - /// let bytes = vec![ - /// // payload bytes here - /// ]; - /// let channel_id = ChannelId::new(381880193700069377); - /// let route = Route::ChannelMessages { channel_id }; - /// - /// let mut request = Request::new(route, LightMethod::Post).body(Some(bytes)); - /// - /// let message = http.fire::(request).await?; - /// - /// println!("Message content: {}", message.content); - /// # Ok(()) - /// # } - /// ``` - /// /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. pub async fn fire(&self, req: Request<'_>) -> Result { let response = self.request(req).await?; - decode_resp(response).await + let response_de = response.json().await?; + Ok(response_de) } /// Performs a request, ratelimiting it if necessary. /// /// Returns the raw reqwest Response. Use [`Self::fire`] to deserialize the response into some /// type. - /// - /// # Examples - /// - /// Send a body of bytes over the create message endpoint: - /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # use serenity::model::prelude::*; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let http: Http = unimplemented!(); - /// use serenity::http::{LightMethod, Request, Route}; - /// - /// let bytes = vec![ - /// // payload bytes here - /// ]; - /// let channel_id = ChannelId::new(381880193700069377); - /// let route = Route::ChannelMessages { channel_id }; - /// - /// let mut request = Request::new(route, LightMethod::Post).body(Some(bytes)); - /// - /// let response = http.request(request).await?; - /// - /// println!("Response successful?: {}", response.status().is_success()); - /// # Ok(()) - /// # } - /// ``` - #[instrument] + #[cfg_attr(feature = "tracing_instrument", instrument)] pub async fn request(&self, req: Request<'_>) -> Result { let method = req.method.reqwest_method(); let response = if let Some(ratelimiter) = &self.ratelimiter { ratelimiter.perform(req).await? } else { - let request = req.build(&self.client, self.token(), self.proxy.as_deref())?.build()?; + let request = req + .build(&self.client, self.token.expose_secret(), self.proxy.as_deref())? + .build()?; self.client.execute(request).await? }; @@ -4814,9 +4275,3 @@ fn configure_client_backend(builder: ClientBuilder) -> ClientBuilder { fn configure_client_backend(builder: ClientBuilder) -> ClientBuilder { builder.use_native_tls() } - -impl AsRef for Http { - fn as_ref(&self) -> &Http { - self - } -} diff --git a/src/http/error.rs b/src/http/error.rs index d57c6e9ea73..1c2004a507a 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -1,5 +1,7 @@ +use std::collections::HashMap; use std::error::Error as StdError; use std::fmt; +use std::sync::Arc; use reqwest::header::InvalidHeaderValue; use reqwest::{Error as ReqwestError, Method, Response, StatusCode}; @@ -7,36 +9,42 @@ use serde::de::{Deserialize, Deserializer, Error as _}; use url::ParseError as UrlError; use crate::internal::prelude::*; -use crate::json::*; #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] #[non_exhaustive] pub struct DiscordJsonError { /// The error code. - pub code: isize, + pub code: i32, /// The error message. - pub message: String, + pub message: FixedString, /// The full explained errors with their path in the request body. #[serde(default, deserialize_with = "deserialize_errors")] - pub errors: Vec, + pub errors: FixedArray, } -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[derive(serde::Deserialize)] +struct RawDiscordJsonSingleError { + code: FixedString, + message: FixedString, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct DiscordJsonSingleError { /// The error code. - pub code: String, + pub code: FixedString, /// The error message. - pub message: String, + pub message: FixedString, /// The path to the error in the request body itself, dot separated. - pub path: String, + #[serde(skip)] + pub path: Arc, } #[derive(Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub struct ErrorResponse { - pub status_code: StatusCode, - pub url: String, pub method: Method, + pub status_code: StatusCode, + pub url: FixedString, pub error: DiscordJsonError, } @@ -44,13 +52,13 @@ impl ErrorResponse { // We need a freestanding from-function since we cannot implement an async From-trait. pub async fn from_response(r: Response, method: Method) -> Self { ErrorResponse { - status_code: r.status(), - url: r.url().to_string(), method, - error: decode_resp(r).await.unwrap_or_else(|e| DiscordJsonError { + status_code: r.status(), + url: FixedString::from_str_trunc(r.url().as_str()), + error: r.json().await.unwrap_or_else(|e| DiscordJsonError { code: -1, - message: format!("[Serenity] Could not decode json when receiving error response from discord:, {e}"), - errors: vec![], + errors: FixedArray::empty(), + message: format!("[Serenity] Could not decode json when receiving error response from discord:, {e}").trunc_into(), }), } } @@ -74,10 +82,6 @@ pub enum HttpError { InvalidHeader(InvalidHeaderValue), /// Reqwest's Error contain information on why sending a request failed. Request(ReqwestError), - /// When using a proxy with an invalid scheme. - InvalidScheme, - /// When using a proxy with an invalid port. - InvalidPort, /// When an application id was expected but missing. ApplicationIdMissing, } @@ -165,8 +169,6 @@ impl fmt::Display for HttpError { Self::InvalidWebhook => f.write_str("Provided URL is not a valid webhook."), Self::InvalidHeader(_) => f.write_str("Provided value is an invalid header value."), Self::Request(_) => f.write_str("Error while sending HTTP request."), - Self::InvalidScheme => f.write_str("Invalid Url scheme."), - Self::InvalidPort => f.write_str("Invalid port."), Self::ApplicationIdMissing => f.write_str("Application id was expected but missing."), } } @@ -185,63 +187,57 @@ impl StdError for HttpError { #[allow(clippy::missing_errors_doc)] pub fn deserialize_errors<'de, D: Deserializer<'de>>( deserializer: D, -) -> StdResult, D::Error> { - let map: Value = Value::deserialize(deserializer)?; - - if !map.is_object() { - return Ok(vec![]); - } +) -> StdResult, D::Error> { + let ErrorValue::Recurse(map) = ErrorValue::deserialize(deserializer)? else { + return Ok(FixedArray::new()); + }; let mut errors = Vec::new(); let mut path = Vec::new(); - loop_errors(&map, &mut errors, &mut path).map_err(D::Error::custom)?; + loop_errors(map, &mut errors, &mut path).map_err(D::Error::custom)?; - Ok(errors) + Ok(errors.trunc_into()) } fn make_error( - errors_value: &Value, + errors_to_process: Vec, errors: &mut Vec, path: &[&str], -) -> StdResult<(), &'static str> { - let found_errors = errors_value.as_array().ok_or("expected array")?; - - for error in found_errors { - let error_object = error.as_object().ok_or("expected object")?; - - errors.push(DiscordJsonSingleError { - code: error_object - .get("code") - .ok_or("expected code")? - .as_str() - .ok_or("expected string")? - .to_owned(), - message: error_object - .get("message") - .ok_or("expected message")? - .as_str() - .ok_or("expected string")? - .to_owned(), - path: path.join("."), - }); - } - Ok(()) +) { + let joined_path = Arc::from(path.join(".")); + errors.extend(errors_to_process.into_iter().map(|raw| DiscordJsonSingleError { + code: raw.code, + message: raw.message, + path: Arc::clone(&joined_path), + })); +} + +#[derive(serde::Deserialize)] +#[serde(untagged)] +enum ErrorValue<'a> { + Base(Vec), + #[serde(borrow)] + Recurse(HashMap<&'a str, ErrorValue<'a>>), } fn loop_errors<'a>( - value: &'a Value, + value: HashMap<&'a str, ErrorValue<'a>>, errors: &mut Vec, path: &mut Vec<&'a str>, -) -> StdResult<(), &'static str> { - for (key, value) in value.as_object().ok_or("expected object")? { +) -> Result<(), &'static str> { + for (key, value) in value { if key == "_errors" { - make_error(value, errors, path)?; + let ErrorValue::Base(value) = value else { return Err("expected array, found map") }; + make_error(value, errors, path); } else { + let ErrorValue::Recurse(value) = value else { return Err("expected map, found array") }; + path.push(key); loop_errors(value, errors, path)?; path.pop(); } } + Ok(()) } @@ -249,6 +245,7 @@ fn loop_errors<'a>( mod test { use http_crate::response::Builder; use reqwest::ResponseBuilderExt; + use serde_json::to_string; use super::*; @@ -256,8 +253,8 @@ mod test { async fn test_error_response_into() { let error = DiscordJsonError { code: 43121215, - message: String::from("This is a Ferris error"), - errors: vec![], + errors: FixedArray::empty(), + message: FixedString::from_static_trunc("This is a Ferris error"), }; let mut builder = Builder::new(); @@ -271,7 +268,7 @@ mod test { let known = ErrorResponse { status_code: reqwest::StatusCode::from_u16(403).unwrap(), - url: String::from("https://ferris.crab/"), + url: FixedString::from_static_trunc("https://ferris.crab/"), method: Method::POST, error, }; diff --git a/src/http/mod.rs b/src/http/mod.rs index 949294a335d..5484634ff73 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -106,10 +106,11 @@ impl CacheHttp for Context { } #[cfg(feature = "cache")] -impl CacheHttp for (&Arc, &Http) { +impl CacheHttp for (Option<&Arc>, &Http) { fn cache(&self) -> Option<&Arc> { - Some(self.0) + self.0 } + fn http(&self) -> &Http { self.1 } @@ -121,20 +122,6 @@ impl CacheHttp for Http { } } -#[cfg(feature = "cache")] -impl AsRef for (&Arc, &Http) { - fn as_ref(&self) -> &Cache { - self.0 - } -} - -#[cfg(feature = "cache")] -impl AsRef for (&Arc, &Http) { - fn as_ref(&self) -> &Http { - self.1 - } -} - /// An method used for ratelimiting special routes. /// /// This is needed because [`reqwest`]'s [`Method`] enum does not derive Copy. diff --git a/src/http/multipart.rs b/src/http/multipart.rs index 35e025af8d8..3a54088c25c 100644 --- a/src/http/multipart.rs +++ b/src/http/multipart.rs @@ -5,7 +5,7 @@ use reqwest::multipart::{Form, Part}; use crate::builder::CreateAttachment; use crate::internal::prelude::*; -impl CreateAttachment { +impl<'a> CreateAttachment<'a> { fn into_part(self) -> Result { let mut part = Part::bytes(self.data); part = guess_mime_str(part, &self.filename)?; @@ -15,18 +15,18 @@ impl CreateAttachment { } #[derive(Clone, Debug)] -pub enum MultipartUpload { +pub enum MultipartUpload<'a> { /// A file sent with the form data as an individual upload. For example, a sticker. - File(CreateAttachment), + File(CreateAttachment<'a>), /// Files sent with the form as message attachments. - Attachments(Vec), + Attachments(Vec>), } /// Holder for multipart body. Contains upload data, multipart fields, and payload_json for /// creating requests with attachments. #[derive(Clone, Debug)] -pub struct Multipart { - pub upload: MultipartUpload, +pub struct Multipart<'a> { + pub upload: MultipartUpload<'a>, /// Multipart text fields that are sent with the form data as individual fields. If a certain /// endpoint does not support passing JSON body via `payload_json`, this must be used instead. pub fields: Vec<(Cow<'static, str>, Cow<'static, str>)>, @@ -34,7 +34,7 @@ pub struct Multipart { pub payload_json: Option, } -impl Multipart { +impl<'a> Multipart<'a> { pub(crate) fn build_form(self) -> Result
{ let mut multipart = Form::new(); @@ -43,8 +43,8 @@ impl Multipart { multipart = multipart.part("file", upload_file.into_part()?); }, MultipartUpload::Attachments(attachment_files) => { - for file in attachment_files { - multipart = multipart.part(format!("files[{}]", file.id), file.into_part()?); + for (idx, file) in attachment_files.into_iter().enumerate() { + multipart = multipart.part(format!("files[{idx}]"), file.into_part()?); } }, } diff --git a/src/http/ratelimiting.rs b/src/http/ratelimiting.rs index cb6904394c5..1e863d7dfc7 100644 --- a/src/http/ratelimiting.rs +++ b/src/http/ratelimiting.rs @@ -35,21 +35,22 @@ //! //! [Taken from]: https://discord.com/developers/docs/topics/rate-limits#rate-limits -use std::collections::HashMap; +use std::borrow::Cow; use std::fmt; use std::str::{self, FromStr}; use std::sync::Arc; use std::time::SystemTime; +use dashmap::DashMap; use reqwest::header::HeaderMap; use reqwest::{Client, Response, StatusCode}; -use secrecy::{ExposeSecret, SecretString}; -use tokio::sync::{Mutex, RwLock}; +use secrecy::{ExposeSecret as _, Secret}; +use tokio::sync::Mutex; use tokio::time::{sleep, Duration}; -use tracing::{debug, instrument}; +use tracing::debug; pub use super::routing::RatelimitingBucket; -use super::{HttpError, LightMethod, Request}; +use super::{HttpError, LightMethod, Request, Token}; use crate::internal::prelude::*; /// Passed to the [`Ratelimiter::set_ratelimit_callback`] callback. If using Client, that callback @@ -60,7 +61,7 @@ pub struct RatelimitInfo { pub timeout: std::time::Duration, pub limit: i64, pub method: LightMethod, - pub path: String, + pub path: Cow<'static, str>, pub global: bool, } @@ -83,13 +84,11 @@ pub struct RatelimitInfo { /// [`reset`]: Ratelimit::reset pub struct Ratelimiter { client: Client, - global: Arc>, - // When futures is implemented, make tasks clear out their respective entry when the 'reset' - // passes. - routes: Arc>>>>, - token: SecretString, + global: Mutex<()>, + routes: DashMap, + token: Secret, absolute_ratelimits: bool, - ratelimit_callback: Box, + ratelimit_callback: parking_lot::RwLock>, } impl fmt::Debug for Ratelimiter { @@ -110,27 +109,23 @@ impl Ratelimiter { /// /// The bot token must be prefixed with `"Bot "`. The ratelimiter does not prefix it. #[must_use] - pub fn new(client: Client, token: impl Into) -> Self { - Self::_new(client, token.into()) - } - - fn _new(client: Client, token: String) -> Self { + pub fn new(client: Client, token: Arc) -> Self { Self { client, - global: Arc::default(), - routes: Arc::default(), - token: SecretString::new(token), - ratelimit_callback: Box::new(|_| {}), + token: Token::new(token), + global: Mutex::default(), + routes: DashMap::new(), absolute_ratelimits: false, + ratelimit_callback: parking_lot::RwLock::new(Box::new(|_| {})), } } /// Sets a callback to be called when a route is rate limited. pub fn set_ratelimit_callback( - &mut self, + &self, ratelimit_callback: Box, ) { - self.ratelimit_callback = ratelimit_callback; + *self.ratelimit_callback.write() = ratelimit_callback; } // Sets whether absolute ratelimits should be used. @@ -156,14 +151,13 @@ impl Ratelimiter { /// # async fn run() -> Result<(), Box> { /// # let http: Http = unimplemented!(); /// let routes = http.ratelimiter.unwrap().routes(); - /// let reader = routes.read().await; /// /// let channel_id = ChannelId::new(7); /// let route = Route::Channel { /// channel_id, /// }; - /// if let Some(route) = reader.get(&route.ratelimiting_bucket()) { - /// if let Some(reset) = route.lock().await.reset() { + /// if let Some(route) = routes.get(&route.ratelimiting_bucket()) { + /// if let Some(reset) = route.reset() { /// println!("Reset time at: {:?}", reset); /// } /// } @@ -171,14 +165,14 @@ impl Ratelimiter { /// # } /// ``` #[must_use] - pub fn routes(&self) -> Arc>>>> { - Arc::clone(&self.routes) + pub fn routes(&self) -> &DashMap { + &self.routes } /// # Errors /// /// Only error kind that may be returned is [`Error::Http`]. - #[instrument] + #[cfg_attr(feature = "tracing_instrument", instrument)] pub async fn perform(&self, req: Request<'_>) -> Result { loop { // This will block if another thread hit the global ratelimit. @@ -191,10 +185,14 @@ impl Ratelimiter { // - sleep if there is 0 remaining // - then, perform the request let ratelimiting_bucket = req.route.ratelimiting_bucket(); - let bucket = - Arc::clone(self.routes.write().await.entry(ratelimiting_bucket).or_default()); + let delay_time = { + let mut bucket = self.routes.entry(ratelimiting_bucket).or_default(); + bucket.pre_hook(&req, &*self.ratelimit_callback.read()) + }; - bucket.lock().await.pre_hook(&req, &self.ratelimit_callback).await; + if let Some(delay_time) = delay_time { + sleep(delay_time).await; + } let request = req.clone().build(&self.client, self.token.expose_secret(), None)?; let response = self.client.execute(request.build()?).await?; @@ -226,11 +224,11 @@ impl Ratelimiter { "Ratelimited on route {:?} for {:?}s", ratelimiting_bucket, retry_after ); - (self.ratelimit_callback)(RatelimitInfo { + (self.ratelimit_callback.read())(RatelimitInfo { timeout: Duration::from_secs_f64(retry_after), limit: 50, method: req.method, - path: req.route.path().to_string(), + path: req.route.path(), global: true, }); sleep(Duration::from_secs_f64(retry_after)).await; @@ -241,11 +239,23 @@ impl Ratelimiter { }, ) } else { - bucket - .lock() - .await - .post_hook(&response, &req, &self.ratelimit_callback, self.absolute_ratelimits) - .await + let delay_time = if let Some(mut bucket) = self.routes.get_mut(&ratelimiting_bucket) + { + bucket.post_hook( + &response, + &req, + &*self.ratelimit_callback.read(), + self.absolute_ratelimits, + ) + } else { + Ok(None) + }; + + if let Ok(Some(delay_time)) = delay_time { + sleep(delay_time).await; + }; + + delay_time.map(|d| d.is_some()) }; if !redo.unwrap_or(true) { @@ -277,20 +287,21 @@ pub struct Ratelimit { } impl Ratelimit { - #[instrument(skip(ratelimit_callback))] - pub async fn pre_hook( + #[must_use] + #[cfg_attr(feature = "tracing_instrument", instrument(skip(ratelimit_callback)))] + pub fn pre_hook( &mut self, req: &Request<'_>, ratelimit_callback: &(dyn Fn(RatelimitInfo) + Send + Sync), - ) { + ) -> Option { if self.limit() == 0 { - return; + return None; } let Some(reset) = self.reset else { // We're probably in the past. self.remaining = self.limit; - return; + return None; }; let Ok(delay) = reset.duration_since(SystemTime::now()) else { @@ -298,7 +309,7 @@ impl Ratelimit { if self.remaining() != 0 { self.remaining -= 1; } - return; + return None; }; if self.remaining() == 0 { @@ -311,26 +322,28 @@ impl Ratelimit { timeout: delay, limit: self.limit, method: req.method, - path: req.route.path().to_string(), + path: req.route.path(), global: false, }); - sleep(delay).await; - - return; + Some(delay) + } else { + self.remaining -= 1; + None } - - self.remaining -= 1; } - #[instrument(skip(ratelimit_callback))] - pub async fn post_hook( + /// # Errors + /// + /// Errors if unable to parse response headers. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(ratelimit_callback)))] + pub fn post_hook( &mut self, response: &Response, req: &Request<'_>, ratelimit_callback: &(dyn Fn(RatelimitInfo) + Send + Sync), absolute_ratelimits: bool, - ) -> Result { + ) -> Result> { if let Some(limit) = parse_header(response.headers(), "x-ratelimit-limit")? { self.limit = limit; } @@ -356,7 +369,7 @@ impl Ratelimit { } Ok(if response.status() != StatusCode::TOO_MANY_REQUESTS { - false + None } else if let Some(retry_after) = parse_header::(response.headers(), "retry-after")? { debug!( "Ratelimited on route {:?} for {:?}s", @@ -367,41 +380,35 @@ impl Ratelimit { timeout: Duration::from_secs_f64(retry_after), limit: self.limit, method: req.method, - path: req.route.path().to_string(), + path: req.route.path(), global: false, }); - sleep(Duration::from_secs_f64(retry_after)).await; - - true + Some(Duration::from_secs_f64(retry_after)) } else { - false + None }) } /// The total number of requests that can be made in a period of time. - #[inline] #[must_use] pub const fn limit(&self) -> i64 { self.limit } /// The number of requests remaining in the period of time. - #[inline] #[must_use] pub const fn remaining(&self) -> i64 { self.remaining } /// The absolute time in milliseconds when the interval resets. - #[inline] #[must_use] pub const fn reset(&self) -> Option { self.reset } /// The total time in milliseconds when the interval resets. - #[inline] #[must_use] pub const fn reset_after(&self) -> Option { self.reset_after diff --git a/src/http/request.rs b/src/http/request.rs index 0419116fb6b..42714b6b48b 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -8,8 +8,7 @@ use reqwest::header::{ CONTENT_TYPE, USER_AGENT, }; -use reqwest::{Client, RequestBuilder as ReqwestRequestBuilder, Url}; -use tracing::instrument; +use reqwest::{Client, RequestBuilder as ReqwestRequestBuilder}; use super::multipart::Multipart; use super::routing::Route; @@ -17,22 +16,19 @@ use super::{HttpError, LightMethod}; use crate::constants; use crate::internal::prelude::*; -#[deprecated = "use Request directly now"] -pub type RequestBuilder<'a> = Request<'a>; - #[derive(Clone, Debug)] #[must_use] pub struct Request<'a> { pub(super) body: Option>, - pub(super) multipart: Option, + pub(super) multipart: Option>, pub(super) headers: Option, pub(super) method: LightMethod, pub(super) route: Route<'a>, - pub(super) params: Option>, + pub(super) params: Option<&'a [(&'a str, &'a str)]>, } impl<'a> Request<'a> { - pub const fn new(route: Route<'a>, method: LightMethod) -> Self { + pub fn new(route: Route<'a>, method: LightMethod) -> Self { Self { body: None, multipart: None, @@ -48,7 +44,7 @@ impl<'a> Request<'a> { self } - pub fn multipart(mut self, multipart: Option) -> Self { + pub fn multipart(mut self, multipart: Option>) -> Self { self.multipart = multipart; self } @@ -58,19 +54,26 @@ impl<'a> Request<'a> { self } - pub fn params(mut self, params: Option>) -> Self { - self.params = params; + pub fn params(mut self, params: &'a [(&'a str, &'a str)]) -> Self { + if params.is_empty() { + self.params = None; + } else { + self.params = Some(params); + } self } - #[instrument(skip(token))] + /// # Errors + /// + /// Errors if the given proxy URL is invalid, or the token cannot be parsed into a HTTP header. + #[cfg_attr(feature = "tracing_instrument", instrument(skip(token)))] pub fn build( self, client: &Client, token: &str, proxy: Option<&str>, ) -> Result { - let mut path = self.route.path().to_string(); + let mut path = self.route.path().into_owned(); if let Some(proxy) = proxy { // trim_end_matches to prevent double slashes after the domain @@ -84,8 +87,7 @@ impl<'a> Request<'a> { } } - let mut builder = client - .request(self.method.reqwest_method(), Url::parse(&path).map_err(HttpError::Url)?); + let mut builder = client.request(self.method.reqwest_method(), path); let mut headers = self.headers.unwrap_or_default(); headers.insert(USER_AGENT, HeaderValue::from_static(constants::USER_AGENT)); @@ -137,12 +139,7 @@ impl<'a> Request<'a> { } #[must_use] - pub fn params_ref(&self) -> Option<&[(&'static str, String)]> { - self.params.as_deref() - } - - #[must_use] - pub fn params_mut(&mut self) -> Option<&mut [(&'static str, String)]> { - self.params.as_deref_mut() + pub fn params_ref(&self) -> Option<&'a [(&'a str, &'a str)]> { + self.params } } diff --git a/src/http/routing.rs b/src/http/routing.rs index 13e047a47be..6a66e26ca8b 100644 --- a/src/http/routing.rs +++ b/src/http/routing.rs @@ -1,12 +1,10 @@ use std::borrow::Cow; -use std::mem::Discriminant; -use std::num::NonZeroU64; use crate::model::id::*; /// Used to group requests together for ratelimiting. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub struct RatelimitingBucket(Option<(std::mem::Discriminant>, Option)>); +pub struct RatelimitingBucket(Option<(RouteKind, Option)>); impl RatelimitingBucket { #[must_use] @@ -18,7 +16,7 @@ impl RatelimitingBucket { enum RatelimitingKind { /// Requests with the same path and major parameter (usually an Id) should be grouped together /// for ratelimiting. - PathAndId(NonZeroU64), + PathAndId(GenericId), /// Requests with the same path should be ratelimited together. Path, } @@ -41,7 +39,20 @@ macro_rules! routes { )+ } + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + enum RouteKind { + $($name,)+ + } + impl<$lt> Route<$lt> { + fn kind(&self) -> RouteKind { + match self { + $( + Self::$name {..} => RouteKind::$name, + )+ + } + } + #[must_use] pub fn path(self) -> Cow<'static, str> { match self { @@ -60,20 +71,12 @@ macro_rules! routes { )+ }; - // This avoids adding a lifetime on RatelimitingBucket and causing lifetime infection - // SAFETY: std::mem::discriminant erases lifetimes. - let discriminant = unsafe { - std::mem::transmute::>, Discriminant>>( - std::mem::discriminant(self), - ) - }; - RatelimitingBucket(ratelimiting_kind.map(|r| { let id = match r { RatelimitingKind::PathAndId(id) => Some(id), RatelimitingKind::Path => None, }; - (discriminant, id) + (self.kind(), id) })) } @@ -88,111 +91,111 @@ macro_rules! routes { routes! ('a, { Channel { channel_id: ChannelId }, api!("/channels/{}", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelInvites { channel_id: ChannelId }, api!("/channels/{}/invites", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelMessage { channel_id: ChannelId, message_id: MessageId }, api!("/channels/{}/messages/{}", channel_id, message_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelMessageCrosspost { channel_id: ChannelId, message_id: MessageId }, api!("/channels/{}/messages/{}/crosspost", channel_id, message_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelMessageReaction { channel_id: ChannelId, message_id: MessageId, user_id: UserId, reaction: &'a str }, api!("/channels/{}/messages/{}/reactions/{}/{}", channel_id, message_id, reaction, user_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelMessageReactionMe { channel_id: ChannelId, message_id: MessageId, reaction: &'a str }, api!("/channels/{}/messages/{}/reactions/{}/@me", channel_id, message_id, reaction), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelMessageReactionEmoji { channel_id: ChannelId, message_id: MessageId, reaction: &'a str }, api!("/channels/{}/messages/{}/reactions/{}", channel_id, message_id, reaction), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelMessageReactions { channel_id: ChannelId, message_id: MessageId }, api!("/channels/{}/messages/{}/reactions", channel_id, message_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelMessages { channel_id: ChannelId }, api!("/channels/{}/messages", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelMessagesBulkDelete { channel_id: ChannelId }, api!("/channels/{}/messages/bulk-delete", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelFollowNews { channel_id: ChannelId }, api!("/channels/{}/followers", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelPermission { channel_id: ChannelId, target_id: TargetId }, api!("/channels/{}/permissions/{}", channel_id, target_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelPin { channel_id: ChannelId, message_id: MessageId }, api!("/channels/{}/pins/{}", channel_id, message_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelPins { channel_id: ChannelId }, api!("/channels/{}/pins", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelTyping { channel_id: ChannelId }, api!("/channels/{}/typing", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelWebhooks { channel_id: ChannelId }, api!("/channels/{}/webhooks", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelMessageThreads { channel_id: ChannelId, message_id: MessageId }, api!("/channels/{}/messages/{}/threads", channel_id, message_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelThreads { channel_id: ChannelId }, api!("/channels/{}/threads", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelForumPosts { channel_id: ChannelId }, api!("/channels/{}/threads", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelThreadMember { channel_id: ChannelId, user_id: UserId }, api!("/channels/{}/thread-members/{}", channel_id, user_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelThreadMemberMe { channel_id: ChannelId }, api!("/channels/{}/thread-members/@me", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelThreadMembers { channel_id: ChannelId }, api!("/channels/{}/thread-members", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelArchivedPublicThreads { channel_id: ChannelId }, api!("/channels/{}/threads/archived/public", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelArchivedPrivateThreads { channel_id: ChannelId }, api!("/channels/{}/threads/archived/private", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelJoinedPrivateThreads { channel_id: ChannelId }, api!("/channels/{}/users/@me/threads/archived/private", channel_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelPollGetAnswerVoters { channel_id: ChannelId, message_id: MessageId, answer_id: AnswerId }, api!("/channels/{}/polls/{}/answers/{}", channel_id, message_id, answer_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); ChannelPollExpire { channel_id: ChannelId, message_id: MessageId }, api!("/channels/{}/polls/{}/expire", channel_id, message_id), - Some(RatelimitingKind::PathAndId(channel_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); Gateway, api!("/gateway"), @@ -204,151 +207,151 @@ routes! ('a, { Guild { guild_id: GuildId }, api!("/guilds/{}", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildAuditLogs { guild_id: GuildId }, api!("/guilds/{}/audit-logs", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildAutomodRule { guild_id: GuildId, rule_id: RuleId }, api!("/guilds/{}/auto-moderation/rules/{}", guild_id, rule_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildAutomodRules { guild_id: GuildId }, api!("/guilds/{}/auto-moderation/rules", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildBan { guild_id: GuildId, user_id: UserId }, api!("/guilds/{}/bans/{}", guild_id, user_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildBulkBan { guild_id: GuildId }, api!("/guilds/{}/bulk-ban", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildBans { guild_id: GuildId }, api!("/guilds/{}/bans", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildChannels { guild_id: GuildId }, api!("/guilds/{}/channels", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildWidget { guild_id: GuildId }, api!("/guilds/{}/widget", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildPreview { guild_id: GuildId }, api!("/guilds/{}/preview", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildEmojis { guild_id: GuildId }, api!("/guilds/{}/emojis", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildEmoji { guild_id: GuildId, emoji_id: EmojiId }, api!("/guilds/{}/emojis/{}", guild_id, emoji_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildIntegration { guild_id: GuildId, integration_id: IntegrationId }, api!("/guilds/{}/integrations/{}", guild_id, integration_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildIntegrationSync { guild_id: GuildId, integration_id: IntegrationId }, api!("/guilds/{}/integrations/{}/sync", guild_id, integration_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildIntegrations { guild_id: GuildId }, api!("/guilds/{}/integrations", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildInvites { guild_id: GuildId }, api!("/guilds/{}/invites", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildMember { guild_id: GuildId, user_id: UserId }, api!("/guilds/{}/members/{}", guild_id, user_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildMemberRole { guild_id: GuildId, user_id: UserId, role_id: RoleId }, api!("/guilds/{}/members/{}/roles/{}", guild_id, user_id, role_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildMembers { guild_id: GuildId }, api!("/guilds/{}/members", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildMembersSearch { guild_id: GuildId }, api!("/guilds/{}/members/search", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildMemberMe { guild_id: GuildId }, api!("/guilds/{}/members/@me", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildMfa { guild_id: GuildId }, api!("/guilds/{}/mfa", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildPrune { guild_id: GuildId }, api!("/guilds/{}/prune", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildRegions { guild_id: GuildId }, api!("/guilds/{}/regions", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildRole { guild_id: GuildId, role_id: RoleId }, api!("/guilds/{}/roles/{}", guild_id, role_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildRoles { guild_id: GuildId }, api!("/guilds/{}/roles", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildScheduledEvent { guild_id: GuildId, event_id: ScheduledEventId }, api!("/guilds/{}/scheduled-events/{}", guild_id, event_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildScheduledEvents { guild_id: GuildId }, api!("/guilds/{}/scheduled-events", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildScheduledEventUsers { guild_id: GuildId, event_id: ScheduledEventId }, api!("/guilds/{}/scheduled-events/{}/users", guild_id, event_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildSticker { guild_id: GuildId, sticker_id: StickerId }, api!("/guilds/{}/stickers/{}", guild_id, sticker_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildStickers { guild_id: GuildId }, api!("/guilds/{}/stickers", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildVanityUrl { guild_id: GuildId }, api!("/guilds/{}/vanity-url", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildVoiceStates { guild_id: GuildId, user_id: UserId }, api!("/guilds/{}/voice-states/{}", guild_id, user_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildVoiceStateMe { guild_id: GuildId }, api!("/guilds/{}/voice-states/@me", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildWebhooks { guild_id: GuildId }, api!("/guilds/{}/webhooks", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildWelcomeScreen { guild_id: GuildId }, api!("/guilds/{}/welcome-screen", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); GuildThreadsActive { guild_id: GuildId }, api!("/guilds/{}/threads/active", guild_id), - Some(RatelimitingKind::PathAndId(guild_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); Guilds, api!("/guilds"), @@ -416,67 +419,67 @@ routes! ('a, { Webhook { webhook_id: WebhookId }, api!("/webhooks/{}", webhook_id), - Some(RatelimitingKind::PathAndId(webhook_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(webhook_id.get()))); WebhookWithToken { webhook_id: WebhookId, token: &'a str }, api!("/webhooks/{}/{}", webhook_id, token), - Some(RatelimitingKind::PathAndId(webhook_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(webhook_id.get()))); WebhookMessage { webhook_id: WebhookId, token: &'a str, message_id: MessageId }, api!("/webhooks/{}/{}/messages/{}", webhook_id, token, message_id), - Some(RatelimitingKind::PathAndId(webhook_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(webhook_id.get()))); WebhookOriginalInteractionResponse { application_id: ApplicationId, token: &'a str }, api!("/webhooks/{}/{}/messages/@original", application_id, token), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); WebhookFollowupMessage { application_id: ApplicationId, token: &'a str, message_id: MessageId }, api!("/webhooks/{}/{}/messages/{}", application_id, token, message_id), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); WebhookFollowupMessages { application_id: ApplicationId, token: &'a str }, api!("/webhooks/{}/{}", application_id, token), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); InteractionResponse { interaction_id: InteractionId, token: &'a str }, api!("/interactions/{}/{}/callback", interaction_id, token), - Some(RatelimitingKind::PathAndId(interaction_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(interaction_id.get()))); Command { application_id: ApplicationId, command_id: CommandId }, api!("/applications/{}/commands/{}", application_id, command_id), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); Commands { application_id: ApplicationId }, api!("/applications/{}/commands", application_id), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); GuildCommand { application_id: ApplicationId, guild_id: GuildId, command_id: CommandId }, api!("/applications/{}/guilds/{}/commands/{}", application_id, guild_id, command_id), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); GuildCommandPermissions { application_id: ApplicationId, guild_id: GuildId, command_id: CommandId }, api!("/applications/{}/guilds/{}/commands/{}/permissions", application_id, guild_id, command_id), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); GuildCommands { application_id: ApplicationId, guild_id: GuildId }, api!("/applications/{}/guilds/{}/commands", application_id, guild_id), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); GuildCommandsPermissions { application_id: ApplicationId, guild_id: GuildId }, api!("/applications/{}/guilds/{}/commands/permissions", application_id, guild_id), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); Skus { application_id: ApplicationId }, api!("/applications/{}/skus", application_id), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); Entitlement { application_id: ApplicationId, entitlement_id: EntitlementId }, api!("/applications/{}/entitlements/{}", application_id, entitlement_id), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); Entitlements { application_id: ApplicationId }, api!("/applications/{}/entitlements", application_id), - Some(RatelimitingKind::PathAndId(application_id.into())); + Some(RatelimitingKind::PathAndId(GenericId::new(application_id.get()))); StageInstances, api!("/stage-instances"), diff --git a/src/internal/macros.rs b/src/internal/macros.rs index 9b437882acd..c683c2d992b 100644 --- a/src/internal/macros.rs +++ b/src/internal/macros.rs @@ -86,43 +86,39 @@ macro_rules! status { macro_rules! enum_number { ( $(#[$outer:meta])* + $(#[ = $default:literal])? $vis:vis enum $Enum:ident { $( $(#[doc = $doc:literal])* $(#[cfg $($cfg:tt)*])? - $(#[default $($dummy:tt)?])? $Variant:ident = $value:literal, )* _ => Unknown($T:ty), } ) => { $(#[$outer])* - $vis enum $Enum { + $vis struct $Enum (pub $T); + + $( + impl Default for $Enum { + fn default() -> Self { + Self($default) + } + } + )? + + #[allow(non_snake_case, non_upper_case_globals)] + impl $Enum { $( $(#[doc = $doc])* $(#[cfg $($cfg)*])? - $(#[default $($dummy:tt)?])? - $Variant, + $vis const $Variant: Self = Self($value); )* - /// Variant value is unknown. - Unknown($T), - } - impl From<$T> for $Enum { - fn from(value: $T) -> Self { - match value { - $($(#[cfg $($cfg)*])? $value => Self::$Variant,)* - unknown => Self::Unknown(unknown), - } - } - } - - impl From<$Enum> for $T { - fn from(value: $Enum) -> Self { - match value { - $($(#[cfg $($cfg)*])? $Enum::$Variant => $value,)* - $Enum::Unknown(unknown) => unknown, - } + /// Variant value is unknown. + #[must_use] + $vis const fn Unknown(val: $T) -> Self { + Self(val) } } }; @@ -176,13 +172,14 @@ macro_rules! bitflags { #[cfg(test)] mod tests { - use crate::json::{assert_json, json}; + use serde_json::json; + + use crate::model::assert_json; #[test] fn enum_number() { enum_number! { #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] - #[serde(from = "u8", into = "u8")] pub enum T { /// AAA A = 1, diff --git a/src/internal/mod.rs b/src/internal/mod.rs index d7ba077c2fe..6d5cb5b8733 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -4,3 +4,5 @@ pub mod macros; pub mod prelude; pub mod tokio; + +pub mod utils; diff --git a/src/internal/prelude.rs b/src/internal/prelude.rs index df676dad5cb..53593b74d36 100644 --- a/src/internal/prelude.rs +++ b/src/internal/prelude.rs @@ -4,5 +4,11 @@ pub use std::result::Result as StdResult; +pub use extract_map::{ExtractKey, ExtractMap, LendingIterator}; +pub use serde_json::Value; +pub use small_fixed_array::{FixedArray, FixedString, TruncatingInto}; + +pub(crate) use super::utils::join_to_string; pub use crate::error::{Error, Result}; -pub use crate::json::{JsonMap, Value}; + +pub type JsonMap = serde_json::Map; diff --git a/src/internal/utils.rs b/src/internal/utils.rs new file mode 100644 index 00000000000..53e35b9391c --- /dev/null +++ b/src/internal/utils.rs @@ -0,0 +1,26 @@ +use std::fmt::Write; + +pub(crate) fn join_to_string( + sep: impl std::fmt::Display, + iter: impl IntoIterator, +) -> String { + let mut buf = String::new(); + for item in iter { + write!(buf, "{item}{sep}").unwrap(); + } + + buf.truncate(buf.len() - 1); + buf +} + +// Required because of https://github.com/Crazytieguy/gat-lending-iterator/issues/31 +macro_rules! lending_for_each { + ($iter:expr, |$item:ident| $body:expr ) => { + let mut __iter = $iter; + while let Some(mut $item) = __iter.next() { + $body + } + }; +} + +pub(crate) use lending_for_each; diff --git a/src/json.rs b/src/json.rs index 0b3701e35c6..f7a5a345afd 100644 --- a/src/json.rs +++ b/src/json.rs @@ -63,7 +63,7 @@ where /// If the `simd_json` feature is enabled, this function turns its argument into `Cow::Owned` /// before deserializing from it. In other words, passing in a `&str` will result in a clone. #[allow(clippy::missing_errors_doc)] -pub fn from_str<'a, T>(s: impl Into>) -> Result +pub fn from_str<'a, T>(s: impl Into>) -> Result where T: DeserializeOwned, { diff --git a/src/lib.rs b/src/lib.rs index 2d6fc5cf578..f23e812d39b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,7 @@ //! [gateway docs]: crate::gateway #![doc(html_root_url = "https://docs.rs/serenity/*")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] #![warn( unused, rust_2018_idioms, @@ -58,30 +59,23 @@ clippy::non_ascii_literal, clippy::fallible_impl_from, clippy::let_underscore_must_use, - clippy::format_collect, clippy::format_push_string, clippy::pedantic )] #![allow( - // Allowed to avoid breaking changes. - clippy::module_name_repetitions, - clippy::struct_excessive_bools, - clippy::unused_self, // Allowed as they are too pedantic clippy::cast_possible_truncation, + clippy::module_name_repetitions, clippy::unreadable_literal, clippy::cast_possible_wrap, clippy::wildcard_imports, clippy::cast_sign_loss, clippy::too_many_lines, clippy::doc_markdown, - clippy::cast_lossless, - clippy::redundant_closure_for_method_calls, - // Covered by other lints - clippy::missing_panics_doc, // clippy::unwrap_used + clippy::missing_panics_doc, + clippy::doc_link_with_quotes, )] #![cfg_attr(test, allow(clippy::unwrap_used))] -#![type_length_limit = "3294819"] // needed so ShardRunner::run compiles with instrument. #[macro_use] extern crate serde; @@ -90,7 +84,6 @@ extern crate serde; mod internal; pub mod constants; -pub mod json; pub mod model; pub mod prelude; @@ -115,33 +108,13 @@ pub mod utils; mod error; -// For the procedural macros in `command_attr`. -pub use async_trait::async_trait; -pub use futures; -pub use futures::future::FutureExt; -#[cfg(feature = "standard_framework")] -#[doc(hidden)] -pub use static_assertions; - #[cfg(all(feature = "client", feature = "gateway"))] pub use crate::client::Client; pub use crate::error::{Error, Result}; -#[cfg(feature = "absolute_ratelimits")] -compile_error!( - "The absolute_ratelimits feature has been removed.\n\ - Configure absolute ratelimits via Ratelimiter::set_absolute_ratelimits.\n\ - You can set the Ratelimiter of Http via HttpBuilder::ratelimiter." -); - /// Special module that re-exports most public items from this crate. /// /// Useful, because you don't have to remember the full paths of serenity items. -/// -/// Not exported: -/// - [`crate::json`]: it's a general-purpose JSON wrapper, not intrinsic to serenity -/// - [`crate::framework::standard`]: has many standard_framework-specific items that may collide -/// with items from the rest of serenity pub mod all { #[cfg(feature = "builder")] #[doc(no_inline)] @@ -185,3 +158,7 @@ pub mod all { *, }; } + +// Re-exports of crates used internally which are already publically exposed. +pub use async_trait::async_trait; +pub use {futures, nonmax, small_fixed_array}; diff --git a/src/model/application/command.rs b/src/model/application/command.rs index 7d2cd91e4f7..78ecdc3b9bb 100644 --- a/src/model/application/command.rs +++ b/src/model/application/command.rs @@ -2,12 +2,12 @@ use std::collections::HashMap; use serde::Serialize; -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] use super::{InstallationContext, InteractionContext}; #[cfg(feature = "model")] -use crate::builder::{Builder, CreateCommand}; +use crate::builder::CreateCommand; #[cfg(feature = "model")] -use crate::http::{CacheHttp, Http}; +use crate::http::Http; use crate::internal::prelude::*; use crate::model::channel::ChannelType; use crate::model::id::{ @@ -24,8 +24,9 @@ use crate::model::Permissions; /// The base command model that belongs to an application. /// /// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct Command { /// The command Id. @@ -40,13 +41,13 @@ pub struct Command { /// **Note**: It may only be present if it is received through the gateway. pub guild_id: Option, /// The command name. - pub name: String, + pub name: FixedString, /// The localized command name of the selected locale. /// /// If the name is localized, either this field or [`Self::name_localizations`] is set, /// depending on which endpoint this data was retrieved from /// ([source](https://discord.com/developers/docs/interactions/application-commands#retrieving-localized-commands)). - pub name_localized: Option, + pub name_localized: Option>, /// All localized command names. /// /// If the name is localized, either this field or [`Self::name_localized`] is set, depending @@ -54,13 +55,13 @@ pub struct Command { /// ([source](https://discord.com/developers/docs/interactions/application-commands#retrieving-localized-commands)). pub name_localizations: Option>, /// The command description. - pub description: String, + pub description: FixedString, /// The localized command description of the selected locale. /// /// If the description is localized, either this field or [`Self::description_localizations`] /// is set, depending on which endpoint this data was retrieved from /// ([source](https://discord.com/developers/docs/interactions/application-commands#retrieving-localized-commands)). - pub description_localized: Option, + pub description_localized: Option>, /// All localized command descriptions. /// /// If the description is localized, either this field or [`Self::description_localized`] is @@ -69,16 +70,13 @@ pub struct Command { pub description_localizations: Option>, /// The parameters for the command. #[serde(default)] - pub options: Vec, + pub options: FixedArray, /// The default permissions required to execute the command. pub default_member_permissions: Option, /// Indicates whether the command is available in DMs with the app, only for globally-scoped /// commands. By default, commands are visible. #[serde(default)] - #[cfg_attr( - all(not(ignore_serenity_deprecated), feature = "unstable_discord_api"), - deprecated = "Use Command::contexts" - )] + #[cfg(not(feature = "unstable"))] pub dm_permission: Option, /// Indicates whether the command is [age-restricted](https://discord.com/developers/docs/interactions/application-commands#agerestricted-commands), /// defaults to false. @@ -87,13 +85,13 @@ pub struct Command { /// Installation context(s) where the command is available, only for globally-scoped commands. /// /// Defaults to [`InstallationContext::Guild`] - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] #[serde(default)] pub integration_types: Vec, /// Interaction context(s) where the command can be used, only for globally-scoped commands. /// /// By default, all interaction context types are included. - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] pub contexts: Option>, /// An autoincremented version identifier updated during substantial record changes. pub version: CommandVersionId, @@ -156,11 +154,8 @@ impl Command { /// See [`CreateCommand::execute`] for a list of possible errors. /// /// [`InteractionCreate`]: crate::client::EventHandler::interaction_create - pub async fn create_global_command( - cache_http: impl CacheHttp, - builder: CreateCommand, - ) -> Result { - builder.execute(cache_http, (None, None)).await + pub async fn create_global_command(http: &Http, builder: CreateCommand<'_>) -> Result { + builder.execute(http, None, None).await } /// Override all global application commands. @@ -169,10 +164,10 @@ impl Command { /// /// Returns the same errors as [`Self::create_global_command`]. pub async fn set_global_commands( - http: impl AsRef, - commands: Vec, + http: &Http, + commands: &[CreateCommand<'_>], ) -> Result> { - http.as_ref().create_global_commands(&commands).await + http.create_global_commands(&commands).await } /// Edit a global command, given its Id. @@ -181,11 +176,11 @@ impl Command { /// /// See [`CreateCommand::execute`] for a list of possible errors. pub async fn edit_global_command( - cache_http: impl CacheHttp, + http: &Http, command_id: CommandId, - builder: CreateCommand, + builder: CreateCommand<'_>, ) -> Result { - builder.execute(cache_http, (None, Some(command_id))).await + builder.execute(http, None, Some(command_id)).await } /// Gets all global commands. @@ -193,8 +188,8 @@ impl Command { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_global_commands(http: impl AsRef) -> Result> { - http.as_ref().get_global_commands().await + pub async fn get_global_commands(http: &Http) -> Result> { + http.get_global_commands().await } /// Gets all global commands with localizations. @@ -202,10 +197,8 @@ impl Command { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_global_commands_with_localizations( - http: impl AsRef, - ) -> Result> { - http.as_ref().get_global_commands_with_localizations().await + pub async fn get_global_commands_with_localizations(http: &Http) -> Result> { + http.get_global_commands_with_localizations().await } /// Gets a global command by its Id. @@ -213,11 +206,8 @@ impl Command { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_global_command( - http: impl AsRef, - command_id: CommandId, - ) -> Result { - http.as_ref().get_global_command(command_id).await + pub async fn get_global_command(http: &Http, command_id: CommandId) -> Result { + http.get_global_command(command_id).await } /// Deletes a global command by its Id. @@ -225,11 +215,8 @@ impl Command { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn delete_global_command( - http: impl AsRef, - command_id: CommandId, - ) -> Result<()> { - http.as_ref().delete_global_command(command_id).await + pub async fn delete_global_command(http: &Http, command_id: CommandId) -> Result<()> { + http.delete_global_command(command_id).await } } @@ -239,7 +226,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum CommandType { ChatInput = 1, @@ -253,22 +239,22 @@ enum_number! { /// /// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct CommandOption { /// The option type. #[serde(rename = "type")] pub kind: CommandOptionType, /// The option name. - pub name: String, + pub name: FixedString, /// Localizations of the option name with locale as the key #[serde(skip_serializing_if = "Option::is_none")] - pub name_localizations: Option>, + pub name_localizations: Option, FixedString>>, /// The option description. - pub description: String, + pub description: FixedString, /// Localizations of the option description with locale as the key #[serde(skip_serializing_if = "Option::is_none")] - pub description_localizations: Option>, + pub description_localizations: Option, FixedString>>, /// Whether the parameter is optional or required. #[serde(default)] pub required: bool, @@ -279,7 +265,7 @@ pub struct CommandOption { /// [`String`]: CommandOptionType::String /// [`Integer`]: CommandOptionType::Integer #[serde(default)] - pub choices: Vec, + pub choices: FixedArray, /// The nested options. /// /// **Note**: Only available for [`SubCommand`] or [`SubCommandGroup`]. @@ -292,7 +278,7 @@ pub struct CommandOption { /// /// [`Channel`]: CommandOptionType::Channel #[serde(default)] - pub channel_types: Vec, + pub channel_types: FixedArray, /// Minimum permitted value for Integer or Number options #[serde(default)] pub min_value: Option, @@ -315,7 +301,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum CommandOptionType { SubCommand = 1, @@ -341,10 +326,10 @@ enum_number! { #[non_exhaustive] pub struct CommandOptionChoice { /// The choice name. - pub name: String, + pub name: FixedString, /// Localizations of the choice name, with locale as key #[serde(skip_serializing_if = "Option::is_none")] - pub name_localizations: Option>, + pub name_localizations: Option>, /// The choice value. pub value: Value, } @@ -363,7 +348,7 @@ pub struct CommandPermissions { /// The id of the guild. pub guild_id: GuildId, /// The permissions for the command in the guild. - pub permissions: Vec, + pub permissions: FixedArray, } /// The [`CommandPermission`] data. @@ -388,7 +373,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permission-type). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum CommandPermissionType { Role = 1, diff --git a/src/model/application/command_interaction.rs b/src/model/application/command_interaction.rs index e90d95620d3..8232542e01a 100644 --- a/src/model/application/command_interaction.rs +++ b/src/model/application/command_interaction.rs @@ -4,11 +4,10 @@ use serde::de::{Deserializer, Error as DeError}; use serde::ser::{Error as _, Serializer}; use serde::{Deserialize, Serialize}; -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] use super::{AuthorizingIntegrationOwners, InteractionContext}; #[cfg(feature = "model")] use crate::builder::{ - Builder, CreateInteractionResponse, CreateInteractionResponseFollowup, CreateInteractionResponseMessage, @@ -17,9 +16,9 @@ use crate::builder::{ #[cfg(feature = "collector")] use crate::client::Context; #[cfg(feature = "model")] -use crate::http::{CacheHttp, Http}; +use crate::http::Http; use crate::internal::prelude::*; -use crate::json::{self, JsonError}; +use crate::internal::utils::lending_for_each; use crate::model::application::{CommandOptionType, CommandType}; use crate::model::channel::{Attachment, Message, PartialChannel}; use crate::model::guild::{Member, PartialMember, Role}; @@ -72,24 +71,24 @@ pub struct CommandInteraction { #[serde(default)] pub user: User, /// A continuation token for responding to the interaction. - pub token: String, + pub token: FixedString, /// Always `1`. pub version: u8, /// Permissions the app or bot has within the channel the interaction was sent from. // TODO(next): This is now always serialized. pub app_permissions: Option, /// The selected language of the invoking user. - pub locale: String, + pub locale: FixedString, /// The guild's preferred locale. - pub guild_locale: Option, + pub guild_locale: Option, /// For monetized applications, any entitlements of the invoking user. pub entitlements: Vec, /// The owners of the applications that authorized the interaction, such as a guild or user. #[serde(default)] - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] pub authorizing_integration_owners: AuthorizingIntegrationOwners, /// The context where the interaction was triggered from. - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] pub context: Option, } @@ -100,8 +99,8 @@ impl CommandInteraction { /// # Errors /// /// Returns an [`Error::Http`] if there is no interaction response. - pub async fn get_response(&self, http: impl AsRef) -> Result { - http.as_ref().get_original_interaction_response(&self.token).await + pub async fn get_response(&self, http: &Http) -> Result { + http.get_original_interaction_response(&self.token).await } /// Creates a response to the interaction received. @@ -115,10 +114,10 @@ impl CommandInteraction { /// deserializing the API response. pub async fn create_response( &self, - cache_http: impl CacheHttp, - builder: CreateInteractionResponse, + http: &Http, + builder: CreateInteractionResponse<'_>, ) -> Result<()> { - builder.execute(cache_http, (self.id, &self.token)).await + builder.execute(http, self.id, &self.token).await } /// Edits the initial interaction response. @@ -132,10 +131,10 @@ impl CommandInteraction { /// deserializing the API response. pub async fn edit_response( &self, - cache_http: impl CacheHttp, - builder: EditInteractionResponse, + http: &Http, + builder: EditInteractionResponse<'_>, ) -> Result { - builder.execute(cache_http, &self.token).await + builder.execute(http, &self.token).await } /// Deletes the initial interaction response. @@ -146,8 +145,8 @@ impl CommandInteraction { /// /// May return [`Error::Http`] if the API returns an error. Such as if the response was already /// deleted. - pub async fn delete_response(&self, http: impl AsRef) -> Result<()> { - http.as_ref().delete_original_interaction_response(&self.token).await + pub async fn delete_response(&self, http: &Http) -> Result<()> { + http.delete_original_interaction_response(&self.token).await } /// Creates a followup response to the response sent. @@ -161,10 +160,10 @@ impl CommandInteraction { /// response. pub async fn create_followup( &self, - cache_http: impl CacheHttp, - builder: CreateInteractionResponseFollowup, + http: &Http, + builder: CreateInteractionResponseFollowup<'_>, ) -> Result { - builder.execute(cache_http, (None, &self.token)).await + builder.execute(http, None, &self.token).await } /// Edits a followup response to the response sent. @@ -178,11 +177,11 @@ impl CommandInteraction { /// response. pub async fn edit_followup( &self, - cache_http: impl CacheHttp, - message_id: impl Into, - builder: CreateInteractionResponseFollowup, + http: &Http, + message_id: MessageId, + builder: CreateInteractionResponseFollowup<'_>, ) -> Result { - builder.execute(cache_http, (Some(message_id.into()), &self.token)).await + builder.execute(http, Some(message_id), &self.token).await } /// Deletes a followup message. @@ -191,12 +190,8 @@ impl CommandInteraction { /// /// May return [`Error::Http`] if the API returns an error. Such as if the response was already /// deleted. - pub async fn delete_followup>( - &self, - http: impl AsRef, - message_id: M, - ) -> Result<()> { - http.as_ref().delete_followup_message(&self.token, message_id.into()).await + pub async fn delete_followup(&self, http: &Http, message_id: MessageId) -> Result<()> { + http.delete_followup_message(&self.token, message_id).await } /// Gets a followup message. @@ -205,12 +200,8 @@ impl CommandInteraction { /// /// May return [`Error::Http`] if the API returns an error. Such as if the response was /// deleted. - pub async fn get_followup>( - &self, - http: impl AsRef, - message_id: M, - ) -> Result { - http.as_ref().get_followup_message(&self.token, message_id.into()).await + pub async fn get_followup(&self, http: &Http, message_id: MessageId) -> Result { + http.get_followup_message(&self.token, message_id).await } /// Helper function to defer an interaction. @@ -219,9 +210,9 @@ impl CommandInteraction { /// /// Returns an [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is /// an error in deserializing the API response. - pub async fn defer(&self, cache_http: impl CacheHttp) -> Result<()> { + pub async fn defer(&self, http: &Http) -> Result<()> { let builder = CreateInteractionResponse::Defer(CreateInteractionResponseMessage::default()); - self.create_response(cache_http, builder).await + self.create_response(http, builder).await } /// Helper function to defer an interaction ephemerally @@ -230,11 +221,11 @@ impl CommandInteraction { /// /// May also return an [`Error::Http`] if the API returns an error, or an [`Error::Json`] if /// there is an error in deserializing the API response. - pub async fn defer_ephemeral(&self, cache_http: impl CacheHttp) -> Result<()> { + pub async fn defer_ephemeral(&self, http: &Http) -> Result<()> { let builder = CreateInteractionResponse::Defer( CreateInteractionResponseMessage::new().ephemeral(true), ); - self.create_response(cache_http, builder).await + self.create_response(http, builder).await } /// See [`CreateQuickModal`]. @@ -246,7 +237,7 @@ impl CommandInteraction { pub async fn quick_modal( &self, ctx: &Context, - builder: CreateQuickModal, + builder: CreateQuickModal<'_>, ) -> Result> { builder.execute(ctx, self.id, &self.token).await } @@ -263,7 +254,9 @@ impl<'de> Deserialize<'de> for CommandInteraction { // If `member` is present, `user` wasn't sent and is still filled with default data interaction.user = member.user.clone(); } - interaction.data.resolved.roles.values_mut().for_each(|r| r.guild_id = guild_id); + + let iter = interaction.data.resolved.roles.iter_mut(); + lending_for_each!(iter, |r| r.guild_id = guild_id); } Ok(interaction) } @@ -286,7 +279,7 @@ pub struct CommandData { /// The Id of the invoked command. pub id: CommandId, /// The name of the invoked command. - pub name: String, + pub name: FixedString, /// The application command type of the triggered application command. #[serde(rename = "type")] pub kind: CommandType, @@ -294,7 +287,7 @@ pub struct CommandData { #[serde(default)] pub resolved: CommandDataResolved, #[serde(default)] - pub options: Vec, + pub options: FixedArray, /// The Id of the guild the command is registered to. #[serde(skip_serializing_if = "Option::is_none")] pub guild_id: Option, @@ -349,10 +342,10 @@ impl CommandData { for opt in opts { let value = match &opt.value { CommandDataOptionValue::SubCommand(opts) => { - ResolvedValue::SubCommand(resolve_options(opts, resolved)) + ResolvedValue::SubCommand(resolve_options(opts, resolved).trunc_into()) }, CommandDataOptionValue::SubCommandGroup(opts) => { - ResolvedValue::SubCommandGroup(resolve_options(opts, resolved)) + ResolvedValue::SubCommandGroup(resolve_options(opts, resolved).trunc_into()) }, CommandDataOptionValue::Autocomplete { kind, @@ -457,8 +450,8 @@ pub enum ResolvedValue<'a> { Integer(i64), Number(f64), String(&'a str), - SubCommand(Vec>), - SubCommandGroup(Vec>), + SubCommand(FixedArray>), + SubCommandGroup(FixedArray>), Attachment(&'a Attachment), Channel(&'a PartialChannel), Role(&'a Role), @@ -496,23 +489,44 @@ pub enum ResolvedTarget<'a> { #[non_exhaustive] pub struct CommandDataResolved { /// The resolved users. - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub users: HashMap, + #[serde( + default, + skip_serializing_if = "ExtractMap::is_empty", + serialize_with = "extract_map::serialize_as_map" + )] + pub users: ExtractMap, /// The resolved partial members. + // Cannot use ExtractMap, as PartialMember does not always store an ID. #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub members: HashMap, /// The resolved roles. - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub roles: HashMap, + #[serde( + default, + skip_serializing_if = "ExtractMap::is_empty", + serialize_with = "extract_map::serialize_as_map" + )] + pub roles: ExtractMap, /// The resolved partial channels. - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub channels: HashMap, + #[serde( + default, + skip_serializing_if = "ExtractMap::is_empty", + serialize_with = "extract_map::serialize_as_map" + )] + pub channels: ExtractMap, /// The resolved messages. - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub messages: HashMap, + #[serde( + default, + skip_serializing_if = "ExtractMap::is_empty", + serialize_with = "extract_map::serialize_as_map" + )] + pub messages: ExtractMap, /// The resolved attachments. - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub attachments: HashMap, + #[serde( + default, + skip_serializing_if = "ExtractMap::is_empty", + serialize_with = "extract_map::serialize_as_map" + )] + pub attachments: ExtractMap, } /// A set of a parameter and a value from the user. @@ -529,7 +543,7 @@ pub struct CommandDataResolved { #[non_exhaustive] pub struct CommandDataOption { /// The name of the parameter. - pub name: String, + pub name: FixedString, /// The given value. pub value: CommandDataOptionValue, } @@ -543,11 +557,11 @@ impl CommandDataOption { #[derive(Deserialize, Serialize)] struct RawCommandDataOption { - name: String, + name: FixedString, #[serde(rename = "type")] kind: CommandOptionType, #[serde(skip_serializing_if = "Option::is_none")] - value: Option, + value: Option, #[serde(skip_serializing_if = "Option::is_none")] options: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -557,8 +571,8 @@ struct RawCommandDataOption { fn option_from_raw(raw: RawCommandDataOption) -> Result { macro_rules! value { () => {{ - json::from_value( - raw.value.ok_or_else::(|| DeError::missing_field("value"))?, + serde_json::from_value( + raw.value.ok_or_else(|| serde_json::Error::missing_field("value"))?, )? }}; } @@ -573,23 +587,21 @@ fn option_from_raw(raw: RawCommandDataOption) -> Result { CommandOptionType::Number => CommandDataOptionValue::Number(value!()), CommandOptionType::String => CommandDataOptionValue::String(value!()), CommandOptionType::SubCommand => { - let options = - raw.options.ok_or_else::(|| DeError::missing_field("options"))?; + let options = raw.options.ok_or_else(|| serde_json::Error::missing_field("options"))?; let options = options.into_iter().map(option_from_raw).collect::>()?; - CommandDataOptionValue::SubCommand(options) + CommandDataOptionValue::SubCommand(FixedArray::from_vec_trunc(options)) }, CommandOptionType::SubCommandGroup => { - let options = - raw.options.ok_or_else::(|| DeError::missing_field("options"))?; + let options = raw.options.ok_or_else(|| serde_json::Error::missing_field("options"))?; let options = options.into_iter().map(option_from_raw).collect::>()?; - CommandDataOptionValue::SubCommandGroup(options) + CommandDataOptionValue::SubCommandGroup(FixedArray::from_vec_trunc(options)) }, CommandOptionType::Attachment => CommandDataOptionValue::Attachment(value!()), CommandOptionType::Channel => CommandDataOptionValue::Channel(value!()), CommandOptionType::Mentionable => CommandDataOptionValue::Mentionable(value!()), CommandOptionType::Role => CommandDataOptionValue::Role(value!()), CommandOptionType::User => CommandDataOptionValue::User(value!()), - CommandOptionType::Unknown(unknown) => CommandDataOptionValue::Unknown(unknown), + CommandOptionType(unknown) => CommandDataOptionValue::Unknown(unknown), }; Ok(CommandDataOption { @@ -612,21 +624,21 @@ fn option_to_raw(option: &CommandDataOption) -> Result { kind: _, value, } => { - raw.value = Some(json::to_value(value)?); + raw.value = Some(serde_json::to_value(value)?); raw.focused = Some(true); }, - CommandDataOptionValue::Boolean(v) => raw.value = Some(json::to_value(v)?), - CommandDataOptionValue::Integer(v) => raw.value = Some(json::to_value(v)?), - CommandDataOptionValue::Number(v) => raw.value = Some(json::to_value(v)?), - CommandDataOptionValue::String(v) => raw.value = Some(json::to_value(v)?), + CommandDataOptionValue::Boolean(v) => raw.value = Some(serde_json::to_value(v)?), + CommandDataOptionValue::Integer(v) => raw.value = Some(serde_json::to_value(v)?), + CommandDataOptionValue::Number(v) => raw.value = Some(serde_json::to_value(v)?), + CommandDataOptionValue::String(v) => raw.value = Some(serde_json::to_value(v)?), CommandDataOptionValue::SubCommand(o) | CommandDataOptionValue::SubCommandGroup(o) => { raw.options = Some(o.iter().map(option_to_raw).collect::>()?); }, - CommandDataOptionValue::Attachment(v) => raw.value = Some(json::to_value(v)?), - CommandDataOptionValue::Channel(v) => raw.value = Some(json::to_value(v)?), - CommandDataOptionValue::Mentionable(v) => raw.value = Some(json::to_value(v)?), - CommandDataOptionValue::Role(v) => raw.value = Some(json::to_value(v)?), - CommandDataOptionValue::User(v) => raw.value = Some(json::to_value(v)?), + CommandDataOptionValue::Attachment(v) => raw.value = Some(serde_json::to_value(v)?), + CommandDataOptionValue::Channel(v) => raw.value = Some(serde_json::to_value(v)?), + CommandDataOptionValue::Mentionable(v) => raw.value = Some(serde_json::to_value(v)?), + CommandDataOptionValue::Role(v) => raw.value = Some(serde_json::to_value(v)?), + CommandDataOptionValue::User(v) => raw.value = Some(serde_json::to_value(v)?), CommandDataOptionValue::Unknown(_) => {}, } @@ -653,13 +665,13 @@ impl Serialize for CommandDataOption { #[derive(Clone, Debug, PartialEq)] #[non_exhaustive] pub enum CommandDataOptionValue { - Autocomplete { kind: CommandOptionType, value: String }, + Autocomplete { kind: CommandOptionType, value: FixedString }, Boolean(bool), Integer(i64), Number(f64), - String(String), - SubCommand(Vec), - SubCommandGroup(Vec), + String(FixedString), + SubCommand(FixedArray), + SubCommandGroup(FixedArray), Attachment(AttachmentId), Channel(ChannelId), Mentionable(GenericId), @@ -815,20 +827,28 @@ impl From for UserId { #[cfg(test)] mod tests { + use serde_json::json; + use super::*; - use crate::json::{assert_json, json}; + use crate::model::utils::assert_json; #[test] fn nested_options() { let value = CommandDataOption { - name: "subcommand_group".into(), - value: CommandDataOptionValue::SubCommandGroup(vec![CommandDataOption { - name: "subcommand".into(), - value: CommandDataOptionValue::SubCommand(vec![CommandDataOption { - name: "channel".into(), - value: CommandDataOptionValue::Channel(ChannelId::new(3)), - }]), - }]), + name: FixedString::from_static_trunc("subcommand_group"), + value: CommandDataOptionValue::SubCommandGroup( + vec![CommandDataOption { + name: FixedString::from_static_trunc("subcommand"), + value: CommandDataOptionValue::SubCommand( + vec![CommandDataOption { + name: FixedString::from_static_trunc("channel"), + value: CommandDataOptionValue::Channel(ChannelId::new(3)), + }] + .trunc_into(), + ), + }] + .trunc_into(), + ), }; assert_json( @@ -849,30 +869,30 @@ mod tests { fn mixed_options() { let value = vec![ CommandDataOption { - name: "boolean".into(), + name: FixedString::from_static_trunc("boolean"), value: CommandDataOptionValue::Boolean(true), }, CommandDataOption { - name: "integer".into(), + name: FixedString::from_static_trunc("integer"), value: CommandDataOptionValue::Integer(1), }, CommandDataOption { - name: "number".into(), + name: FixedString::from_static_trunc("number"), value: CommandDataOptionValue::Number(2.0), }, CommandDataOption { - name: "string".into(), - value: CommandDataOptionValue::String("foobar".into()), + name: FixedString::from_static_trunc("string"), + value: CommandDataOptionValue::String(FixedString::from_static_trunc("foobar")), }, CommandDataOption { - name: "empty_subcommand".into(), - value: CommandDataOptionValue::SubCommand(vec![]), + name: FixedString::from_static_trunc("empty_subcommand"), + value: CommandDataOptionValue::SubCommand(FixedArray::default()), }, CommandDataOption { - name: "autocomplete".into(), + name: FixedString::from_static_trunc("autocomplete"), value: CommandDataOptionValue::Autocomplete { kind: CommandOptionType::Integer, - value: "not an integer".into(), + value: FixedString::from_static_trunc("not an integer"), }, }, ]; diff --git a/src/model/application/component.rs b/src/model/application/component.rs index 3f45b8ecea3..7d0778cf285 100644 --- a/src/model/application/component.rs +++ b/src/model/application/component.rs @@ -1,8 +1,8 @@ use serde::de::Error as DeError; use serde::ser::{Serialize, Serializer}; +use serde_json::from_value; use crate::internal::prelude::*; -use crate::json::from_value; use crate::model::prelude::*; use crate::model::utils::{default_true, deserialize_val}; @@ -10,7 +10,6 @@ enum_number! { /// The type of a component #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ComponentType { ActionRow = 1, @@ -37,7 +36,7 @@ pub struct ActionRow { pub kind: ComponentType, /// The components of this ActionRow. #[serde(default)] - pub components: Vec, + pub components: FixedArray, } /// A component which can be inside of an [`ActionRow`]. @@ -70,7 +69,7 @@ impl<'de> Deserialize<'de> for ActionRowComponent { ComponentType::ActionRow => { return Err(DeError::custom("Invalid component type ActionRow")) }, - ComponentType::Unknown(i) => { + ComponentType(i) => { return Err(DeError::custom(format_args!("Unknown component type {i}"))) }, } @@ -104,8 +103,8 @@ impl From for ActionRowComponent { #[derive(Clone, Debug, Deserialize, PartialEq, Eq)] #[serde(untagged)] pub enum ButtonKind { - Link { url: String }, - NonLink { custom_id: String, style: ButtonStyle }, + Link { url: FixedString }, + NonLink { custom_id: FixedString, style: ButtonStyle }, } impl Serialize for ButtonKind { @@ -134,7 +133,7 @@ impl Serialize for ButtonKind { custom_id, style, } => Helper { - style: (*style).into(), + style: style.0, url: None, custom_id: Some(custom_id), }, @@ -158,7 +157,7 @@ pub struct Button { pub data: ButtonKind, /// The text which appears on the button. #[serde(skip_serializing_if = "Option::is_none")] - pub label: Option, + pub label: Option, /// The emoji of this button, if there is one. #[serde(skip_serializing_if = "Option::is_none")] pub emoji: Option, @@ -171,7 +170,6 @@ enum_number! { /// The style of a button. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ButtonStyle { Primary = 1, @@ -196,17 +194,17 @@ pub struct SelectMenu { #[serde(rename = "type")] pub kind: ComponentType, /// An identifier defined by the developer for the select menu. - pub custom_id: Option, + pub custom_id: Option, /// The options of this select menu. /// /// Required for [`ComponentType::StringSelect`] and unavailable for all others. #[serde(default)] - pub options: Vec, + pub options: FixedArray, /// List of channel types to include in the [`ComponentType::ChannelSelect`]. #[serde(default)] - pub channel_types: Vec, + pub channel_types: FixedArray, /// The placeholder shown when nothing is selected. - pub placeholder: Option, + pub placeholder: Option, /// The minimum number of selections allowed. pub min_values: Option, /// The maximum number of selections allowed. @@ -224,11 +222,11 @@ pub struct SelectMenu { #[non_exhaustive] pub struct SelectMenuOption { /// The text displayed on this option. - pub label: String, + pub label: FixedString, /// The value to be sent for this option. - pub value: String, + pub value: FixedString, /// The description shown for this option. - pub description: Option, + pub description: Option, /// The emoji displayed on this option. pub emoji: Option, /// Render this option as the default selection. @@ -247,7 +245,7 @@ pub struct InputText { #[serde(rename = "type")] pub kind: ComponentType, /// Developer-defined identifier for the input; max 100 characters - pub custom_id: String, + pub custom_id: FixedString, /// The [`InputTextStyle`]. Required when sending modal data. /// /// Discord docs are wrong here; it says the field is always sent in modal submit interactions @@ -259,7 +257,7 @@ pub struct InputText { /// Discord docs are wrong here; it says the field is always sent in modal submit interactions /// but it's not. It's only required when _sending_ modal data to Discord. /// - pub label: Option, + pub label: Option>, /// Minimum input length for a text input; min 0, max 4000 #[serde(skip_serializing_if = "Option::is_none")] pub min_length: Option, @@ -273,10 +271,10 @@ pub struct InputText { /// /// When receiving: The input from the user (always Some) #[serde(skip_serializing_if = "Option::is_none")] - pub value: Option, + pub value: Option>, /// Custom placeholder text if the input is empty; max 100 characters #[serde(skip_serializing_if = "Option::is_none")] - pub placeholder: Option, + pub placeholder: Option>, } enum_number! { @@ -285,7 +283,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-styles). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum InputTextStyle { Short = 1, @@ -296,18 +293,20 @@ enum_number! { #[cfg(test)] mod tests { + use serde_json::json; + use super::*; - use crate::json::{assert_json, json}; + use crate::model::utils::assert_json; #[test] fn test_button_serde() { let mut button = Button { kind: ComponentType::Button, data: ButtonKind::NonLink { - custom_id: "hello".into(), + custom_id: FixedString::from_static_trunc("hello"), style: ButtonStyle::Danger, }, - label: Some("a".into()), + label: Some(FixedString::from_static_trunc("a")), emoji: None, disabled: false, }; @@ -317,7 +316,7 @@ mod tests { ); button.data = ButtonKind::Link { - url: "https://google.com".into(), + url: FixedString::from_static_trunc("https://google.com"), }; assert_json( &button, diff --git a/src/model/application/component_interaction.rs b/src/model/application/component_interaction.rs index 78442982edf..ca10be8472b 100644 --- a/src/model/application/component_interaction.rs +++ b/src/model/application/component_interaction.rs @@ -1,9 +1,9 @@ use serde::de::Error as DeError; use serde::ser::{Error as _, Serialize}; +use serde_json::{from_value, json}; #[cfg(feature = "model")] use crate::builder::{ - Builder, CreateInteractionResponse, CreateInteractionResponseFollowup, CreateInteractionResponseMessage, @@ -12,9 +12,8 @@ use crate::builder::{ #[cfg(feature = "collector")] use crate::client::Context; #[cfg(feature = "model")] -use crate::http::{CacheHttp, Http}; +use crate::http::Http; use crate::internal::prelude::*; -use crate::json::{self, json}; use crate::model::prelude::*; #[cfg(all(feature = "collector", feature = "utils"))] use crate::utils::{CreateQuickModal, QuickModalResponse}; @@ -49,7 +48,7 @@ pub struct ComponentInteraction { #[serde(default)] pub user: User, /// A continuation token for responding to the interaction. - pub token: String, + pub token: FixedString, /// Always `1`. pub version: u8, /// The message this interaction was triggered by, if it is a component. @@ -57,17 +56,17 @@ pub struct ComponentInteraction { /// Permissions the app or bot has within the channel the interaction was sent from. pub app_permissions: Option, /// The selected language of the invoking user. - pub locale: String, + pub locale: FixedString, /// The guild's preferred locale. - pub guild_locale: Option, + pub guild_locale: Option, /// For monetized applications, any entitlements of the invoking user. pub entitlements: Vec, /// The owners of the applications that authorized the interaction, such as a guild or user. #[serde(default)] - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] pub authorizing_integration_owners: AuthorizingIntegrationOwners, /// The context where the interaction was triggered from. - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] pub context: Option, } @@ -78,8 +77,8 @@ impl ComponentInteraction { /// # Errors /// /// Returns an [`Error::Http`] if there is no interaction response. - pub async fn get_response(&self, http: impl AsRef) -> Result { - http.as_ref().get_original_interaction_response(&self.token).await + pub async fn get_response(&self, http: &Http) -> Result { + http.get_original_interaction_response(&self.token).await } /// Creates a response to the interaction received. @@ -93,10 +92,10 @@ impl ComponentInteraction { /// deserializing the API response. pub async fn create_response( &self, - cache_http: impl CacheHttp, - builder: CreateInteractionResponse, + http: &Http, + builder: CreateInteractionResponse<'_>, ) -> Result<()> { - builder.execute(cache_http, (self.id, &self.token)).await + builder.execute(http, self.id, &self.token).await } /// Edits the initial interaction response. @@ -110,10 +109,10 @@ impl ComponentInteraction { /// deserializing the API response. pub async fn edit_response( &self, - cache_http: impl CacheHttp, - builder: EditInteractionResponse, + http: &Http, + builder: EditInteractionResponse<'_>, ) -> Result { - builder.execute(cache_http, &self.token).await + builder.execute(http, &self.token).await } /// Deletes the initial interaction response. @@ -124,8 +123,8 @@ impl ComponentInteraction { /// /// May return [`Error::Http`] if the API returns an error. Such as if the response was already /// deleted. - pub async fn delete_response(&self, http: impl AsRef) -> Result<()> { - http.as_ref().delete_original_interaction_response(&self.token).await + pub async fn delete_response(&self, http: &Http) -> Result<()> { + http.delete_original_interaction_response(&self.token).await } /// Creates a followup response to the response sent. @@ -139,10 +138,10 @@ impl ComponentInteraction { /// response. pub async fn create_followup( &self, - cache_http: impl CacheHttp, - builder: CreateInteractionResponseFollowup, + http: &Http, + builder: CreateInteractionResponseFollowup<'_>, ) -> Result { - builder.execute(cache_http, (None, &self.token)).await + builder.execute(http, None, &self.token).await } /// Edits a followup response to the response sent. @@ -156,11 +155,11 @@ impl ComponentInteraction { /// response. pub async fn edit_followup( &self, - cache_http: impl CacheHttp, - message_id: impl Into, - builder: CreateInteractionResponseFollowup, + http: &Http, + message_id: MessageId, + builder: CreateInteractionResponseFollowup<'_>, ) -> Result { - builder.execute(cache_http, (Some(message_id.into()), &self.token)).await + builder.execute(http, Some(message_id), &self.token).await } /// Deletes a followup message. @@ -169,12 +168,8 @@ impl ComponentInteraction { /// /// May return [`Error::Http`] if the API returns an error. Such as if the response was already /// deleted. - pub async fn delete_followup>( - &self, - http: impl AsRef, - message_id: M, - ) -> Result<()> { - http.as_ref().delete_followup_message(&self.token, message_id.into()).await + pub async fn delete_followup(&self, http: &Http, message_id: MessageId) -> Result<()> { + http.delete_followup_message(&self.token, message_id).await } /// Gets a followup message. @@ -183,12 +178,8 @@ impl ComponentInteraction { /// /// May return [`Error::Http`] if the API returns an error. Such as if the response was /// deleted. - pub async fn get_followup>( - &self, - http: impl AsRef, - message_id: M, - ) -> Result { - http.as_ref().get_followup_message(&self.token, message_id.into()).await + pub async fn get_followup(&self, http: &Http, message_id: MessageId) -> Result { + http.get_followup_message(&self.token, message_id).await } /// Helper function to defer an interaction. @@ -197,8 +188,8 @@ impl ComponentInteraction { /// /// Returns an [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is /// an error in deserializing the API response. - pub async fn defer(&self, cache_http: impl CacheHttp) -> Result<()> { - self.create_response(cache_http, CreateInteractionResponse::Acknowledge).await + pub async fn defer(&self, http: &Http) -> Result<()> { + self.create_response(http, CreateInteractionResponse::Acknowledge).await } /// Helper function to defer an interaction ephemerally @@ -207,11 +198,11 @@ impl ComponentInteraction { /// /// May also return an [`Error::Http`] if the API returns an error, or an [`Error::Json`] if /// there is an error in deserializing the API response. - pub async fn defer_ephemeral(&self, cache_http: impl CacheHttp) -> Result<()> { + pub async fn defer_ephemeral(&self, http: &Http) -> Result<()> { let builder = CreateInteractionResponse::Defer( CreateInteractionResponseMessage::new().ephemeral(true), ); - self.create_response(cache_http, builder).await + self.create_response(http, builder).await } /// See [`CreateQuickModal`]. @@ -223,7 +214,7 @@ impl ComponentInteraction { pub async fn quick_modal( &self, ctx: &Context, - builder: CreateQuickModal, + builder: CreateQuickModal<'_>, ) -> Result> { builder.execute(ctx, self.id, &self.token).await } @@ -254,11 +245,11 @@ impl Serialize for ComponentInteraction { #[derive(Clone, Debug)] pub enum ComponentInteractionDataKind { Button, - StringSelect { values: Vec }, - UserSelect { values: Vec }, - RoleSelect { values: Vec }, - MentionableSelect { values: Vec }, - ChannelSelect { values: Vec }, + StringSelect { values: FixedArray }, + UserSelect { values: FixedArray }, + RoleSelect { values: FixedArray }, + MentionableSelect { values: FixedArray }, + ChannelSelect { values: FixedArray }, Unknown(u8), } @@ -268,13 +259,13 @@ impl<'de> Deserialize<'de> for ComponentInteractionDataKind { #[derive(Deserialize)] struct Json { component_type: ComponentType, - values: Option, + values: Option, } let json = Json::deserialize(deserializer)?; macro_rules! parse_values { () => { - json::from_value(json.values.ok_or_else(|| D::Error::missing_field("values"))?) + from_value(json.values.ok_or_else(|| D::Error::missing_field("values"))?) .map_err(D::Error::custom)? }; } @@ -296,12 +287,12 @@ impl<'de> Deserialize<'de> for ComponentInteractionDataKind { ComponentType::ChannelSelect => Self::ChannelSelect { values: parse_values!(), }, - ComponentType::Unknown(x) => Self::Unknown(x), x @ (ComponentType::ActionRow | ComponentType::InputText) => { return Err(D::Error::custom(format_args!( "invalid message component type in this context: {x:?}", ))); }, + ComponentType(x) => Self::Unknown(x), }) } } @@ -319,12 +310,12 @@ impl Serialize for ComponentInteractionDataKind { Self::Unknown(x) => *x, }, "values": match self { - Self::StringSelect { values } => json::to_value(values).map_err(S::Error::custom)?, - Self::UserSelect { values } => json::to_value(values).map_err(S::Error::custom)?, - Self::RoleSelect { values } => json::to_value(values).map_err(S::Error::custom)?, - Self::MentionableSelect { values } => json::to_value(values).map_err(S::Error::custom)?, - Self::ChannelSelect { values } => json::to_value(values).map_err(S::Error::custom)?, - Self::Button | Self::Unknown(_) => json::NULL, + Self::StringSelect { values } => serde_json::to_value(values).map_err(S::Error::custom)?, + Self::UserSelect { values } => serde_json::to_value(values).map_err(S::Error::custom)?, + Self::RoleSelect { values } => serde_json::to_value(values).map_err(S::Error::custom)?, + Self::MentionableSelect { values } => serde_json::to_value(values).map_err(S::Error::custom)?, + Self::ChannelSelect { values } => serde_json::to_value(values).map_err(S::Error::custom)?, + Self::Button | Self::Unknown(_) => Value::Null, }, }) .serialize(serializer) @@ -339,7 +330,7 @@ impl Serialize for ComponentInteractionDataKind { #[non_exhaustive] pub struct ComponentInteractionData { /// The custom id of the component. - pub custom_id: String, + pub custom_id: FixedString, /// Type and type-specific data of this component interaction. #[serde(flatten)] pub kind: ComponentInteractionDataKind, diff --git a/src/model/application/interaction.rs b/src/model/application/interaction.rs index bb2e98a8020..6ed64cdbb18 100644 --- a/src/model/application/interaction.rs +++ b/src/model/application/interaction.rs @@ -1,19 +1,20 @@ use serde::de::{Deserialize, Deserializer, Error as DeError}; use serde::ser::{Serialize, Serializer}; +use serde_json::from_value; -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] use super::InstallationContext; use super::{CommandInteraction, ComponentInteraction, ModalInteraction, PingInteraction}; use crate::internal::prelude::*; -use crate::json::from_value; +#[cfg(not(feature = "unstable"))] use crate::model::guild::PartialMember; use crate::model::id::{ApplicationId, InteractionId}; -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] use crate::model::id::{GuildId, MessageId, UserId}; use crate::model::monetization::Entitlement; use crate::model::user::User; use crate::model::utils::deserialize_val; -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] use crate::model::utils::StrOrInt; use crate::model::Permissions; @@ -243,7 +244,7 @@ impl<'de> Deserialize<'de> for Interaction { InteractionType::Autocomplete => from_value(value).map(Interaction::Autocomplete), InteractionType::Modal => from_value(value).map(Interaction::Modal), InteractionType::Ping => from_value(value).map(Interaction::Ping), - InteractionType::Unknown(_) => return Err(DeError::custom("Unknown interaction type")), + InteractionType(_) => return Err(DeError::custom("Unknown interaction type")), } .map_err(DeError::custom) } @@ -266,7 +267,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-type). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum InteractionType { Ping = 1, @@ -299,7 +299,7 @@ bitflags! { /// /// [Discord Docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object) #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] #[derive(Clone, Debug)] #[non_exhaustive] pub enum AuthorizingIntegrationOwner { @@ -315,12 +315,12 @@ pub enum AuthorizingIntegrationOwner { } #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] #[derive(Clone, Debug, Default)] #[repr(transparent)] pub struct AuthorizingIntegrationOwners(Vec); -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] impl<'de> serde::Deserialize<'de> for AuthorizingIntegrationOwners { fn deserialize>(deserializer: D) -> StdResult { struct Visitor; @@ -339,13 +339,19 @@ impl<'de> serde::Deserialize<'de> for AuthorizingIntegrationOwners { let mut out = Vec::new(); while let Some(key_str) = map.next_key::>()? { let key_int = key_str.0.parse::().map_err(serde::de::Error::custom)?; - let value = match InstallationContext::from(key_int) { + let value = match InstallationContext(key_int) { InstallationContext::Guild => { // GuildId here can be `0`, which signals the command is guild installed // but invoked in a DM, we have to do this fun deserialisation dance. let id_str = map.next_value::>()?; let id_int = id_str.parse().map_err(A::Error::custom)?; - let id = std::num::NonZeroU64::new(id_int).map(GuildId::from); + let id = if id_int == 0 { + None + } else if id_int == u64::MAX { + return Err(serde::de::Error::custom("GuildId cannot be u64::MAX")); + } else { + Some(GuildId::new(id_int)) + }; AuthorizingIntegrationOwner::GuildInstall(id) }, @@ -366,7 +372,7 @@ impl<'de> serde::Deserialize<'de> for AuthorizingIntegrationOwners { } } -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] impl serde::Serialize for AuthorizingIntegrationOwners { fn serialize(&self, serializer: S) -> StdResult { use serde::ser::SerializeMap; @@ -395,10 +401,7 @@ impl serde::Serialize for AuthorizingIntegrationOwners { /// [`Message`]: crate::model::channel::Message /// /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object). -#[cfg_attr( - all(not(ignore_serenity_deprecated), feature = "unstable_discord_api"), - deprecated = "Use Message::interaction_metadata" -)] +#[cfg(not(feature = "unstable"))] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] @@ -411,7 +414,7 @@ pub struct MessageInteraction { /// The name of the [`Command`]. /// /// [`Command`]: crate::model::application::Command - pub name: String, + pub name: FixedString, /// The user who invoked the interaction. pub user: User, /// The member who invoked the interaction in the guild. @@ -423,7 +426,7 @@ pub struct MessageInteraction { /// user IDs. #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] pub struct MessageInteractionMetadata { /// The ID of the interaction pub id: InteractionId, diff --git a/src/model/application/mod.rs b/src/model/application/mod.rs index 7750c478b66..c3c0a32fe17 100644 --- a/src/model/application/mod.rs +++ b/src/model/application/mod.rs @@ -21,6 +21,7 @@ use super::id::{ApplicationId, GenericId, GuildId, SkuId, UserId}; use super::misc::ImageHash; use super::user::User; use super::Permissions; +use crate::internal::prelude::*; /// Partial information about the given application. /// @@ -42,29 +43,29 @@ pub struct PartialCurrentApplicationInfo { #[non_exhaustive] pub struct CurrentApplicationInfo { pub id: ApplicationId, - pub name: String, + pub name: FixedString, pub icon: Option, - pub description: String, + pub description: FixedString, #[serde(default)] - pub rpc_origins: Vec, + pub rpc_origins: FixedArray, pub bot_public: bool, pub bot_require_code_grant: bool, #[serde(default)] - pub terms_of_service_url: Option, + pub terms_of_service_url: Option, #[serde(default)] - pub privacy_policy_url: Option, + pub privacy_policy_url: Option, pub owner: Option, // omitted `summary` because it deprecated - pub verify_key: String, + pub verify_key: FixedString, pub team: Option, #[serde(default)] pub guild_id: Option, #[serde(default)] pub primary_sku_id: Option, #[serde(default)] - pub slug: Option, + pub slug: Option, #[serde(default)] - pub cover_image: Option, + pub cover_image: Option, #[serde(default)] pub flags: Option, #[serde(default)] @@ -72,24 +73,23 @@ pub struct CurrentApplicationInfo { #[serde(default)] pub install_params: Option, #[serde(default)] - pub custom_install_url: Option, + pub custom_install_url: Option, /// The application's role connection verification entry point, which when configured will /// render the app as a verification method in the guild role verification configuration. - pub role_connections_verification_url: Option, - #[cfg(feature = "unstable_discord_api")] + pub role_connections_verification_url: Option, + #[cfg(feature = "unstable")] #[serde(default)] pub integration_types_config: std::collections::HashMap, } -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] enum_number! { /// An enum representing the [installation contexts]. /// /// [interaction contexts](https://discord.com/developers/docs/resources/application#application-object-application-integration-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum InstallationContext { Guild = 0, @@ -98,14 +98,13 @@ enum_number! { } } -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] enum_number! { /// An enum representing the different [interaction contexts]. /// /// [interaction contexts](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-context-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum InteractionContext { /// Interaction can be used within servers @@ -121,7 +120,7 @@ enum_number! { /// Information about how the [`CurrentApplicationInfo`] is installed. /// /// [Discord docs](https://discord.com/developers/docs/resources/application#application-object-application-integration-types). -#[cfg(feature = "unstable_discord_api")] +#[cfg(feature = "unstable")] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct InstallationContextConfig { pub oauth2_install_params: InstallParams, @@ -138,9 +137,9 @@ pub struct Team { /// The snowflake ID of the team. pub id: GenericId, /// The name of the team. - pub name: String, + pub name: FixedString, /// The members of the team - pub members: Vec, + pub members: FixedArray, /// The user id of the team owner. pub owner_user_id: UserId, } @@ -153,11 +152,6 @@ pub struct Team { pub struct TeamMember { /// The member's membership state. pub membership_state: MembershipState, - /// The list of permissions of the member on the team. - /// - /// NOTE: Will always be "*" for now. - #[deprecated = "This field is not sent by the API anymore"] - pub permissions: Vec, /// The ID of the team they are a member of. pub team_id: GenericId, /// The user type of the team member. @@ -169,7 +163,6 @@ pub struct TeamMember { enum_number! { /// [Discord docs](https://discord.com/developers/docs/topics/teams#data-models-membership-state-enum). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum MembershipState { Invited = 1, @@ -262,7 +255,7 @@ bitflags! { #[derive(Debug, Clone, Serialize, Deserialize)] #[non_exhaustive] pub struct InstallParams { - pub scopes: Vec, + pub scopes: FixedArray, pub permissions: Permissions, } diff --git a/src/model/application/modal_interaction.rs b/src/model/application/modal_interaction.rs index fcb04db26b1..b1b79f14ed5 100644 --- a/src/model/application/modal_interaction.rs +++ b/src/model/application/modal_interaction.rs @@ -2,14 +2,13 @@ use serde::Serialize; #[cfg(feature = "model")] use crate::builder::{ - Builder, CreateInteractionResponse, CreateInteractionResponseFollowup, CreateInteractionResponseMessage, EditInteractionResponse, }; #[cfg(feature = "model")] -use crate::http::{CacheHttp, Http}; +use crate::http::Http; use crate::internal::prelude::*; use crate::model::prelude::*; @@ -43,7 +42,7 @@ pub struct ModalInteraction { #[serde(default)] pub user: User, /// A continuation token for responding to the interaction. - pub token: String, + pub token: FixedString, /// Always `1`. pub version: u8, /// The message this interaction was triggered by @@ -55,9 +54,9 @@ pub struct ModalInteraction { /// Permissions the app or bot has within the channel the interaction was sent from. pub app_permissions: Option, /// The selected language of the invoking user. - pub locale: String, + pub locale: FixedString, /// The guild's preferred locale. - pub guild_locale: Option, + pub guild_locale: Option, /// For monetized applications, any entitlements of the invoking user. pub entitlements: Vec, } @@ -69,8 +68,8 @@ impl ModalInteraction { /// # Errors /// /// Returns an [`Error::Http`] if there is no interaction response. - pub async fn get_response(&self, http: impl AsRef) -> Result { - http.as_ref().get_original_interaction_response(&self.token).await + pub async fn get_response(&self, http: &Http) -> Result { + http.get_original_interaction_response(&self.token).await } /// Creates a response to the interaction received. @@ -84,10 +83,10 @@ impl ModalInteraction { /// deserializing the API response. pub async fn create_response( &self, - cache_http: impl CacheHttp, - builder: CreateInteractionResponse, + http: &Http, + builder: CreateInteractionResponse<'_>, ) -> Result<()> { - builder.execute(cache_http, (self.id, &self.token)).await + builder.execute(http, self.id, &self.token).await } /// Edits the initial interaction response. @@ -101,10 +100,10 @@ impl ModalInteraction { /// deserializing the API response. pub async fn edit_response( &self, - cache_http: impl CacheHttp, - builder: EditInteractionResponse, + http: &Http, + builder: EditInteractionResponse<'_>, ) -> Result { - builder.execute(cache_http, &self.token).await + builder.execute(http, &self.token).await } /// Deletes the initial interaction response. @@ -115,8 +114,8 @@ impl ModalInteraction { /// /// May return [`Error::Http`] if the API returns an error. Such as if the response was already /// deleted. - pub async fn delete_response(&self, http: impl AsRef) -> Result<()> { - http.as_ref().delete_original_interaction_response(&self.token).await + pub async fn delete_response(&self, http: &Http) -> Result<()> { + http.delete_original_interaction_response(&self.token).await } /// Creates a followup response to the response sent. @@ -130,10 +129,10 @@ impl ModalInteraction { /// response. pub async fn create_followup( &self, - cache_http: impl CacheHttp, - builder: CreateInteractionResponseFollowup, + http: &Http, + builder: CreateInteractionResponseFollowup<'_>, ) -> Result { - builder.execute(cache_http, (None, &self.token)).await + builder.execute(http, None, &self.token).await } /// Edits a followup response to the response sent. @@ -147,11 +146,11 @@ impl ModalInteraction { /// response. pub async fn edit_followup( &self, - cache_http: impl CacheHttp, - message_id: impl Into, - builder: CreateInteractionResponseFollowup, + http: &Http, + message_id: MessageId, + builder: CreateInteractionResponseFollowup<'_>, ) -> Result { - builder.execute(cache_http, (Some(message_id.into()), &self.token)).await + builder.execute(http, Some(message_id), &self.token).await } /// Deletes a followup message. @@ -160,12 +159,8 @@ impl ModalInteraction { /// /// May return [`Error::Http`] if the API returns an error. Such as if the response was already /// deleted. - pub async fn delete_followup>( - &self, - http: impl AsRef, - message_id: M, - ) -> Result<()> { - http.as_ref().delete_followup_message(&self.token, message_id.into()).await + pub async fn delete_followup(&self, http: &Http, message_id: MessageId) -> Result<()> { + http.delete_followup_message(&self.token, message_id).await } /// Helper function to defer an interaction. @@ -174,8 +169,8 @@ impl ModalInteraction { /// /// Returns an [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is /// an error in deserializing the API response. - pub async fn defer(&self, cache_http: impl CacheHttp) -> Result<()> { - self.create_response(cache_http, CreateInteractionResponse::Acknowledge).await + pub async fn defer(&self, http: &Http) -> Result<()> { + self.create_response(http, CreateInteractionResponse::Acknowledge).await } /// Helper function to defer an interaction ephemerally @@ -184,11 +179,11 @@ impl ModalInteraction { /// /// May also return an [`Error::Http`] if the API returns an error, or an [`Error::Json`] if /// there is an error in deserializing the API response. - pub async fn defer_ephemeral(&self, cache_http: impl CacheHttp) -> Result<()> { + pub async fn defer_ephemeral(&self, http: &Http) -> Result<()> { let builder = CreateInteractionResponse::Defer( CreateInteractionResponseMessage::new().ephemeral(true), ); - self.create_response(cache_http, builder).await + self.create_response(http, builder).await } } @@ -219,7 +214,7 @@ impl Serialize for ModalInteraction { #[non_exhaustive] pub struct ModalInteractionData { /// The custom id of the modal - pub custom_id: String, + pub custom_id: FixedString, /// The components. - pub components: Vec, + pub components: FixedArray, } diff --git a/src/model/application/ping_interaction.rs b/src/model/application/ping_interaction.rs index 269f377a0c8..c8bd3b14930 100644 --- a/src/model/application/ping_interaction.rs +++ b/src/model/application/ping_interaction.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; +use crate::internal::prelude::*; use crate::model::id::{ApplicationId, InteractionId}; /// A ping interaction, which can only be received through an endpoint url. @@ -14,7 +15,7 @@ pub struct PingInteraction { /// Id of the application this interaction is for. pub application_id: ApplicationId, /// A continuation token for responding to the interaction. - pub token: String, + pub token: FixedString, /// Always `1`. pub version: u8, } diff --git a/src/model/channel/attachment.rs b/src/model/channel/attachment.rs index d34bd9088a5..cb18516f273 100644 --- a/src/model/channel/attachment.rs +++ b/src/model/channel/attachment.rs @@ -1,8 +1,8 @@ +use nonmax::NonMaxU32; #[cfg(feature = "model")] use reqwest::Client as ReqwestClient; use serde_cow::CowStr; -#[cfg(feature = "model")] use crate::internal::prelude::*; use crate::model::prelude::*; use crate::model::utils::is_false; @@ -37,23 +37,23 @@ pub struct Attachment { pub id: AttachmentId, /// The filename of the file that was uploaded. This is equivalent to what the uploader had /// their file named. - pub filename: String, + pub filename: FixedString, /// Description for the file (max 1024 characters). - pub description: Option, + pub description: Option>, /// If the attachment is an image, then the height of the image is provided. - pub height: Option, + pub height: Option, + /// If the attachment is an image, then the width of the image is provided. + pub width: Option, /// The proxy URL. - pub proxy_url: String, + pub proxy_url: FixedString, /// The size of the file in bytes. pub size: u32, /// The URL of the uploaded attachment. - pub url: String, - /// If the attachment is an image, then the width of the image is provided. - pub width: Option, + pub url: FixedString, /// The attachment's [media type]. /// /// [media type]: https://en.wikipedia.org/wiki/Media_type - pub content_type: Option, + pub content_type: Option, /// Whether this attachment is ephemeral. /// /// Ephemeral attachments will automatically be removed after a set period of time. @@ -81,7 +81,7 @@ pub struct Attachment { impl Attachment { /// If this attachment is an image, then a tuple of the width and height in pixels is returned. #[must_use] - pub fn dimensions(&self) -> Option<(u32, u32)> { + pub fn dimensions(&self) -> Option<(NonMaxU32, NonMaxU32)> { self.width.and_then(|width| self.height.map(|height| (width, height))) } @@ -105,14 +105,16 @@ impl Attachment { /// #[serenity::async_trait] /// # #[cfg(feature = "client")] /// impl EventHandler for Handler { - /// async fn message(&self, context: Context, mut message: Message) { + /// async fn message(&self, context: Context, message: Message) { /// for attachment in message.attachments { /// let content = match attachment.download().await { /// Ok(content) => content, /// Err(why) => { /// println!("Error downloading attachment: {:?}", why); - /// let _ = - /// message.channel_id.say(&context, "Error downloading attachment").await; + /// let _ = message + /// .channel_id + /// .say(&context.http, "Error downloading attachment") + /// .await; /// /// return; /// }, @@ -122,7 +124,7 @@ impl Attachment { /// Ok(file) => file, /// Err(why) => { /// println!("Error creating file: {:?}", why); - /// let _ = message.channel_id.say(&context, "Error creating file").await; + /// let _ = message.channel_id.say(&context.http, "Error creating file").await; /// /// return; /// }, @@ -136,7 +138,7 @@ impl Attachment { /// /// let _ = message /// .channel_id - /// .say(&context, &format!("Saved {:?}", attachment.filename)) + /// .say(&context.http, &format!("Saved {:?}", attachment.filename)) /// .await; /// } /// } @@ -152,7 +154,13 @@ impl Attachment { /// [`Message`]: super::Message pub async fn download(&self) -> Result> { let reqwest = ReqwestClient::new(); - let bytes = reqwest.get(&self.url).send().await?.bytes().await?; + let bytes = reqwest.get(&*self.url).send().await?.bytes().await?; Ok(bytes.to_vec()) } } + +impl ExtractKey for Attachment { + fn extract_key(&self) -> &AttachmentId { + &self.id + } +} diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index 3494da5a6a2..0e8d925718e 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; #[cfg(feature = "model")] use std::sync::Arc; @@ -6,7 +7,6 @@ use futures::stream::Stream; #[cfg(feature = "model")] use crate::builder::{ - Builder, CreateAttachment, CreateForumPost, CreateInvite, @@ -21,15 +21,14 @@ use crate::builder::{ GetMessages, }; #[cfg(all(feature = "cache", feature = "model"))] -use crate::cache::{Cache, GuildChannelRef}; +use crate::cache::Cache; #[cfg(feature = "collector")] use crate::collector::{MessageCollector, ReactionCollector}; #[cfg(feature = "collector")] use crate::gateway::ShardMessenger; #[cfg(feature = "model")] use crate::http::{CacheHttp, Http, Typing}; -#[cfg(feature = "model")] -use crate::json::json; +use crate::internal::prelude::*; use crate::model::prelude::*; #[cfg(feature = "model")] @@ -61,9 +60,8 @@ impl ChannelId { /// channel. /// /// [Send Messages]: Permissions::SEND_MESSAGES - #[inline] - pub async fn broadcast_typing(self, http: impl AsRef) -> Result<()> { - http.as_ref().broadcast_typing(self).await + pub async fn broadcast_typing(self, http: &Http) -> Result<()> { + http.broadcast_typing(self).await } /// Creates an invite for the given channel. @@ -72,16 +70,11 @@ impl ChannelId { /// /// # Errors /// - /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE - pub async fn create_invite( - self, - cache_http: impl CacheHttp, - builder: CreateInvite<'_>, - ) -> Result { - builder.execute(cache_http, self).await + pub async fn create_invite(self, http: &Http, builder: CreateInvite<'_>) -> Result { + builder.execute(http, self).await } /// Creates a [permission overwrite][`PermissionOverwrite`] for either a single [`Member`] or @@ -99,11 +92,12 @@ impl ChannelId { /// [Manage Channels]: Permissions::MANAGE_CHANNELS pub async fn create_permission( self, - http: impl AsRef, + http: &Http, target: PermissionOverwrite, + reason: Option<&str>, ) -> Result<()> { let data: PermissionOverwriteData = target.into(); - http.as_ref().create_permission(self, data.id, &data, None).await + http.create_permission(self, data.id, &data, reason).await } /// React to a [`Message`] with a custom [`Emoji`] or unicode character. @@ -118,14 +112,13 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Add Reactions]: Permissions::ADD_REACTIONS - #[inline] pub async fn create_reaction( self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, reaction_type: impl Into, ) -> Result<()> { - http.as_ref().create_reaction(self, message_id.into(), &reaction_type.into()).await + http.create_reaction(self, message_id, &reaction_type.into()).await } /// Deletes this channel, returning the channel on a successful deletion. @@ -137,9 +130,8 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - #[inline] - pub async fn delete(self, http: impl AsRef) -> Result { - http.as_ref().delete_channel(self, None).await + pub async fn delete(self, http: &Http, reason: Option<&str>) -> Result { + http.delete_channel(self, reason).await } /// Deletes a [`Message`] given its Id. @@ -154,13 +146,13 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission to delete the message. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] pub async fn delete_message( self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, + reason: Option<&str>, ) -> Result<()> { - http.as_ref().delete_message(self, message_id.into(), None).await + http.delete_message(self, message_id, reason).await } /// Deletes all messages by Ids from the given vector in the given channel. @@ -173,32 +165,36 @@ impl ChannelId { /// /// # Errors /// - /// Returns [`ModelError::BulkDeleteAmount`] if an attempt was made to delete either 0 or more - /// than 100 messages. + /// Returns [`ModelError::TooSmall`] or [`ModelError::TooLarge`] if an attempt was made to + /// delete either 0 or more than 100 messages. /// /// Also will return [`Error::Http`] if the current user lacks permission to delete messages. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn delete_messages>( + pub async fn delete_messages( self, - http: impl AsRef, - message_ids: impl IntoIterator, + http: &Http, + message_ids: &[MessageId], + reason: Option<&str>, ) -> Result<()> { - let ids = - message_ids.into_iter().map(|message_id| *message_id.as_ref()).collect::>(); - - let len = ids.len(); + use crate::model::error::{Maximum, Minimum}; - if len == 0 || len > 100 { - return Err(Error::Model(ModelError::BulkDeleteAmount)); + #[derive(serde::Serialize)] + struct DeleteMessages<'a> { + messages: &'a [MessageId], } - if ids.len() == 1 { - self.delete_message(http, ids[0]).await + Minimum::BulkDeleteAmount.check_underflow(message_ids.len())?; + Maximum::BulkDeleteAmount.check_overflow(message_ids.len())?; + + if message_ids.len() == 1 { + self.delete_message(http, message_ids[0], reason).await } else { - let map = json!({ "messages": ids }); + let req = DeleteMessages { + messages: message_ids, + }; - http.as_ref().delete_messages(self, &map, None).await + http.delete_messages(self, &req, reason).await } } @@ -213,14 +209,15 @@ impl ChannelId { /// [Manage Channel]: Permissions::MANAGE_CHANNELS pub async fn delete_permission( self, - http: impl AsRef, + http: &Http, permission_type: PermissionOverwriteType, + reason: Option<&str>, ) -> Result<()> { let id = match permission_type { PermissionOverwriteType::Member(id) => id.into(), PermissionOverwriteType::Role(id) => id.get().into(), }; - http.as_ref().delete_permission(self, id, None).await + http.delete_permission(self, id, reason).await } /// Deletes the given [`Reaction`] from the channel. @@ -234,16 +231,13 @@ impl ChannelId { /// permission. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] pub async fn delete_reaction( self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, user_id: Option, reaction_type: impl Into, ) -> Result<()> { - let http = http.as_ref(); - let message_id = message_id.into(); let reaction_type = reaction_type.into(); match user_id { Some(user_id) => http.delete_reaction(self, message_id, user_id, &reaction_type).await, @@ -261,13 +255,8 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] - pub async fn delete_reactions( - self, - http: impl AsRef, - message_id: impl Into, - ) -> Result<()> { - http.as_ref().delete_message_reactions(self, message_id.into()).await + pub async fn delete_reactions(self, http: &Http, message_id: MessageId) -> Result<()> { + http.delete_message_reactions(self, message_id).await } /// Deletes all [`Reaction`]s of the given emoji to a message within the channel. @@ -279,16 +268,13 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] pub async fn delete_reaction_emoji( self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, reaction_type: impl Into, ) -> Result<()> { - http.as_ref() - .delete_message_reaction_emoji(self, message_id.into(), &reaction_type.into()) - .await + http.delete_message_reaction_emoji(self, message_id, &reaction_type.into()).await } /// Edits a channel's settings. @@ -316,18 +302,12 @@ impl ChannelId { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] - pub async fn edit( - self, - cache_http: impl CacheHttp, - builder: EditChannel<'_>, - ) -> Result { - builder.execute(cache_http, self).await + pub async fn edit(self, http: &Http, builder: EditChannel<'_>) -> Result { + builder.execute(http, self).await } /// Edits a [`Message`] in the channel given its Id. @@ -344,14 +324,13 @@ impl ChannelId { /// /// See [`EditMessage::execute`] for a list of possible errors, and their corresponding /// reasons. - #[inline] pub async fn edit_message( self, - cache_http: impl CacheHttp, - message_id: impl Into, - builder: EditMessage, + http: &Http, + message_id: MessageId, + builder: EditMessage<'_>, ) -> Result { - builder.execute(cache_http, (self, message_id.into(), None)).await + builder.execute(http, self, message_id, None).await } /// Follows the News Channel @@ -366,19 +345,19 @@ impl ChannelId { /// Permissions::MANAGE_WEBHOOKS pub async fn follow( self, - http: impl AsRef, - target_channel_id: impl Into, + http: &Http, + target_channel_id: ChannelId, ) -> Result { - http.as_ref().follow_news_channel(self, target_channel_id.into()).await - } + #[derive(serde::Serialize)] + struct FollowChannel { + webhook_channel_id: ChannelId, + } - /// Attempts to find a [`GuildChannel`] by its Id in the cache. - #[cfg(feature = "cache")] - #[inline] - #[deprecated = "Use Cache::guild and Guild::channels instead"] - pub fn to_channel_cached(self, cache: &Cache) -> Option> { - #[allow(deprecated)] - cache.channel(self) + let map = FollowChannel { + webhook_channel_id: target_channel_id, + }; + + http.follow_news_channel(self, &map).await } /// First attempts to retrieve the channel from the `temp_cache` if enabled, otherwise performs @@ -390,7 +369,6 @@ impl ChannelId { /// # Errors /// /// Returns [`Error::Http`] if the channel retrieval request failed. - #[inline] pub async fn to_channel(self, cache_http: impl CacheHttp) -> Result { #[cfg(feature = "temp_cache")] { @@ -427,9 +405,8 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - #[inline] - pub async fn invites(self, http: impl AsRef) -> Result> { - http.as_ref().get_channel_invites(self).await + pub async fn invites(self, http: &Http) -> Result> { + http.get_channel_invites(self).await } /// Gets a message from the channel. @@ -444,14 +421,11 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - #[inline] pub async fn message( self, cache_http: impl CacheHttp, - message_id: impl Into, + message_id: MessageId, ) -> Result { - let message_id = message_id.into(); - #[cfg(feature = "cache")] if let Some(cache) = cache_http.cache() { if let Some(message) = cache.message(self, message_id) { @@ -518,25 +492,11 @@ impl ChannelId { /// } /// # } /// ``` - pub fn messages_iter>(self, http: H) -> impl Stream> { - MessagesIter::::stream(http, self) - } - - /// Returns the name of whatever channel this id holds. - /// - /// DM channels don't have a name, so a name is generated according to - /// [`PrivateChannel::name()`]. - /// - /// # Errors - /// - /// Same as [`Self::to_channel()`]. - pub async fn name(self, cache_http: impl CacheHttp) -> Result { - let channel = self.to_channel(cache_http).await?; - - Ok(match channel { - Channel::Guild(channel) => channel.name, - Channel::Private(channel) => channel.name(), - }) + pub fn messages_iter( + self, + cache_http: &impl CacheHttp, + ) -> impl Stream> + '_ { + MessagesIter::stream(cache_http, self) } /// Pins a [`Message`] to the channel. @@ -549,9 +509,8 @@ impl ChannelId { /// many pinned messages. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] - pub async fn pin(self, http: impl AsRef, message_id: impl Into) -> Result<()> { - http.as_ref().pin_message(self, message_id.into(), None).await + pub async fn pin(self, http: &Http, message_id: MessageId, reason: Option<&str>) -> Result<()> { + http.pin_message(self, message_id, reason).await } /// Crossposts a [`Message`]. @@ -567,16 +526,15 @@ impl ChannelId { /// author of the message. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn crosspost( - self, - http: impl AsRef, - message_id: impl Into, - ) -> Result { - http.as_ref().crosspost_message(self, message_id.into()).await + pub async fn crosspost(self, http: &Http, message_id: MessageId) -> Result { + http.crosspost_message(self, message_id).await } /// Gets the list of [`Message`]s which are pinned to the channel. /// + /// If the cache is enabled, this method will fill up the message cache for the channel, if the + /// messages returned are newer than the existing cached messages or the cache is not full yet. + /// /// **Note**: Returns an empty [`Vec`] if the current user does not have the [Read Message /// History] permission. /// @@ -585,9 +543,15 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission to view the channel. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - #[inline] - pub async fn pins(self, http: impl AsRef) -> Result> { - http.as_ref().get_pins(self).await + pub async fn pins(self, cache_http: impl CacheHttp) -> Result> { + let messages = cache_http.http().get_pins(self).await?; + + #[cfg(feature = "cache")] + if let Some(cache) = cache_http.cache() { + cache.fill_message_cache(self, messages.iter().cloned()); + } + + Ok(messages) } /// Gets the list of [`User`]s who have reacted to a [`Message`] with a certain [`Emoji`]. @@ -613,23 +577,15 @@ impl ChannelId { /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY pub async fn reaction_users( self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, reaction_type: impl Into, limit: Option, - after: impl Into>, + after: Option, ) -> Result> { let limit = limit.map_or(50, |x| if x > 100 { 100 } else { x }); - http.as_ref() - .get_reaction_users( - self, - message_id.into(), - &reaction_type.into(), - limit, - after.into().map(UserId::get), - ) - .await + http.get_reaction_users(self, message_id, &reaction_type.into(), limit, after).await } /// Sends a message with just the given message content in the channel. @@ -638,16 +594,11 @@ impl ChannelId { /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content length is over the above limit. See + /// Returns a [`ModelError::TooLarge`] if the content length is over the above limit. See /// [`CreateMessage::execute`] for more details. - #[inline] - pub async fn say( - self, - cache_http: impl CacheHttp, - content: impl Into, - ) -> Result { + pub async fn say(self, http: &Http, content: impl Into>) -> Result { let builder = CreateMessage::new().content(content); - self.send_message(cache_http, builder).await + self.send_message(http, builder).await } /// Sends file(s) along with optional message contents. The filename _must_ be specified. @@ -716,13 +667,13 @@ impl ChannelId { /// reasons. /// /// [`File`]: tokio::fs::File - pub async fn send_files( + pub async fn send_files<'a>( self, - cache_http: impl CacheHttp, - files: impl IntoIterator, - builder: CreateMessage, + http: &Http, + files: impl IntoIterator>, + builder: CreateMessage<'a>, ) -> Result { - self.send_message(cache_http, builder.files(files)).await + self.send_message(http, builder.files(files)).await } /// Sends a message to the channel. @@ -734,12 +685,8 @@ impl ChannelId { /// /// See [`CreateMessage::execute`] for a list of possible errors, and their corresponding /// reasons. - pub async fn send_message( - self, - cache_http: impl CacheHttp, - builder: CreateMessage, - ) -> Result { - builder.execute(cache_http, (self, None)).await + pub async fn send_message(self, http: &Http, builder: CreateMessage<'_>) -> Result { + builder.execute(http, self, None).await } /// Starts typing in the channel for an indefinite period of time. @@ -766,7 +713,7 @@ impl ChannelId { /// # fn main() { /// # let http: Arc = unimplemented!(); /// // Initiate typing (assuming http is `Arc`) - /// let typing = ChannelId::new(7).start_typing(&http); + /// let typing = ChannelId::new(7).start_typing(http); /// /// // Run some long-running process /// long_process(); @@ -780,8 +727,8 @@ impl ChannelId { /// /// Returns [`Error::Http`] if the current user lacks permission /// to send messages in this channel. - pub fn start_typing(self, http: &Arc) -> Typing { - http.start_typing(self) + pub fn start_typing(self, http: Arc) -> Typing { + Typing::start(http, self) } /// Unpins a [`Message`] in the channel given by its Id. @@ -793,13 +740,13 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] pub async fn unpin( self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, + reason: Option<&str>, ) -> Result<()> { - http.as_ref().unpin_message(self, message_id.into(), None).await + http.unpin_message(self, message_id, reason).await } /// Retrieves the channel's webhooks. @@ -811,9 +758,8 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Webhooks]: Permissions::MANAGE_WEBHOOKS - #[inline] - pub async fn webhooks(self, http: impl AsRef) -> Result> { - http.as_ref().get_channel_webhooks(self).await + pub async fn webhooks(self, http: &Http) -> Result> { + http.get_channel_webhooks(self).await } /// Creates a webhook in the channel. @@ -821,40 +767,33 @@ impl ChannelId { /// # Errors /// /// See [`CreateWebhook::execute`] for a detailed list of possible errors. - pub async fn create_webhook( - self, - cache_http: impl CacheHttp, - builder: CreateWebhook<'_>, - ) -> Result { - builder.execute(cache_http, self).await + pub async fn create_webhook(self, http: &Http, builder: CreateWebhook<'_>) -> Result { + builder.execute(http, self).await } /// Returns a builder which can be awaited to obtain a message or stream of messages in this /// channel. #[cfg(feature = "collector")] - pub fn await_reply(self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_reply(self, shard_messenger: ShardMessenger) -> MessageCollector { MessageCollector::new(shard_messenger).channel_id(self) } /// Same as [`Self::await_reply`]. #[cfg(feature = "collector")] - pub fn await_replies(&self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_replies(&self, shard_messenger: ShardMessenger) -> MessageCollector { self.await_reply(shard_messenger) } /// Returns a builder which can be awaited to obtain a reaction or stream of reactions sent in /// this channel. #[cfg(feature = "collector")] - pub fn await_reaction(self, shard_messenger: impl AsRef) -> ReactionCollector { + pub fn await_reaction(self, shard_messenger: ShardMessenger) -> ReactionCollector { ReactionCollector::new(shard_messenger).channel_id(self) } /// Same as [`Self::await_reaction`]. #[cfg(feature = "collector")] - pub fn await_reactions( - &self, - shard_messenger: impl AsRef, - ) -> ReactionCollector { + pub fn await_reactions(&self, shard_messenger: ShardMessenger) -> ReactionCollector { self.await_reaction(shard_messenger) } @@ -864,8 +803,8 @@ impl ChannelId { /// /// Returns [`Error::Http`] if the channel is not a stage channel, or if there is no stage /// instance currently. - pub async fn get_stage_instance(self, http: impl AsRef) -> Result { - http.as_ref().get_stage_instance(self).await + pub async fn get_stage_instance(self, http: &Http) -> Result { + http.get_stage_instance(self).await } /// Creates a stage instance. @@ -875,10 +814,10 @@ impl ChannelId { /// Returns [`Error::Http`] if there is already a stage instance currently. pub async fn create_stage_instance( self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateStageInstance<'_>, ) -> Result { - builder.execute(cache_http, self).await + builder.execute(http, self).await } /// Edits the stage instance @@ -891,10 +830,10 @@ impl ChannelId { /// instance currently. pub async fn edit_stage_instance( self, - cache_http: impl CacheHttp, + http: &Http, builder: EditStageInstance<'_>, ) -> Result { - builder.execute(cache_http, self).await + builder.execute(http, self).await } /// Edits a thread. @@ -902,12 +841,8 @@ impl ChannelId { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission. - pub async fn edit_thread( - self, - cache_http: impl CacheHttp, - builder: EditThread<'_>, - ) -> Result { - builder.execute(cache_http, self).await + pub async fn edit_thread(self, http: &Http, builder: EditThread<'_>) -> Result { + builder.execute(http, self).await } /// Deletes a stage instance. @@ -916,8 +851,8 @@ impl ChannelId { /// /// Returns [`Error::Http`] if the channel is not a stage channel, or if there is no stage /// instance currently. - pub async fn delete_stage_instance(self, http: impl AsRef) -> Result<()> { - http.as_ref().delete_stage_instance(self, None).await + pub async fn delete_stage_instance(self, http: &Http, reason: Option<&str>) -> Result<()> { + http.delete_stage_instance(self, reason).await } /// Creates a public thread that is connected to a message. @@ -928,11 +863,11 @@ impl ChannelId { #[doc(alias = "create_public_thread")] pub async fn create_thread_from_message( self, - cache_http: impl CacheHttp, - message_id: impl Into, + http: &Http, + message_id: MessageId, builder: CreateThread<'_>, ) -> Result { - builder.execute(cache_http, (self, Some(message_id.into()))).await + builder.execute(http, self, Some(message_id)).await } /// Creates a thread that is not connected to a message. @@ -943,10 +878,10 @@ impl ChannelId { #[doc(alias = "create_public_thread", alias = "create_private_thread")] pub async fn create_thread( self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateThread<'_>, ) -> Result { - builder.execute(cache_http, (self, None)).await + builder.execute(http, self, None).await } /// Creates a post in a forum channel. @@ -956,10 +891,10 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. pub async fn create_forum_post( self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateForumPost<'_>, ) -> Result { - builder.execute(cache_http, self).await + builder.execute(http, self).await } /// Gets the thread members, if this channel is a thread. @@ -967,8 +902,8 @@ impl ChannelId { /// # Errors /// /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn get_thread_members(self, http: impl AsRef) -> Result> { - http.as_ref().get_channel_thread_members(self).await + pub async fn get_thread_members(self, http: &Http) -> Result> { + http.get_channel_thread_members(self).await } /// Joins the thread, if this channel is a thread. @@ -976,8 +911,8 @@ impl ChannelId { /// # Errors /// /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn join_thread(self, http: impl AsRef) -> Result<()> { - http.as_ref().join_thread_channel(self).await + pub async fn join_thread(self, http: &Http) -> Result<()> { + http.join_thread_channel(self).await } /// Leaves the thread, if this channel is a thread. @@ -985,8 +920,8 @@ impl ChannelId { /// # Errors /// /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn leave_thread(self, http: impl AsRef) -> Result<()> { - http.as_ref().leave_thread_channel(self).await + pub async fn leave_thread(self, http: &Http) -> Result<()> { + http.leave_thread_channel(self).await } /// Adds a thread member, if this channel is a thread. @@ -994,8 +929,8 @@ impl ChannelId { /// # Errors /// /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn add_thread_member(self, http: impl AsRef, user_id: UserId) -> Result<()> { - http.as_ref().add_thread_channel_member(self, user_id).await + pub async fn add_thread_member(self, http: &Http, user_id: UserId) -> Result<()> { + http.add_thread_channel_member(self, user_id).await } /// Removes a thread member, if this channel is a thread. @@ -1003,8 +938,8 @@ impl ChannelId { /// # Errors /// /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn remove_thread_member(self, http: impl AsRef, user_id: UserId) -> Result<()> { - http.as_ref().remove_thread_channel_member(self, user_id).await + pub async fn remove_thread_member(self, http: &Http, user_id: UserId) -> Result<()> { + http.remove_thread_channel_member(self, user_id).await } /// Gets a thread member, if this channel is a thread. @@ -1016,11 +951,11 @@ impl ChannelId { /// It may return an [`Error::Http`] if the channel is not a thread channel pub async fn get_thread_member( self, - http: impl AsRef, + http: &Http, user_id: UserId, with_member: bool, ) -> Result { - http.as_ref().get_thread_channel_member(self, user_id, with_member).await + http.get_thread_channel_member(self, user_id, with_member).await } /// Gets private archived threads of a channel. @@ -1030,11 +965,11 @@ impl ChannelId { /// It may return an [`Error::Http`] if the bot doesn't have the permission to get it. pub async fn get_archived_private_threads( self, - http: impl AsRef, - before: Option, + http: &Http, + before: Option, limit: Option, ) -> Result { - http.as_ref().get_channel_archived_private_threads(self, before, limit).await + http.get_channel_archived_private_threads(self, before, limit).await } /// Gets public archived threads of a channel. @@ -1044,11 +979,11 @@ impl ChannelId { /// It may return an [`Error::Http`] if the bot doesn't have the permission to get it. pub async fn get_archived_public_threads( self, - http: impl AsRef, - before: Option, + http: &Http, + before: Option, limit: Option, ) -> Result { - http.as_ref().get_channel_archived_public_threads(self, before, limit).await + http.get_channel_archived_public_threads(self, before, limit).await } /// Gets private archived threads joined by the current user of a channel. @@ -1058,11 +993,11 @@ impl ChannelId { /// It may return an [`Error::Http`] if the bot doesn't have the permission to get it. pub async fn get_joined_archived_private_threads( self, - http: impl AsRef, - before: Option, + http: &Http, + before: Option, limit: Option, ) -> Result { - http.as_ref().get_channel_joined_archived_private_threads(self, before, limit).await + http.get_channel_joined_archived_private_threads(self, before, limit).await } /// Get a list of users that voted for this specific answer. @@ -1152,8 +1087,10 @@ impl<'a> From<&'a WebhookChannel> for ChannelId { /// A helper class returned by [`ChannelId::messages_iter`] #[derive(Clone, Debug)] #[cfg(feature = "model")] -pub struct MessagesIter> { - http: H, +pub struct MessagesIter<'a> { + http: &'a Http, + #[cfg(feature = "cache")] + cache: Option<&'a Arc>, channel_id: ChannelId, buffer: Vec, before: Option, @@ -1161,10 +1098,12 @@ pub struct MessagesIter> { } #[cfg(feature = "model")] -impl> MessagesIter { - fn new(http: H, channel_id: ChannelId) -> MessagesIter { +impl<'a> MessagesIter<'a> { + fn new(cache_http: &'a impl CacheHttp, channel_id: ChannelId) -> MessagesIter<'a> { MessagesIter { - http, + http: cache_http.http(), + #[cfg(feature = "cache")] + cache: cache_http.cache(), channel_id, buffer: Vec::new(), before: None, @@ -1172,6 +1111,16 @@ impl> MessagesIter { } } + #[cfg(not(feature = "cache"))] + fn cache_http(&self) -> impl CacheHttp + '_ { + self.http + } + + #[cfg(feature = "cache")] + fn cache_http(&self) -> impl CacheHttp + '_ { + (self.cache, self.http) + } + /// Fills the `self.buffer` cache with [`Message`]s. /// /// This drops any messages that were currently in the buffer. Ideally, it should only be @@ -1198,7 +1147,7 @@ impl> MessagesIter { if let Some(before) = self.before { builder = builder.before(before); } - self.buffer = self.channel_id.messages(self.http.as_ref(), builder).await?; + self.buffer = self.channel_id.messages(self.cache_http(), builder).await?; self.buffer.reverse(); @@ -1224,11 +1173,11 @@ impl> MessagesIter { /// # /// # async fn run() { /// # let channel_id = ChannelId::new(1); - /// # let ctx: Http = unimplemented!(); + /// # let http: Http = unimplemented!(); /// use serenity::futures::StreamExt; /// use serenity::model::channel::MessagesIter; /// - /// let mut messages = MessagesIter::::stream(&ctx, channel_id).boxed(); + /// let mut messages = MessagesIter::stream(&http, channel_id).boxed(); /// while let Some(message_result) = messages.next().await { /// match message_result { /// Ok(message) => println!("{} said \"{}\"", message.author.name, message.content,), @@ -1238,10 +1187,10 @@ impl> MessagesIter { /// # } /// ``` pub fn stream( - http: impl AsRef, + cache_http: &'a impl CacheHttp, channel_id: ChannelId, - ) -> impl Stream> { - let init_state = MessagesIter::new(http, channel_id); + ) -> impl Stream> + 'a { + let init_state = MessagesIter::new(cache_http, channel_id); futures::stream::unfold(init_state, |mut state| async { if state.buffer.is_empty() && state.before.is_some() || !state.tried_fetch { diff --git a/src/model/channel/embed.rs b/src/model/channel/embed.rs index afc849436f7..5e84c2bc546 100644 --- a/src/model/channel/embed.rs +++ b/src/model/channel/embed.rs @@ -1,3 +1,6 @@ +use nonmax::NonMaxU32; + +use crate::internal::prelude::*; use crate::model::{Colour, Timestamp}; /// Represents a rich embed which allows using richer markdown, multiple fields and more. This was @@ -6,31 +9,31 @@ use crate::model::{Colour, Timestamp}; /// You can include an attachment in your own message by a user or a bot, or in a webhook. /// /// **Note**: Maximum amount of characters you can put is 256 in a field name, -/// 1024 in a field value, and 2048 in a description. +/// 1024 in a field value, and 4096 in a description. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#embed-object). /// /// [slack's attachments]: https://api.slack.com/docs/message-attachments #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Default, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct Embed { /// The title of the embed. #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, + pub title: Option>, /// The type of the embed. For embeds not generated by Discord's backend, this will always be /// "rich". #[serde(rename = "type")] #[serde(skip_serializing_if = "Option::is_none")] - pub kind: Option, + pub kind: Option>, /// The description of the embed. /// /// The maximum value for this field is 2048 unicode codepoints. #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, + pub description: Option>, /// The URL of the embed. #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, + pub url: Option, /// Timestamp information. #[serde(skip_serializing_if = "Option::is_none")] pub timestamp: Option, @@ -65,47 +68,47 @@ pub struct Embed { /// /// The maximum number of fields is 25. #[serde(default)] - #[serde(skip_serializing_if = "Vec::is_empty")] - pub fields: Vec, + #[serde(skip_serializing_if = "FixedArray::is_empty")] + pub fields: FixedArray, } /// An author object in an embed. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct EmbedAuthor { /// The name of the author. - pub name: String, + pub name: FixedString, /// The URL of the author. #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, + pub url: Option, /// The URL of the author icon. /// /// This only supports HTTP(S) and attachments. #[serde(skip_serializing_if = "Option::is_none")] - pub icon_url: Option, + pub icon_url: Option, /// A proxied URL of the author icon. #[serde(skip_serializing_if = "Option::is_none")] - pub proxy_icon_url: Option, + pub proxy_icon_url: Option, } /// A field object in an embed. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct EmbedField { /// The name of the field. /// /// The maximum length of this field is 512 unicode codepoints. - pub name: String, + pub name: FixedString, /// The value of the field. /// /// The maximum length of this field is 1024 unicode codepoints. - pub value: String, + pub value: FixedString, /// Indicator of whether the field should display as inline. #[serde(default)] pub inline: bool, @@ -121,10 +124,14 @@ impl EmbedField { T: Into, U: Into, { - Self::_new(name.into(), value.into(), inline) + Self::_new(name.into().trunc_into(), value.into().trunc_into(), inline) } - pub(crate) const fn _new(name: String, value: String, inline: bool) -> Self { + pub(crate) const fn _new( + name: FixedString, + value: FixedString, + inline: bool, + ) -> Self { Self { name, value, @@ -137,85 +144,85 @@ impl EmbedField { /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct EmbedFooter { /// The associated text with the footer. - pub text: String, + pub text: FixedString, /// The URL of the footer icon. /// /// This only supports HTTP(S) and attachments. #[serde(skip_serializing_if = "Option::is_none")] - pub icon_url: Option, + pub icon_url: Option, /// A proxied URL of the footer icon. #[serde(skip_serializing_if = "Option::is_none")] - pub proxy_icon_url: Option, + pub proxy_icon_url: Option, } /// An image object in an embed. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct EmbedImage { /// Source URL of the image. /// /// This only supports HTTP(S) and attachments. - pub url: String, + pub url: FixedString, /// A proxied URL of the image. - pub proxy_url: Option, + pub proxy_url: Option, /// The height of the image. - pub height: Option, + pub height: Option, /// The width of the image. - pub width: Option, + pub width: Option, } /// The provider of an embed. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#embed-object-embed-provider-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct EmbedProvider { /// The name of the provider. - pub name: Option, + pub name: Option, /// The URL of the provider. - pub url: Option, + pub url: Option, } /// The dimensions and URL of an embed thumbnail. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct EmbedThumbnail { /// The source URL of the thumbnail. /// /// This only supports HTTP(S) and attachments. - pub url: String, + pub url: FixedString, /// A proxied URL of the thumbnail. - pub proxy_url: Option, + pub proxy_url: Option, /// The height of the thumbnail in pixels. - pub height: Option, + pub height: Option, /// The width of the thumbnail in pixels. - pub width: Option, + pub width: Option, } /// Video information for an embed. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct EmbedVideo { /// The source URL of the video. - pub url: String, + pub url: FixedString, /// A proxied URL of the thumbnail. - pub proxy_url: Option, + pub proxy_url: Option, /// The height of the video in pixels. - pub height: Option, + pub height: Option, /// The width of the video in pixels. - pub width: Option, + pub width: Option, } diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index c7ce5344d0f..cdffbeb4949 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -1,10 +1,12 @@ +use std::borrow::Cow; use std::fmt; #[cfg(feature = "model")] use std::sync::Arc; +use nonmax::{NonMaxU16, NonMaxU32, NonMaxU8}; + #[cfg(feature = "model")] use crate::builder::{ - Builder, CreateAttachment, CreateForumPost, CreateInvite, @@ -27,7 +29,6 @@ use crate::collector::{MessageCollector, ReactionCollector}; use crate::gateway::ShardMessenger; #[cfg(feature = "model")] use crate::http::{CacheHttp, Http, Typing}; -#[cfg(all(feature = "cache", feature = "model"))] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -45,7 +46,7 @@ pub struct GuildChannel { /// The bitrate of the channel. /// /// **Note**: This is only available for voice and stage channels. - pub bitrate: Option, + pub bitrate: Option, /// The Id of the parent category for a channel, or of the parent text channel for a thread. /// /// **Note**: This is only available for channels in a category and thread channels. @@ -72,11 +73,11 @@ pub struct GuildChannel { /// /// **Note**: This is only available for text channels. pub last_pin_timestamp: Option, - /// The name of the channel. - pub name: String, + /// The name of the channel. (1-100 characters) + pub name: FixedString, /// Permission overwrites for [`Member`]s and for [`Role`]s. #[serde(default)] - pub permission_overwrites: Vec, + pub permission_overwrites: FixedArray, /// The position of the channel. /// /// The default text channel will _almost always_ have a position of `0`. @@ -85,11 +86,11 @@ pub struct GuildChannel { /// The topic of the channel. /// /// **Note**: This is only available for text, forum and stage channels. - pub topic: Option, + pub topic: Option>, /// The maximum number of members allowed in the channel. /// - /// **Note**: This is only available for voice channels. - pub user_limit: Option, + /// This is max 99 for voice channels and 10,000 for stage channels (0 refers to no limit). + pub user_limit: Option, /// Used to tell if the channel is not safe for work. // This field can or can not be present sometimes, but if it isn't default to `false`. #[serde(default)] @@ -99,24 +100,22 @@ pub struct GuildChannel { /// **Note**: This is only available for text channels excluding news channels. #[doc(alias = "slowmode")] #[serde(default)] - pub rate_limit_per_user: Option, + pub rate_limit_per_user: Option, /// The region override. /// /// **Note**: This is only available for voice and stage channels. [`None`] for voice and stage /// channels means automatic region selection. - pub rtc_region: Option, + pub rtc_region: Option>, /// The video quality mode for a voice channel. pub video_quality_mode: Option, /// An approximate count of messages in the thread. /// - /// This is currently saturated at 255 to prevent breaking. - /// /// **Note**: This is only available on thread channels. - pub message_count: Option, + pub message_count: Option, /// An approximate count of users in a thread, stops counting at 50. /// /// **Note**: This is only available on thread channels. - pub member_count: Option, + pub member_count: Option, /// The thread metadata. /// /// **Note**: This is only available on thread channels. @@ -138,17 +137,17 @@ pub struct GuildChannel { pub flags: ChannelFlags, /// The number of messages ever sent in a thread, it's similar to `message_count` on message /// creation, but will not decrement the number when a message is deleted. - pub total_message_sent: Option, + pub total_message_sent: Option, /// The set of available tags. /// /// **Note**: This is only available in forum channels. #[serde(default)] - pub available_tags: Vec, + pub available_tags: FixedArray, /// The set of applied tags. /// /// **Note**: This is only available in a thread in a forum. #[serde(default)] - pub applied_tags: Vec, + pub applied_tags: FixedArray, /// The emoji to show in the add reaction button /// /// **Note**: This is only available in a forum. @@ -157,11 +156,11 @@ pub struct GuildChannel { /// is copied to the thread at creation time and does not live update. /// /// **Note**: This is only available in a forum or text channel. - pub default_thread_rate_limit_per_user: Option, + pub default_thread_rate_limit_per_user: Option, /// The status of a voice channel. /// /// **Note**: This is only available in voice channels. - pub status: Option, + pub status: Option>, /// The default sort order type used to order posts /// /// **Note**: This is only available in a forum. @@ -179,11 +178,9 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object-forum-layout-types). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ForumLayoutType { /// No default has been set for forum channel. - #[default] NotSet = 0, /// Display posts as a list. ListView = 1, @@ -215,8 +212,7 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user does not have the required permissions. /// /// [Send Messages]: Permissions::SEND_MESSAGES - #[inline] - pub async fn broadcast_typing(&self, http: impl AsRef) -> Result<()> { + pub async fn broadcast_typing(&self, http: &Http) -> Result<()> { self.id.broadcast_typing(http).await } @@ -235,107 +231,33 @@ impl GuildChannel { /// /// # Errors /// - /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE - #[inline] #[cfg(feature = "utils")] pub async fn create_invite( &self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateInvite<'_>, ) -> Result { - builder.execute(cache_http, self.id).await + builder.execute(http, self.id).await } /// Creates a [permission overwrite][`PermissionOverwrite`] for either a single [`Member`] or /// [`Role`] within a [`Channel`]. /// - /// Refer to the documentation for [`PermissionOverwrite`]s for more information. - /// - /// Requires the [Manage Channels] permission. - /// - /// # Examples - /// - /// Creating a permission overwrite for a member by specifying the - /// [`PermissionOverwriteType::Member`] variant, allowing it the [Send Messages] permission, - /// but denying the [Send TTS Messages] and [Attach Files] permissions: - /// - /// ```rust,no_run - /// # #[cfg(feature = "cache")] - /// # async fn run() -> Result<(), Box> { - /// # use serenity::{cache::Cache, http::Http, model::id::{ChannelId, UserId}}; - /// # use std::sync::Arc; - /// # - /// # let http: Arc = unimplemented!(); - /// # let cache = Cache::default(); - /// # let (channel_id, user_id) = (ChannelId::new(1), UserId::new(1)); - /// use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType}; - /// use serenity::model::{ModelError, Permissions}; - /// let allow = Permissions::SEND_MESSAGES; - /// let deny = Permissions::SEND_TTS_MESSAGES | Permissions::ATTACH_FILES; - /// let overwrite = PermissionOverwrite { - /// allow, - /// deny, - /// kind: PermissionOverwriteType::Member(user_id), - /// }; - /// // assuming the cache has been unlocked - /// let channel = cache.channel(channel_id).ok_or(ModelError::ItemMissing)?; - /// - /// channel.create_permission(&http, overwrite).await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// Creating a permission overwrite for a role by specifying the - /// [`PermissionOverwriteType::Role`] variant, allowing it the [Manage Webhooks] - /// permission, but denying the [Send TTS Messages] and [Attach Files] - /// permissions: - /// - /// ```rust,no_run - /// # #[cfg(feature = "cache")] - /// # async fn run() -> Result<(), Box> { - /// # use serenity::{cache::Cache, http::Http, model::id::{ChannelId, UserId, RoleId}}; - /// # use std::sync::Arc; - /// # - /// # let http: Arc = unimplemented!(); - /// # let cache = Cache::default(); - /// # let (channel_id, user_id, role_id) = (ChannelId::new(1), UserId::new(1), RoleId::new(1)); - /// use serenity::model::channel::{Channel, PermissionOverwrite, PermissionOverwriteType}; - /// use serenity::model::{ModelError, Permissions}; - /// - /// let allow = Permissions::SEND_MESSAGES; - /// let deny = Permissions::SEND_TTS_MESSAGES | Permissions::ATTACH_FILES; - /// let overwrite = PermissionOverwrite { - /// allow, - /// deny, - /// kind: PermissionOverwriteType::Role(role_id), - /// }; - /// - /// let channel = cache.channel(channel_id).ok_or(ModelError::ItemMissing)?; - /// - /// channel.create_permission(&http, overwrite).await?; - /// # Ok(()) - /// # } - /// ``` + /// See [`ChannelId::create_permission`] for more detailed documentation. /// /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission. - /// - /// [Attach Files]: Permissions::ATTACH_FILES - /// [Manage Channels]: Permissions::MANAGE_CHANNELS - /// [Manage Webhooks]: Permissions::MANAGE_WEBHOOKS - /// [Send Messages]: Permissions::SEND_MESSAGES - /// [Send TTS Messages]: Permissions::SEND_TTS_MESSAGES - #[inline] pub async fn create_permission( &self, - http: impl AsRef, + http: &Http, target: PermissionOverwrite, + reason: Option<&str>, ) -> Result<()> { - self.id.create_permission(http, target).await + self.id.create_permission(http, target, reason).await } /// Deletes this channel, returning the channel on a successful deletion. @@ -344,21 +266,11 @@ impl GuildChannel { /// /// # Errors /// - /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user - /// does not have permission. - /// - /// Otherwise returns [`Error::Http`] if the current user lacks permission. + /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - pub async fn delete(&self, cache_http: impl CacheHttp) -> Result { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - crate::utils::user_has_perms_cache(cache, self.id, Permissions::MANAGE_CHANNELS)?; - } - } - - let channel = self.id.delete(cache_http.http()).await?; + pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result { + let channel = self.id.delete(http, reason).await?; channel.guild().ok_or(Error::Model(ModelError::InvalidChannelType)) } @@ -372,17 +284,17 @@ impl GuildChannel { /// /// # Errors /// - /// Returns [`ModelError::BulkDeleteAmount`] if an attempt was made to delete either 0 or more - /// than 100 messages. + /// Returns [`ModelError::TooSmall`] or [`ModelError::TooLarge`] if an attempt was made to + /// delete either 0 or more than 100 messages. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] - pub async fn delete_messages>( + pub async fn delete_messages( &self, - http: impl AsRef, - message_ids: impl IntoIterator, + http: &Http, + message_ids: &[MessageId], + reason: Option<&str>, ) -> Result<()> { - self.id.delete_messages(http, message_ids).await + self.id.delete_messages(http, message_ids, reason).await } /// Deletes all permission overrides in the channel from a member or role. @@ -394,13 +306,13 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Channel]: Permissions::MANAGE_CHANNELS - #[inline] pub async fn delete_permission( &self, - http: impl AsRef, + http: &Http, permission_type: PermissionOverwriteType, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_permission(http, permission_type).await + self.id.delete_permission(http, permission_type, reason).await } /// Deletes the given [`Reaction`] from the channel. @@ -412,11 +324,10 @@ impl GuildChannel { /// /// Returns [`Error::Http`] if the current user lacks permission. [Manage Messages]: /// Permissions::MANAGE_MESSAGES - #[inline] pub async fn delete_reaction( &self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, user_id: Option, reaction_type: impl Into, ) -> Result<()> { @@ -434,12 +345,7 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user lacks permission /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] - pub async fn delete_reactions( - &self, - http: impl AsRef, - message_id: impl Into, - ) -> Result<()> { + pub async fn delete_reactions(&self, http: &Http, message_id: MessageId) -> Result<()> { self.id.delete_reactions(http, message_id).await } @@ -468,17 +374,12 @@ impl GuildChannel { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS /// [Manage Roles]: Permissions::MANAGE_ROLES - pub async fn edit( - &mut self, - cache_http: impl CacheHttp, - builder: EditChannel<'_>, - ) -> Result<()> { - let channel = builder.execute(cache_http, self.id).await?; + pub async fn edit(&mut self, http: &Http, builder: EditChannel<'_>) -> Result<()> { + let channel = builder.execute(http, self.id).await?; *self = channel; Ok(()) } @@ -497,14 +398,13 @@ impl GuildChannel { /// /// See [`EditMessage::execute`] for a list of possible errors, and their corresponding /// reasons. - #[inline] pub async fn edit_message( &self, - cache_http: impl CacheHttp, - message_id: impl Into, - builder: EditMessage, + http: &Http, + message_id: MessageId, + builder: EditMessage<'_>, ) -> Result { - self.id.edit_message(cache_http, message_id, builder).await + self.id.edit_message(http, message_id, builder).await } /// Edits a thread. @@ -512,12 +412,8 @@ impl GuildChannel { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission. - pub async fn edit_thread( - &mut self, - cache_http: impl CacheHttp, - builder: EditThread<'_>, - ) -> Result<()> { - *self = self.id.edit_thread(cache_http, builder).await?; + pub async fn edit_thread(&mut self, http: &Http, builder: EditThread<'_>) -> Result<()> { + *self = self.id.edit_thread(http, builder).await?; Ok(()) } @@ -535,16 +431,18 @@ impl GuildChannel { /// # #[cfg(feature = "cache")] /// # async fn run() -> Result<(), Box> { /// # use std::sync::Arc; - /// # use serenity::{cache::Cache, http::Http, model::id::{ChannelId, UserId}}; + /// # use serenity::{cache::Cache, http::Http, model::id::{GuildId, ChannelId, UserId}}; /// # /// # let http: Http = unimplemented!(); /// # let cache = Cache::default(); - /// # let (channel_id, user_id) = (ChannelId::new(1), UserId::new(1)); + /// # let (guild_id, channel_id, user_id) = (GuildId::new(1), ChannelId::new(1), UserId::new(1)); /// use serenity::builder::EditVoiceState; /// use serenity::model::ModelError; /// - /// // assuming the cache has been unlocked - /// let channel = cache.channel(channel_id).ok_or(ModelError::ItemMissing)?; + /// let channel = { + /// let guild = cache.guild(guild_id).ok_or(ModelError::ItemMissing)?; + /// guild.channels.get(&channel_id).ok_or(ModelError::ItemMissing)?.clone() + /// }; /// /// let builder = EditVoiceState::new().suppress(false); /// channel.edit_voice_state(&http, user_id, builder).await?; @@ -562,15 +460,15 @@ impl GuildChannel { /// [Mute Members]: Permissions::MUTE_MEMBERS pub async fn edit_voice_state( &self, - cache_http: impl CacheHttp, - user_id: impl Into, + http: &Http, + user_id: UserId, builder: EditVoiceState, ) -> Result<()> { if self.kind != ChannelType::Stage { return Err(Error::from(ModelError::InvalidChannelType)); } - builder.execute(cache_http, (self.guild_id, self.id, Some(user_id.into()))).await + builder.execute(http, self.guild_id, self.id, Some(user_id)).await } /// Edits the current user's voice state in a stage channel. @@ -586,16 +484,18 @@ impl GuildChannel { /// # #[cfg(feature = "cache")] /// # async fn run() -> Result<(), Box> { /// # use std::sync::Arc; - /// # use serenity::{cache::Cache, http::Http, model::id::ChannelId}; + /// # use serenity::{cache::Cache, http::Http, model::id::{GuildId, ChannelId}}; /// # /// # let http: Http = unimplemented!(); /// # let cache = Cache::default(); - /// # let channel_id = ChannelId::new(1); + /// # let (guild_id, channel_id) = (GuildId::new(1), ChannelId::new(1)); /// use serenity::builder::EditVoiceState; /// use serenity::model::ModelError; /// - /// // assuming the cache has been unlocked - /// let channel = cache.channel(channel_id).ok_or(ModelError::ItemMissing)?; + /// let channel = { + /// let guild = cache.guild(guild_id).ok_or(ModelError::ItemMissing)?; + /// guild.channels.get(&channel_id).ok_or(ModelError::ItemMissing)?.clone() + /// }; /// /// // Send a request to speak /// let builder = EditVoiceState::new().request_to_speak(true); @@ -617,12 +517,8 @@ impl GuildChannel { /// /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK /// [Mute Members]: Permissions::MUTE_MEMBERS - pub async fn edit_own_voice_state( - &self, - cache_http: impl CacheHttp, - builder: EditVoiceState, - ) -> Result<()> { - builder.execute(cache_http, (self.guild_id, self.id, None)).await + pub async fn edit_own_voice_state(&self, http: &Http, builder: EditVoiceState) -> Result<()> { + builder.execute(http, self.guild_id, self.id, None).await } /// Follows the News Channel @@ -635,20 +531,18 @@ impl GuildChannel { /// /// Returns [`Error::Http`] if the current user lacks permission. /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] pub async fn follow( &self, - http: impl AsRef, - target_channel_id: impl Into, + http: &Http, + target_channel_id: ChannelId, ) -> Result { self.id.follow(http, target_channel_id).await } /// Attempts to find this channel's guild in the Cache. #[cfg(feature = "cache")] - #[inline] - pub fn guild<'a>(&self, cache: &'a impl AsRef) -> Option> { - cache.as_ref().guild(self.guild_id) + pub fn guild<'a>(&self, cache: &'a Cache) -> Option> { + cache.guild(self.guild_id) } /// Gets all of the channel's invites. @@ -660,19 +554,10 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - #[inline] - pub async fn invites(&self, http: impl AsRef) -> Result> { + pub async fn invites(&self, http: &Http) -> Result> { self.id.invites(http).await } - /// Determines if the channel is NSFW. - #[inline] - #[must_use] - #[deprecated = "Use the GuildChannel::nsfw field"] - pub fn is_nsfw(&self) -> bool { - self.nsfw - } - /// Gets a message from the channel. /// /// Requires the [Read Message History] permission. @@ -683,11 +568,10 @@ impl GuildChannel { /// given Id does not exist in the channel. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - #[inline] pub async fn message( &self, cache_http: impl CacheHttp, - message_id: impl Into, + message_id: MessageId, ) -> Result { self.id.message(cache_http, message_id).await } @@ -702,19 +586,8 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - #[inline] - pub async fn messages( - &self, - cache_http: impl CacheHttp, - builder: GetMessages, - ) -> Result> { - self.id.messages(cache_http, builder).await - } - - /// Returns the name of the guild channel. - #[must_use] - pub fn name(&self) -> &str { - &self.name + pub async fn messages(&self, http: &Http, builder: GetMessages) -> Result> { + self.id.messages(http, builder).await } /// Calculates the permissions of a member. @@ -733,12 +606,15 @@ impl GuildChannel { /// #[serenity::async_trait] /// impl EventHandler for Handler { /// async fn message(&self, context: Context, msg: Message) { - /// let channel = match context.cache.channel(msg.channel_id) { - /// Some(channel) => channel, - /// None => return, + /// let Some(guild) = msg.guild(&context.cache) else { + /// return; /// }; /// - /// if let Ok(permissions) = channel.permissions_for_user(&context.cache, &msg.author) { + /// let Some(channel) = guild.channels.get(&msg.channel_id) else { + /// return; + /// }; + /// + /// if let Ok(permissions) = channel.permissions_for_user(&context.cache, msg.author.id) { /// println!("The user's permissions: {:?}", permissions); /// } /// } @@ -753,44 +629,12 @@ impl GuildChannel { /// [Attach Files]: Permissions::ATTACH_FILES /// [Send Messages]: Permissions::SEND_MESSAGES #[cfg(feature = "cache")] - #[inline] - pub fn permissions_for_user( - &self, - cache: impl AsRef, - user_id: impl Into, - ) -> Result { - let guild = self.guild(&cache).ok_or(Error::Model(ModelError::GuildNotFound))?; - let member = - guild.members.get(&user_id.into()).ok_or(Error::Model(ModelError::MemberNotFound))?; + pub fn permissions_for_user(&self, cache: &Cache, user_id: UserId) -> Result { + let guild = self.guild(cache).ok_or(Error::Model(ModelError::GuildNotFound))?; + let member = guild.members.get(&user_id).ok_or(Error::Model(ModelError::MemberNotFound))?; Ok(guild.user_permissions_in(self, member)) } - /// Calculates the permissions of a role. - /// - /// The Id of the argument must be a [`Role`] of the [`Guild`] that the channel is in. - /// - /// # Errors - /// - /// Returns a [`ModelError::GuildNotFound`] if the channel's guild could not be found in the - /// [`Cache`]. - /// - /// Returns a [`ModelError::RoleNotFound`] if the given role could not be found in the - /// [`Cache`]. - #[cfg(feature = "cache")] - #[inline] - #[deprecated = "this function ignores other roles the user may have as well as user-specific permissions; use Guild::user_permissions_in instead"] - #[allow(deprecated)] - pub fn permissions_for_role( - &self, - cache: impl AsRef, - role_id: impl Into, - ) -> Result { - let guild = self.guild(&cache).ok_or(Error::Model(ModelError::GuildNotFound))?; - let role = - guild.roles.get(&role_id.into()).ok_or(Error::Model(ModelError::RoleNotFound))?; - guild.role_permissions_in(self, role) - } - /// Pins a [`Message`] to the channel. /// /// **Note**: Requires the [Manage Messages] permission. @@ -801,13 +645,13 @@ impl GuildChannel { /// too many pinned messages. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] pub async fn pin( &self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, + reason: Option<&str>, ) -> Result<()> { - self.id.pin(http, message_id).await + self.id.pin(http, message_id, reason).await } /// Gets all channel's pins. @@ -820,8 +664,7 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user lacks permission to view the channel. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - #[inline] - pub async fn pins(&self, http: impl AsRef) -> Result> { + pub async fn pins(&self, http: &Http) -> Result> { self.id.pins(http).await } @@ -847,11 +690,11 @@ impl GuildChannel { /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY pub async fn reaction_users( &self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, reaction_type: impl Into, limit: Option, - after: impl Into>, + after: Option, ) -> Result> { self.id.reaction_users(http, message_id, reaction_type, limit, after).await } @@ -862,15 +705,10 @@ impl GuildChannel { /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content length is over the above limit. See + /// Returns a [`ModelError::TooLarge`] if the content length is over the above limit. See /// [`CreateMessage::execute`] for more details. - #[inline] - pub async fn say( - &self, - cache_http: impl CacheHttp, - content: impl Into, - ) -> Result { - self.id.say(cache_http, content).await + pub async fn say(&self, http: &Http, content: impl Into>) -> Result { + self.id.say(http, content).await } /// Sends file(s) along with optional message contents. @@ -881,14 +719,13 @@ impl GuildChannel { /// /// See [`CreateMessage::execute`] for a list of possible errors, and their corresponding /// reasons. - #[inline] - pub async fn send_files( + pub async fn send_files<'a>( self, - cache_http: impl CacheHttp, - files: impl IntoIterator, - builder: CreateMessage, + http: &Http, + files: impl IntoIterator>, + builder: CreateMessage<'a>, ) -> Result { - self.send_message(cache_http, builder.files(files)).await + self.send_message(http, builder.files(files)).await } /// Sends a message to the channel. @@ -900,12 +737,8 @@ impl GuildChannel { /// /// See [`CreateMessage::execute`] for a list of possible errors, and their corresponding /// reasons. - pub async fn send_message( - &self, - cache_http: impl CacheHttp, - builder: CreateMessage, - ) -> Result { - builder.execute(cache_http, (self.id, Some(self.guild_id))).await + pub async fn send_message(&self, http: &Http, builder: CreateMessage<'_>) -> Result { + builder.execute(http, self.id, Some(self.guild_id)).await } /// Starts typing in the channel for an indefinite period of time. @@ -935,7 +768,7 @@ impl GuildChannel { /// # let cache = Cache::default(); /// # let channel: GuildChannel = unimplemented!(); /// // Initiate typing (assuming http is `Arc` and `channel` is bound) - /// let typing = channel.start_typing(&http); + /// let typing = channel.start_typing(http); /// /// // Run some long-running process /// long_process(); @@ -945,8 +778,8 @@ impl GuildChannel { /// # } /// ``` #[allow(clippy::missing_errors_doc)] - pub fn start_typing(&self, http: &Arc) -> Typing { - http.start_typing(self.id) + pub fn start_typing(&self, http: Arc) -> Typing { + self.id.start_typing(http) } /// Unpins a [`Message`] in the channel given by its Id. @@ -958,13 +791,13 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] pub async fn unpin( &self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, + reason: Option<&str>, ) -> Result<()> { - self.id.unpin(http, message_id).await + self.id.unpin(http, message_id, reason).await } /// Retrieves the channel's webhooks. @@ -976,8 +809,7 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Webhooks]: Permissions::MANAGE_WEBHOOKS - #[inline] - pub async fn webhooks(&self, http: impl AsRef) -> Result> { + pub async fn webhooks(&self, http: &Http) -> Result> { self.id.webhooks(http).await } @@ -993,15 +825,13 @@ impl GuildChannel { /// Other [`ChannelType`]s lack the concept of [`Member`]s and will return: /// [`ModelError::InvalidChannelType`]. #[cfg(feature = "cache")] - #[inline] - pub fn members(&self, cache: impl AsRef) -> Result> { - let cache = cache.as_ref(); + pub fn members(&self, cache: &Cache) -> Result> { let guild = cache.guild(self.guild_id).ok_or(ModelError::GuildNotFound)?; match self.kind { ChannelType::Voice | ChannelType::Stage => Ok(guild .voice_states - .values() + .iter() .filter_map(|v| { v.channel_id.and_then(|c| { if c == self.id { @@ -1015,12 +845,12 @@ impl GuildChannel { ChannelType::News | ChannelType::Text => Ok(guild .members .iter() - .filter(|e| { - self.permissions_for_user(cache, e.0) + .filter(|m| { + self.permissions_for_user(cache, m.user.id) .map(|p| p.contains(Permissions::VIEW_CHANNEL)) .unwrap_or(false) }) - .map(|e| e.1.clone()) + .cloned() .collect::>()), _ => Err(Error::from(ModelError::InvalidChannelType)), } @@ -1029,29 +859,26 @@ impl GuildChannel { /// Returns a builder which can be awaited to obtain a message or stream of messages sent in /// this guild channel. #[cfg(feature = "collector")] - pub fn await_reply(&self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_reply(&self, shard_messenger: ShardMessenger) -> MessageCollector { MessageCollector::new(shard_messenger).channel_id(self.id) } /// Same as [`Self::await_reply`]. #[cfg(feature = "collector")] - pub fn await_replies(&self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_replies(&self, shard_messenger: ShardMessenger) -> MessageCollector { self.await_reply(shard_messenger) } /// Returns a stream builder which can be awaited to obtain a reaction or stream of reactions /// sent by this guild channel. #[cfg(feature = "collector")] - pub fn await_reaction(&self, shard_messenger: impl AsRef) -> ReactionCollector { + pub fn await_reaction(&self, shard_messenger: ShardMessenger) -> ReactionCollector { ReactionCollector::new(shard_messenger).channel_id(self.id) } /// Same as [`Self::await_reaction`]. #[cfg(feature = "collector")] - pub fn await_reactions( - &self, - shard_messenger: impl AsRef, - ) -> ReactionCollector { + pub fn await_reactions(&self, shard_messenger: ShardMessenger) -> ReactionCollector { self.await_reaction(shard_messenger) } @@ -1064,18 +891,14 @@ impl GuildChannel { /// /// See [`CreateWebhook::execute`] for a detailed list of other /// possible errors, - pub async fn create_webhook( - &self, - cache_http: impl CacheHttp, - builder: CreateWebhook<'_>, - ) -> Result { + pub async fn create_webhook(&self, http: &Http, builder: CreateWebhook<'_>) -> Result { // forum channels are not text-based, but webhooks can be created in them // and used to send messages in their posts if !self.is_text_based() && self.kind != ChannelType::Forum { return Err(Error::Model(ModelError::InvalidChannelType)); } - self.id.create_webhook(cache_http, builder).await + self.id.create_webhook(http, builder).await } /// Gets a stage instance. @@ -1085,7 +908,7 @@ impl GuildChannel { /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. /// /// Returns [`Error::Http`] if there is no stage instance currently. - pub async fn get_stage_instance(&self, http: impl AsRef) -> Result { + pub async fn get_stage_instance(&self, http: &Http) -> Result { if self.kind != ChannelType::Stage { return Err(Error::Model(ModelError::InvalidChannelType)); } @@ -1102,14 +925,14 @@ impl GuildChannel { /// Returns [`Error::Http`] if there is already a stage instance currently. pub async fn create_stage_instance( &self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateStageInstance<'_>, ) -> Result { if self.kind != ChannelType::Stage { return Err(Error::Model(ModelError::InvalidChannelType)); } - self.id.create_stage_instance(cache_http, builder).await + self.id.create_stage_instance(http, builder).await } /// Edits the stage instance @@ -1122,14 +945,14 @@ impl GuildChannel { /// instance currently. pub async fn edit_stage_instance( &self, - cache_http: impl CacheHttp, + http: &Http, builder: EditStageInstance<'_>, ) -> Result { if self.kind != ChannelType::Stage { return Err(Error::Model(ModelError::InvalidChannelType)); } - self.id.edit_stage_instance(cache_http, builder).await + self.id.edit_stage_instance(http, builder).await } /// Deletes a stage instance. @@ -1139,12 +962,12 @@ impl GuildChannel { /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. /// /// Returns [`Error::Http`] if there is no stage instance currently. - pub async fn delete_stage_instance(&self, http: impl AsRef) -> Result<()> { + pub async fn delete_stage_instance(&self, http: &Http, reason: Option<&str>) -> Result<()> { if self.kind != ChannelType::Stage { return Err(Error::Model(ModelError::InvalidChannelType)); } - self.id.delete_stage_instance(http).await + self.id.delete_stage_instance(http, reason).await } /// Creates a public thread that is connected to a message. @@ -1154,11 +977,11 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. pub async fn create_thread_from_message( &self, - cache_http: impl CacheHttp, - message_id: impl Into, + http: &Http, + message_id: MessageId, builder: CreateThread<'_>, ) -> Result { - self.id.create_thread_from_message(cache_http, message_id, builder).await + self.id.create_thread_from_message(http, message_id, builder).await } /// Creates a thread that is not connected to a message. @@ -1168,10 +991,10 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. pub async fn create_thread( &self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateThread<'_>, ) -> Result { - self.id.create_thread(cache_http, builder).await + self.id.create_thread(http, builder).await } /// Creates a post in a forum channel. @@ -1181,10 +1004,10 @@ impl GuildChannel { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. pub async fn create_forum_post( &self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateForumPost<'_>, ) -> Result { - self.id.create_forum_post(cache_http, builder).await + self.id.create_forum_post(http, builder).await } } @@ -1195,6 +1018,12 @@ impl fmt::Display for GuildChannel { } } +impl ExtractKey for GuildChannel { + fn extract_key(&self) -> &ChannelId { + &self.id + } +} + /// A partial guild channel. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object), diff --git a/src/model/channel/message.rs b/src/model/channel/message.rs index c8dc8495510..81d1a52d62e 100644 --- a/src/model/channel/message.rs +++ b/src/model/channel/message.rs @@ -1,12 +1,13 @@ //! Models relating to Discord channels. +use std::borrow::Cow; #[cfg(feature = "model")] use std::fmt::Display; -#[cfg(all(feature = "cache", feature = "model"))] -use std::fmt::Write; + +use nonmax::NonMaxU64; #[cfg(all(feature = "model", feature = "utils"))] -use crate::builder::{Builder, CreateAllowedMentions, CreateMessage, EditMessage}; +use crate::builder::{CreateAllowedMentions, CreateMessage, EditMessage}; #[cfg(all(feature = "cache", feature = "model"))] use crate::cache::{Cache, GuildRef}; #[cfg(feature = "collector")] @@ -21,17 +22,17 @@ use crate::constants; use crate::gateway::ShardMessenger; #[cfg(feature = "model")] use crate::http::{CacheHttp, Http}; +use crate::internal::prelude::*; use crate::model::prelude::*; use crate::model::utils::{discord_colours, StrOrInt}; -#[cfg(all(feature = "model", feature = "cache"))] -use crate::utils; /// A representation of a message over a guild's text channel, a group, or a private channel. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#message-object) with some /// [extra fields](https://discord.com/developers/docs/topics/gateway-events#message-create-message-create-extra-fields). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct Message { /// The unique Id of the message. Can be used to calculate the creation date of the message. @@ -41,7 +42,7 @@ pub struct Message { /// The user that sent the message. pub author: User, /// The content of the message. - pub content: String, + pub content: FixedString, /// Initial message creation timestamp, calculated from its Id. pub timestamp: Timestamp, /// The timestamp of the last time the message was updated, if it was. @@ -53,9 +54,9 @@ pub struct Message { /// Indicator of whether the message mentions everyone. pub mention_everyone: bool, /// Array of users mentioned in the message. - pub mentions: Vec, + pub mentions: FixedArray, /// Array of [`Role`]s' Ids mentioned in the message. - pub mention_roles: Vec, + pub mention_roles: FixedArray, /// Channels specifically mentioned in this message. /// /// **Note**: Not all channel mentions in a message will appear in [`Self::mention_channels`]. @@ -73,15 +74,15 @@ pub struct Message { /// [Refer to Discord's documentation for more information][discord-docs]. /// /// [discord-docs]: https://discord.com/developers/docs/resources/channel#message-object - #[serde(default = "Vec::new")] - pub mention_channels: Vec, + #[serde(default)] + pub mention_channels: FixedArray, /// An vector of the files attached to a message. - pub attachments: Vec, + pub attachments: FixedArray, /// Array of embeds sent with the message. - pub embeds: Vec, + pub embeds: FixedArray, /// Array of reactions performed on the message. #[serde(default)] - pub reactions: Vec, + pub reactions: FixedArray, /// Non-repeating number used for ensuring message order. #[serde(default)] pub nonce: Option, @@ -106,28 +107,25 @@ pub struct Message { pub flags: Option, /// The message that was replied to using this message. pub referenced_message: Option>, // Boxed to avoid recursion - #[cfg_attr( - all(not(ignore_serenity_deprecated), feature = "unstable_discord_api"), - deprecated = "Use interaction_metadata" - )] + #[cfg(not(feature = "unstable"))] pub interaction: Option>, /// Sent if the message is a response to an [`Interaction`]. /// /// [`Interaction`]: crate::model::application::Interaction - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] pub interaction_metadata: Option>, /// The thread that was started from this message, includes thread member object. - pub thread: Option, + pub thread: Option>, /// The components of this message #[serde(default)] - pub components: Vec, + pub components: FixedArray, /// Array of message sticker item objects. #[serde(default)] - pub sticker_items: Vec, + pub sticker_items: FixedArray, /// A generally increasing integer (there may be gaps or duplicates) that represents the /// approximate position of the message in a thread, it can be used to estimate the relative /// position of the message in a thread in company with total_message_sent on parent thread. - pub position: Option, + pub position: Option, /// Data of the role subscription purchase or renewal that prompted this /// [`MessageType::RoleSubscriptionPurchase`] message. pub role_subscription_data: Option, @@ -163,29 +161,13 @@ impl Message { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have the required permissions. - /// /// Returns a [`ModelError::MessageAlreadyCrossposted`] if the message has already been /// crossposted. /// /// Returns a [`ModelError::CannotCrosspostMessage`] if the message cannot be crossposted. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn crosspost(&self, cache_http: impl CacheHttp) -> Result { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - if self.author.id != cache.current_user().id && self.guild_id.is_some() { - utils::user_has_perms_cache( - cache, - self.channel_id, - Permissions::MANAGE_MESSAGES, - )?; - } - } - } - + pub async fn crosspost(&self, http: &Http) -> Result { if let Some(flags) = self.flags { if flags.contains(MessageFlags::CROSSPOSTED) { return Err(Error::Model(ModelError::MessageAlreadyCrossposted)); @@ -196,7 +178,7 @@ impl Message { } } - self.channel_id.crosspost(cache_http.http(), self.id).await + self.channel_id.crosspost(http, self.id).await } /// First attempts to find a [`Channel`] by its Id in the cache, upon failure requests it via @@ -208,18 +190,10 @@ impl Message { /// # Errors /// /// Can return an error if the HTTP request fails. - #[inline] pub async fn channel(&self, cache_http: impl CacheHttp) -> Result { self.channel_id.to_channel(cache_http).await } - /// A util function for determining whether this message was sent by someone else, or the bot. - #[cfg(feature = "cache")] - #[deprecated = "Check Message::author is equal to Cache::current_user"] - pub fn is_own(&self, cache: impl AsRef) -> bool { - self.author.id == cache.as_ref().current_user().id - } - /// Deletes the message. /// /// **Note**: The logged in user must either be the author of the message or have the [Manage @@ -227,25 +201,11 @@ impl Message { /// /// # Errors /// - /// If the `cache` feature is enabled, then returns a [`ModelError::InvalidPermissions`] if the - /// current user does not have the required permissions. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn delete(&self, cache_http: impl CacheHttp) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - if self.author.id != cache.current_user().id { - utils::user_has_perms_cache( - cache, - self.channel_id, - Permissions::MANAGE_MESSAGES, - )?; - } - } - } - - self.channel_id.delete_message(cache_http.http(), self.id).await + pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result<()> { + self.channel_id.delete_message(http, self.id, reason).await } /// Deletes all of the [`Reaction`]s associated with the message. @@ -254,19 +214,11 @@ impl Message { /// /// # Errors /// - /// If the `cache` feature is enabled, then returns a [`ModelError::InvalidPermissions`] if the - /// current user does not have the required permissions. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn delete_reactions(&self, cache_http: impl CacheHttp) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - utils::user_has_perms_cache(cache, self.channel_id, Permissions::MANAGE_MESSAGES)?; - } - } - - self.channel_id.delete_reactions(cache_http.http(), self.id).await + pub async fn delete_reactions(&self, http: &Http) -> Result<()> { + self.channel_id.delete_reactions(http, self.id).await } /// Deletes the given [`Reaction`] from the message. @@ -280,10 +232,9 @@ impl Message { /// permission. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] pub async fn delete_reaction( &self, - http: impl AsRef, + http: &Http, user_id: Option, reaction_type: impl Into, ) -> Result<()> { @@ -296,27 +247,15 @@ impl Message { /// /// # Errors /// - /// If the `cache` feature is enabled, then returns a [`ModelError::InvalidPermissions`] if the - /// current user does not have the required permissions. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES pub async fn delete_reaction_emoji( &self, - cache_http: impl CacheHttp, + http: &Http, reaction_type: impl Into, ) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - utils::user_has_perms_cache(cache, self.channel_id, Permissions::MANAGE_MESSAGES)?; - } - } - - cache_http - .http() - .as_ref() - .delete_message_reaction_emoji(self.channel_id, self.id, &reaction_type.into()) - .await + http.delete_message_reaction_emoji(self.channel_id, self.id, &reaction_type.into()).await } /// Edits this message, replacing the original content with new content. @@ -355,66 +294,36 @@ impl Message { /// the author. Otherwise returns [`Error::Http`] if the user lacks permission, as well as if /// invalid data is given. /// - /// Returns a [`ModelError::MessageTooLong`] if the message contents are too long. + /// Returns a [`ModelError::TooLarge`] if the message contents are too long. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn edit(&mut self, cache_http: impl CacheHttp, builder: EditMessage) -> Result<()> { + pub async fn edit( + &mut self, + cache_http: impl CacheHttp, + builder: EditMessage<'_>, + ) -> Result<()> { if let Some(flags) = self.flags { if flags.contains(MessageFlags::IS_VOICE_MESSAGE) { return Err(Error::Model(ModelError::CannotEditVoiceMessage)); } } - *self = - builder.execute(cache_http, (self.channel_id, self.id, Some(self.author.id))).await?; + *self = builder.execute(cache_http, self.channel_id, self.id, Some(self.author.id)).await?; Ok(()) } /// Returns message content, but with user and role mentions replaced with /// names and everyone/here mentions cancelled. - #[cfg(feature = "cache")] - pub fn content_safe(&self, cache: impl AsRef) -> String { - let mut result = self.content.clone(); - - // First replace all user mentions. - for u in &self.mentions { - let mut at_distinct = String::with_capacity(38); - at_distinct.push('@'); - at_distinct.push_str(&u.name); - if let Some(discriminator) = u.discriminator { - at_distinct.push('#'); - write!(at_distinct, "{:04}", discriminator.get()).unwrap(); - } - - let mut m = u.mention().to_string(); - // Check whether we're replacing a nickname mention or a normal mention. - // `UserId::mention` returns a normal mention. If it isn't present in the message, it's - // a nickname mention. - if !result.contains(&m) { - m.insert(2, '!'); - } - - result = result.replace(&m, &at_distinct); - } - - // Then replace all role mentions. - if let Some(guild_id) = self.guild_id { - for id in &self.mention_roles { - let mention = id.mention().to_string(); + #[cfg(all(feature = "cache", feature = "utils"))] + pub fn content_safe(&self, cache: &Cache) -> String { + let Some(guild) = self.guild(cache) else { return self.content.to_string() }; - if let Some(guild) = cache.as_ref().guild(guild_id) { - if let Some(role) = guild.roles.get(id) { - result = result.replace(&mention, &format!("@{}", role.name)); - continue; - } - } + let options = crate::utils::ContentSafeOptions::new() + .clean_user(true) + .clean_role(true) + .clean_everyone(true); - result = result.replace(&mention, "@deleted-role"); - } - } - - // And finally replace everyone and here mentions. - result.replace("@everyone", "@\u{200B}everyone").replace("@here", "@\u{200B}here") + crate::utils::content_safe(&guild, &self.content, options, &self.mentions) } /// Gets the list of [`User`]s who have reacted to a [`Message`] with a certain [`Emoji`]. @@ -437,13 +346,12 @@ impl Message { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - #[inline] pub async fn reaction_users( &self, - http: impl AsRef, + http: &Http, reaction_type: impl Into, limit: Option, - after: impl Into>, + after: Option, ) -> Result> { self.channel_id.reaction_users(http, self.id, reaction_type, limit, after).await } @@ -459,18 +367,6 @@ impl Message { cache.guild(self.guild_id?) } - /// True if message was sent using direct messages. - /// - /// **Only use this for messages from the gateway (event handler)!** Not for returned Message - /// objects from HTTP requests, like [`ChannelId::send_message`], because [`Self::guild_id`] is - /// never set for those, which this method relies on. - #[inline] - #[must_use] - #[deprecated = "Check if guild_id is None if the message is received from the gateway."] - pub fn is_private(&self) -> bool { - self.guild_id.is_none() - } - /// Retrieves a clone of the author's Member instance, if this message was sent in a guild. /// /// If the instance cannot be found in the cache, or the `cache` feature is disabled, a HTTP @@ -492,7 +388,12 @@ impl Message { /// inner value of how many unicode code points the message is over. #[must_use] pub fn overflow_length(content: &str) -> Option { - crate::builder::check_overflow(content.chars().count(), constants::MESSAGE_CODE_LIMIT).err() + let char_count = content.chars().count(); + if char_count > constants::MESSAGE_CODE_LIMIT { + Some(constants::MESSAGE_CODE_LIMIT - char_count) + } else { + None + } } /// Pins this message to its channel. @@ -501,25 +402,11 @@ impl Message { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have the required permissions. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn pin(&self, cache_http: impl CacheHttp) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - if self.guild_id.is_some() { - utils::user_has_perms_cache( - cache, - self.channel_id, - Permissions::MANAGE_MESSAGES, - )?; - } - } - } - - self.channel_id.pin(cache_http.http(), self.id).await + pub async fn pin(&self, http: &Http, reason: Option<&str>) -> Result<()> { + self.channel_id.pin(http, self.id, reason).await } /// React to the message with a custom [`Emoji`] or unicode character. @@ -528,96 +415,29 @@ impl Message { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have the required [permissions]. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Add Reactions]: Permissions::ADD_REACTIONS - /// [permissions]: crate::model::permissions - #[inline] - pub async fn react( - &self, - cache_http: impl CacheHttp, - reaction_type: impl Into, - ) -> Result { - self._react(cache_http, reaction_type.into(), false).await + pub async fn react(&self, http: &Http, reaction_type: impl Into) -> Result<()> { + http.create_reaction(self.channel_id, self.id, &reaction_type.into()).await } /// React to the message with a custom [`Emoji`] or unicode character. /// - /// **Note**: Requires [Add Reactions] and [Use External Emojis] permissions. + /// **Note**: Requires [Add Reactions] and [Use External Emojis] permissions. /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have the required [permissions]. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Add Reactions]: Permissions::ADD_REACTIONS /// [Use External Emojis]: Permissions::USE_EXTERNAL_EMOJIS - /// [permissions]: crate::model::permissions - #[inline] pub async fn super_react( &self, - cache_http: impl CacheHttp, + http: &Http, reaction_type: impl Into, - ) -> Result { - self._react(cache_http, reaction_type.into(), true).await - } - - async fn _react( - &self, - cache_http: impl CacheHttp, - reaction_type: ReactionType, - burst: bool, - ) -> Result { - #[cfg_attr(not(feature = "cache"), allow(unused_mut))] - let mut user_id = None; - - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - if self.guild_id.is_some() { - utils::user_has_perms_cache( - cache, - self.channel_id, - Permissions::ADD_REACTIONS, - )?; - - if burst { - utils::user_has_perms_cache( - cache, - self.channel_id, - Permissions::USE_EXTERNAL_EMOJIS, - )?; - } - } - - user_id = Some(cache.current_user().id); - } - } - - let reaction_types = if burst { - cache_http - .http() - .create_super_reaction(self.channel_id, self.id, &reaction_type) - .await?; - ReactionTypes::Burst - } else { - cache_http.http().create_reaction(self.channel_id, self.id, &reaction_type).await?; - ReactionTypes::Normal - }; - - Ok(Reaction { - channel_id: self.channel_id, - emoji: reaction_type, - message_id: self.id, - user_id, - guild_id: self.guild_id, - member: self.member.as_deref().map(|member| member.clone().into()), - message_author_id: None, - burst, - burst_colours: None, - reaction_type: reaction_types, - }) + ) -> Result<()> { + http.create_super_reaction(self.channel_id, self.id, &reaction_type.into()).await } /// Uses Discord's inline reply to a user without pinging them. @@ -630,20 +450,12 @@ impl Message { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have the required permissions. - /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message is over the above + /// Returns a [`ModelError::TooLarge`] if the content of the message is over the above /// limit, containing the number of unicode code points over the limit. /// /// [Send Messages]: Permissions::SEND_MESSAGES - #[inline] - pub async fn reply( - &self, - cache_http: impl CacheHttp, - content: impl Into, - ) -> Result { - self._reply(cache_http, content, Some(false)).await + pub async fn reply(&self, http: &Http, content: impl Into>) -> Result { + self._reply(http, content, Some(false)).await } /// Uses Discord's inline reply to a user with a ping. @@ -654,20 +466,18 @@ impl Message { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have the required permissions. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message is over the above + /// Returns a [`ModelError::TooLarge`] if the content of the message is over the above /// limit, containing the number of unicode code points over the limit. /// /// [Send Messages]: Permissions::SEND_MESSAGES - #[inline] pub async fn reply_ping( &self, - cache_http: impl CacheHttp, - content: impl Into, + http: &Http, + content: impl Into>, ) -> Result { - self._reply(cache_http, content, Some(true)).await + self._reply(http, content, Some(true)).await } /// Replies to the user, mentioning them prior to the content in the form of: `@ @@ -681,42 +491,23 @@ impl Message { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have the required permissions. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message is over the above + /// Returns a [`ModelError::TooLarge`] if the content of the message is over the above /// limit, containing the number of unicode code points over the limit. /// /// [Send Messages]: Permissions::SEND_MESSAGES - #[inline] - pub async fn reply_mention( - &self, - cache_http: impl CacheHttp, - content: impl Display, - ) -> Result { - self._reply(cache_http, format!("{} {content}", self.author.mention()), None).await + pub async fn reply_mention(&self, http: &Http, content: impl Display) -> Result { + self._reply(http, format!("{} {content}", self.author.mention()), None).await } /// `inlined` decides whether this reply is inlined and whether it pings. async fn _reply( &self, - cache_http: impl CacheHttp, - content: impl Into, + http: &Http, + content: impl Into>, inlined: Option, ) -> Result { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - if self.guild_id.is_some() { - utils::user_has_perms_cache( - cache, - self.channel_id, - Permissions::SEND_MESSAGES, - )?; - } - } - } - let mut builder = CreateMessage::new().content(content); if let Some(ping_user) = inlined { let allowed_mentions = CreateAllowedMentions::new() @@ -728,18 +519,16 @@ impl Message { .all_roles(true); builder = builder.reference_message(self).allowed_mentions(allowed_mentions); } - self.channel_id.send_message(cache_http, builder).await + self.channel_id.send_message(http, builder).await } /// Checks whether the message mentions passed [`UserId`]. - #[inline] - pub fn mentions_user_id(&self, id: impl Into) -> bool { - let id = id.into(); + #[must_use] + pub fn mentions_user_id(&self, id: UserId) -> bool { self.mentions.iter().any(|mentioned_user| mentioned_user.id == id) } /// Checks whether the message mentions passed [`User`]. - #[inline] #[must_use] pub fn mentions_user(&self, user: &User) -> bool { self.mentions_user_id(user.id) @@ -769,25 +558,11 @@ impl Message { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have the required permissions. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn unpin(&self, cache_http: impl CacheHttp) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - if self.guild_id.is_some() { - utils::user_has_perms_cache( - cache, - self.channel_id, - Permissions::MANAGE_MESSAGES, - )?; - } - } - } - - cache_http.http().unpin_message(self.channel_id, self.id, None).await + pub async fn unpin(&self, http: &Http, reason: Option<&str>) -> Result<()> { + http.unpin_message(self.channel_id, self.id, reason).await } /// Ends the [`Poll`] on this message, if there is one. @@ -802,42 +577,27 @@ impl Message { /// Tries to return author's nickname in the current channel's guild. /// /// Refer to [`User::nick_in()`] inside and [`None`] outside of a guild. - #[inline] pub async fn author_nick(&self, cache_http: impl CacheHttp) -> Option { self.author.nick_in(cache_http, self.guild_id?).await } /// Returns a link referencing this message. When clicked, users will jump to the message. The /// link will be valid for messages in either private channels or guilds. - #[inline] #[must_use] pub fn link(&self) -> String { self.id.link(self.channel_id, self.guild_id) } - /// Same as [`Self::link`] but tries to find the [`GuildId`] if Discord does not provide it. - /// - /// [`guild_id`]: Self::guild_id - #[inline] - #[allow(deprecated)] - #[deprecated = "Use Self::link if Message was recieved via an event, otherwise use MessageId::link to provide the guild_id yourself."] - pub async fn link_ensured(&self, cache_http: impl CacheHttp) -> String { - self.id.link_ensured(cache_http, self.channel_id, self.guild_id).await - } - /// Returns a builder which can be awaited to obtain a reaction or stream of reactions on this /// message. #[cfg(feature = "collector")] - pub fn await_reaction(&self, shard_messenger: impl AsRef) -> ReactionCollector { + pub fn await_reaction(&self, shard_messenger: ShardMessenger) -> ReactionCollector { ReactionCollector::new(shard_messenger).message_id(self.id) } /// Same as [`Self::await_reaction`]. #[cfg(feature = "collector")] - pub fn await_reactions( - &self, - shard_messenger: impl AsRef, - ) -> ReactionCollector { + pub fn await_reactions(&self, shard_messenger: ShardMessenger) -> ReactionCollector { self.await_reaction(shard_messenger) } @@ -846,7 +606,7 @@ impl Message { #[cfg(feature = "collector")] pub fn await_component_interaction( &self, - shard_messenger: impl AsRef, + shard_messenger: ShardMessenger, ) -> ComponentInteractionCollector { ComponentInteractionCollector::new(shard_messenger).message_id(self.id) } @@ -855,7 +615,7 @@ impl Message { #[cfg(feature = "collector")] pub fn await_component_interactions( &self, - shard_messenger: impl AsRef, + shard_messenger: ShardMessenger, ) -> ComponentInteractionCollector { self.await_component_interaction(shard_messenger) } @@ -865,7 +625,7 @@ impl Message { #[cfg(feature = "collector")] pub fn await_modal_interaction( &self, - shard_messenger: impl AsRef, + shard_messenger: ShardMessenger, ) -> ModalInteractionCollector { ModalInteractionCollector::new(shard_messenger).message_id(self.id) } @@ -874,7 +634,7 @@ impl Message { #[cfg(feature = "collector")] pub fn await_modal_interactions( &self, - shard_messenger: impl AsRef, + shard_messenger: ShardMessenger, ) -> ModalInteractionCollector { self.await_modal_interaction(shard_messenger) } @@ -904,8 +664,8 @@ impl Message { } } -impl AsRef for Message { - fn as_ref(&self) -> &MessageId { +impl ExtractKey for Message { + fn extract_key(&self) -> &MessageId { &self.id } } @@ -970,11 +730,9 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/channel#message-object-message-types). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum MessageType { /// A regular message. - #[default] Regular = 0, /// An indicator that a recipient was added by the author. GroupRecipientAddition = 1, @@ -1041,7 +799,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/channel#message-object-message-activity-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum MessageActivityKind { Join = 1, @@ -1065,11 +822,11 @@ pub struct MessageApplication { /// ID of the embed's image asset. pub cover_image: Option, /// Application's description. - pub description: String, + pub description: FixedString, /// ID of the application's icon. pub icon: Option, /// Name of the application. - pub name: String, + pub name: FixedString, } /// Rich Presence activity information. @@ -1083,7 +840,7 @@ pub struct MessageActivity { #[serde(rename = "type")] pub kind: MessageActivityKind, /// `party_id` from a Rich Presence event. - pub party_id: Option, + pub party_id: Option, } /// Reference data sent with crossposted messages. @@ -1139,7 +896,7 @@ pub struct ChannelMention { #[serde(rename = "type")] pub kind: ChannelType, /// The name of the channel - pub name: String, + pub name: FixedString, } bitflags! { @@ -1197,34 +954,13 @@ impl MessageId { format!("https://discord.com/channels/@me/{channel_id}/{self}") } } - - /// Same as [`Self::link`] but tries to find the [`GuildId`] if it is not provided. - #[deprecated = "Use GuildChannel::guild_id if you have no GuildId"] - pub async fn link_ensured( - &self, - cache_http: impl CacheHttp, - channel_id: ChannelId, - mut guild_id: Option, - ) -> String { - if guild_id.is_none() { - let found_channel = channel_id.to_channel(cache_http).await; - - if let Ok(channel) = found_channel { - if let Some(c) = channel.guild() { - guild_id = Some(c.guild_id); - } - } - } - - self.link(channel_id, guild_id) - } } #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum Nonce { - String(String), + String(FixedString), Number(u64), } @@ -1241,7 +977,7 @@ pub struct RoleSubscriptionData { /// The id of the sku and listing that the user is subscribed to. pub role_subscription_listing_id: SkuId, /// The name of the tier that the user is subscribed to. - pub tier_name: String, + pub tier_name: FixedString, /// The cumulative number of months that the user has been subscribed for. pub total_months_subscribed: u16, /// Whether this notification is for a renewal rather than a new purchase. @@ -1339,12 +1075,11 @@ enum_number! { /// Currently, there is only the one option. /// /// [Discord docs](https://discord.com/developers/docs/resources/poll#layout-type) - #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] + #[ = 1] pub enum PollLayoutType { - #[default] Default = 1, _ => Unknown(u8), } diff --git a/src/model/channel/mod.rs b/src/model/channel/mod.rs index 71161a45e5d..6f1dfd1259f 100644 --- a/src/model/channel/mod.rs +++ b/src/model/channel/mod.rs @@ -12,6 +12,7 @@ mod reaction; use std::fmt; use serde::de::{Error as DeError, Unexpected}; +use serde_json::from_value; pub use self::attachment::*; pub use self::channel_id::*; @@ -22,21 +23,16 @@ pub use self::partial_channel::*; pub use self::private_channel::*; pub use self::reaction::*; #[cfg(feature = "model")] -use crate::http::CacheHttp; -use crate::json::*; +use crate::http::Http; +use crate::internal::prelude::*; use crate::model::prelude::*; use crate::model::utils::is_false; -#[deprecated = "use CreateAttachment instead"] -#[cfg(feature = "model")] -pub type AttachmentType<'a> = crate::builder::CreateAttachment; - /// A container for any channel. #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Debug, Serialize)] #[serde(untagged)] #[non_exhaustive] -#[allow(clippy::large_enum_variant)] // https://github.com/rust-lang/rust-clippy/issues/9798 pub enum Channel { /// A channel within a [`Guild`]. Guild(GuildChannel), @@ -121,38 +117,21 @@ impl Channel { /// /// # Errors /// - /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`], if the current user - /// lacks permission. - /// - /// Otherwise will return [`Error::Http`] if the current user does not have permission. - pub async fn delete(&self, cache_http: impl CacheHttp) -> Result<()> { + /// Returns [`Error::Http`] if the current user lacks permission. + pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result<()> { match self { Self::Guild(public_channel) => { - public_channel.delete(cache_http).await?; + public_channel.delete(http, reason).await?; }, Self::Private(private_channel) => { - private_channel.delete(cache_http.http()).await?; + private_channel.delete(http).await?; }, } Ok(()) } - /// Determines if the channel is NSFW. - #[inline] - #[must_use] - #[cfg(feature = "model")] - #[deprecated = "Use the GuildChannel::nsfw field, as PrivateChannel is never NSFW"] - pub fn is_nsfw(&self) -> bool { - match self { - #[allow(deprecated)] - Self::Guild(channel) => channel.is_nsfw(), - Self::Private(_) => false, - } - } - /// Retrieves the Id of the inner [`GuildChannel`], or [`PrivateChannel`]. - #[inline] #[must_use] pub const fn id(&self) -> ChannelId { match self { @@ -164,7 +143,6 @@ impl Channel { /// Retrieves the position of the inner [`GuildChannel`]. /// /// In DMs (private channel) it will return None. - #[inline] #[must_use] pub const fn position(&self) -> Option { match self { @@ -220,11 +198,9 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object-channel-types). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ChannelType { /// An indicator that the channel is a text [`GuildChannel`]. - #[default] Text = 0, /// An indicator that the channel is a [`PrivateChannel`]. Private = 1, @@ -257,10 +233,9 @@ enum_number! { } impl ChannelType { - #[inline] #[must_use] - pub const fn name(&self) -> &str { - match *self { + pub const fn name(self) -> &'static str { + match self { Self::Private => "private", Self::Text => "text", Self::Voice => "voice", @@ -273,7 +248,7 @@ impl ChannelType { Self::Stage => "stage", Self::Directory => "directory", Self::Forum => "forum", - Self::Unknown(_) => "unknown", + Self(_) => "unknown", } } } @@ -366,7 +341,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object-video-quality-modes). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum VideoQualityMode { /// An indicator that the video quality is chosen by Discord for optimal @@ -382,15 +356,14 @@ enum_number! { /// See [`StageInstance::privacy_level`]. /// /// [Discord docs](https://discord.com/developers/docs/resources/stage-instance#stage-instance-object-privacy-level). - #[derive(Clone, Copy, Default, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] + #[ = 2] pub enum StageInstancePrivacyLevel { /// The Stage instance is visible publicly. (deprecated) Public = 1, /// The Stage instance is visible to only guild members. - #[default] GuildOnly = 2, _ => Unknown(u8), } @@ -402,7 +375,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/channel#thread-metadata-object) #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u16", into = "u16")] #[non_exhaustive] pub enum AutoArchiveDuration { None = 0, @@ -426,7 +398,7 @@ pub struct StageInstance { /// The Id of the associated stage channel. pub channel_id: ChannelId, /// The topic of the stage instance. - pub topic: String, + pub topic: FixedString, /// The privacy level of the Stage instance. pub privacy_level: StageInstancePrivacyLevel, /// Whether or not Stage Discovery is disabled (deprecated). @@ -438,8 +410,9 @@ pub struct StageInstance { /// A thread data. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#thread-metadata-object). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct ThreadMetadata { /// Whether the thread is archived. @@ -475,9 +448,9 @@ pub struct ThreadMetadata { #[non_exhaustive] pub struct ThreadsData { /// The threads channels. - pub threads: Vec, + pub threads: FixedArray, /// A thread member for each returned thread the current user has joined. - pub members: Vec, + pub members: FixedArray, /// Whether there are potentially more threads that could be returned on a subsequent call. #[serde(default)] pub has_more: bool, @@ -494,13 +467,13 @@ pub enum ForumEmoji { /// The id of a guild's custom emoji. Id(EmojiId), /// The unicode character of the emoji. - Name(String), + Name(FixedString), } #[derive(Serialize, Deserialize)] struct RawForumEmoji { emoji_id: Option, - emoji_name: Option, + emoji_name: Option, } impl serde::Serialize for ForumEmoji { @@ -545,7 +518,7 @@ pub struct ForumTag { /// The id of the tag. pub id: ForumTagId, /// The name of the tag (0-20 characters). - pub name: String, + pub name: FixedString, /// Whether this tag can only be added to or removed from threads by a member with the /// MANAGE_THREADS permission. pub moderated: bool, @@ -560,7 +533,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object-sort-order-types). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum SortOrder { /// Sort forum posts by activity. diff --git a/src/model/channel/partial_channel.rs b/src/model/channel/partial_channel.rs index 663eac6d052..4317986b384 100644 --- a/src/model/channel/partial_channel.rs +++ b/src/model/channel/partial_channel.rs @@ -1,3 +1,4 @@ +use crate::internal::prelude::*; use crate::model::channel::{ChannelType, ThreadMetadata}; use crate::model::id::{ChannelId, WebhookId}; use crate::model::Permissions; @@ -13,7 +14,7 @@ pub struct PartialChannel { /// The channel Id. pub id: ChannelId, /// The channel name. - pub name: Option, + pub name: Option, /// The channel type. #[serde(rename = "type")] pub kind: ChannelType, @@ -29,6 +30,12 @@ pub struct PartialChannel { pub parent_id: Option, } +impl ExtractKey for PartialChannel { + fn extract_key(&self) -> &ChannelId { + &self.id + } +} + /// A container for the IDs returned by following a news channel. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#followed-channel-object). diff --git a/src/model/channel/private_channel.rs b/src/model/channel/private_channel.rs index 6eb441a41d0..b979de19a1f 100644 --- a/src/model/channel/private_channel.rs +++ b/src/model/channel/private_channel.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt; #[cfg(feature = "model")] use std::sync::Arc; @@ -8,6 +9,7 @@ use crate::builder::{CreateAttachment, CreateMessage, EditMessage, GetMessages}; use crate::http::CacheHttp; #[cfg(feature = "model")] use crate::http::{Http, Typing}; +use crate::internal::prelude::*; use crate::model::prelude::*; use crate::model::utils::single_recipient; @@ -42,8 +44,7 @@ impl PrivateChannel { /// /// See [ChannelId::broadcast_typing] for more details. #[allow(clippy::missing_errors_doc)] - #[inline] - pub async fn broadcast_typing(&self, http: impl AsRef) -> Result<()> { + pub async fn broadcast_typing(&self, http: &Http) -> Result<()> { self.id.broadcast_typing(http).await } @@ -55,11 +56,10 @@ impl PrivateChannel { /// /// Returns [`Error::Http`] if the reaction cannot be added, or if a message with that Id does /// not exist. - #[inline] pub async fn create_reaction( &self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, reaction_type: impl Into, ) -> Result<()> { self.id.create_reaction(http, message_id, reaction_type).await @@ -68,9 +68,9 @@ impl PrivateChannel { /// Deletes the channel. This does not delete the contents of the channel, and is equivalent to /// closing a private channel on the client, which can be re-opened. #[allow(clippy::missing_errors_doc)] - #[inline] - pub async fn delete(&self, http: impl AsRef) -> Result { - self.id.delete(http).await?.private().ok_or(Error::Model(ModelError::InvalidChannelType)) + pub async fn delete(&self, http: &Http) -> Result { + let resp = self.id.delete(http, None).await?; + resp.private().ok_or(Error::Model(ModelError::InvalidChannelType)) } /// Deletes all messages by Ids from the given vector in the channel. @@ -83,17 +83,12 @@ impl PrivateChannel { /// /// # Errors /// - /// Returns [`ModelError::BulkDeleteAmount`] if an attempt was made to delete either 0 or more - /// than 100 messages. + /// Returns [`ModelError::TooSmall`] or [`ModelError::TooLarge`] if an attempt was made to + /// delete either 0 or more than 100 messages. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES - #[inline] - pub async fn delete_messages>( - &self, - http: impl AsRef, - message_ids: impl IntoIterator, - ) -> Result<()> { - self.id.delete_messages(http, message_ids).await + pub async fn delete_messages(&self, http: &Http, message_ids: &[MessageId]) -> Result<()> { + self.id.delete_messages(http, message_ids, None).await } /// Deletes all permission overrides in the channel from a member or role. @@ -102,13 +97,12 @@ impl PrivateChannel { /// /// [Manage Channel]: Permissions::MANAGE_CHANNELS #[allow(clippy::missing_errors_doc)] - #[inline] pub async fn delete_permission( &self, - http: impl AsRef, + http: &Http, permission_type: PermissionOverwriteType, ) -> Result<()> { - self.id.delete_permission(http, permission_type).await + self.id.delete_permission(http, permission_type, None).await } /// Deletes the given [`Reaction`] from the channel. @@ -118,11 +112,10 @@ impl PrivateChannel { /// # Errors /// /// Returns [`Error::Http`] if the reaction is not from the current user. - #[inline] pub async fn delete_reaction( &self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, user_id: Option, reaction_type: impl Into, ) -> Result<()> { @@ -145,23 +138,13 @@ impl PrivateChannel { /// reasons. /// /// [`EditMessage::execute`]: ../../builder/struct.EditMessage.html#method.execute - #[inline] pub async fn edit_message( &self, - cache_http: impl CacheHttp, - message_id: impl Into, - builder: EditMessage, + http: &Http, + message_id: MessageId, + builder: EditMessage<'_>, ) -> Result { - self.id.edit_message(cache_http, message_id, builder).await - } - - /// Determines if the channel is NSFW. - #[inline] - #[must_use] - #[allow(clippy::unused_self)] - #[deprecated = "This always returns false"] - pub fn is_nsfw(&self) -> bool { - false + self.id.edit_message(http, message_id, builder).await } /// Gets a message from the channel. @@ -169,11 +152,10 @@ impl PrivateChannel { /// # Errors /// /// Returns [`Error::Http`] if a message with that Id does not exist in this channel. - #[inline] pub async fn message( &self, cache_http: impl CacheHttp, - message_id: impl Into, + message_id: MessageId, ) -> Result { self.id.message(cache_http, message_id).await } @@ -188,19 +170,8 @@ impl PrivateChannel { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - #[inline] - pub async fn messages( - &self, - cache_http: impl CacheHttp, - builder: GetMessages, - ) -> Result> { - self.id.messages(cache_http, builder).await - } - - /// Returns "DM with $username#discriminator". - #[must_use] - pub fn name(&self) -> String { - format!("DM with {}", self.recipient.tag()) + pub async fn messages(&self, http: &Http, builder: GetMessages) -> Result> { + self.id.messages(http, builder).await } /// Gets the list of [`User`]s who have reacted to a [`Message`] with a certain [`Emoji`]. @@ -215,14 +186,13 @@ impl PrivateChannel { /// # Errors /// /// Returns [`Error::Http`] if a message with the given Id does not exist in the channel. - #[inline] pub async fn reaction_users( &self, - http: impl AsRef, - message_id: impl Into, + http: &Http, + message_id: MessageId, reaction_type: impl Into, limit: Option, - after: impl Into>, + after: Option, ) -> Result> { self.id.reaction_users(http, message_id, reaction_type, limit, after).await } @@ -232,19 +202,13 @@ impl PrivateChannel { /// # Errors /// /// Returns [`Error::Http`] if the number of pinned messages would exceed the 50 message limit. - #[inline] - pub async fn pin( - &self, - http: impl AsRef, - message_id: impl Into, - ) -> Result<()> { - self.id.pin(http, message_id).await + pub async fn pin(&self, http: &Http, message_id: MessageId) -> Result<()> { + self.id.pin(http, message_id, None).await } /// Retrieves the list of messages that have been pinned in the private channel. #[allow(clippy::missing_errors_doc)] - #[inline] - pub async fn pins(&self, http: impl AsRef) -> Result> { + pub async fn pins(&self, http: &Http) -> Result> { self.id.pins(http).await } @@ -254,17 +218,12 @@ impl PrivateChannel { /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content length is over the above limit. See + /// Returns a [`ModelError::TooLarge`] if the content length is over the above limit. See /// [`CreateMessage::execute`] for more details. /// /// [`CreateMessage::execute`]: ../../builder/struct.CreateMessage.html#method.execute - #[inline] - pub async fn say( - &self, - cache_http: impl CacheHttp, - content: impl Into, - ) -> Result { - self.id.say(cache_http, content).await + pub async fn say(&self, http: &Http, content: impl Into>) -> Result { + self.id.say(http, content).await } /// Sends file(s) along with optional message contents. @@ -277,14 +236,13 @@ impl PrivateChannel { /// reasons. /// /// [`CreateMessage::execute`]: ../../builder/struct.CreateMessage.html#method.execute - #[inline] - pub async fn send_files( + pub async fn send_files<'a>( self, - cache_http: impl CacheHttp, - files: impl IntoIterator, - builder: CreateMessage, + http: &Http, + files: impl IntoIterator>, + builder: CreateMessage<'a>, ) -> Result { - self.id.send_files(cache_http, files, builder).await + self.id.send_files(http, files, builder).await } /// Sends a message to the channel. @@ -298,13 +256,8 @@ impl PrivateChannel { /// reasons. /// /// [`CreateMessage::execute`]: ../../builder/struct.CreateMessage.html#method.execute - #[inline] - pub async fn send_message( - &self, - cache_http: impl CacheHttp, - builder: CreateMessage, - ) -> Result { - self.id.send_message(cache_http, builder).await + pub async fn send_message(&self, http: &Http, builder: CreateMessage<'_>) -> Result { + self.id.send_message(http, builder).await } /// Starts typing in the channel for an indefinite period of time. @@ -334,7 +287,7 @@ impl PrivateChannel { /// # let cache = Cache::default(); /// # let channel: PrivateChannel = unimplemented!(); /// // Initiate typing (assuming http is `Arc` and `channel` is bound) - /// let typing = channel.start_typing(&http); + /// let typing = channel.start_typing(http); /// /// // Run some long-running process /// long_process(); @@ -347,8 +300,8 @@ impl PrivateChannel { /// # Errors /// /// May return [`Error::Http`] if the current user cannot send a direct message to this user. - pub fn start_typing(self, http: &Arc) -> Typing { - http.start_typing(self.id) + pub fn start_typing(self, http: Arc) -> Typing { + self.id.start_typing(http) } /// Unpins a [`Message`] in the channel given by its Id. @@ -357,13 +310,8 @@ impl PrivateChannel { /// /// Returns [`Error::Http`] if the current user lacks permission, if the message was deleted, /// or if the channel already has the limit of 50 pinned messages. - #[inline] - pub async fn unpin( - &self, - http: impl AsRef, - message_id: impl Into, - ) -> Result<()> { - self.id.unpin(http, message_id).await + pub async fn unpin(&self, http: &Http, message_id: MessageId) -> Result<()> { + self.id.unpin(http, message_id, None).await } } diff --git a/src/model/channel/reaction.rs b/src/model/channel/reaction.rs index d0cc503db07..a28170f25d2 100644 --- a/src/model/channel/reaction.rs +++ b/src/model/channel/reaction.rs @@ -4,6 +4,7 @@ use std::fmt::Display as _; use std::fmt::{self, Write as _}; use std::str::FromStr; +use nonmax::NonMaxU8; #[cfg(feature = "http")] use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use serde::de::Error as DeError; @@ -62,7 +63,6 @@ enum_number! { /// A list of types a reaction can be. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ReactionTypes { Normal = 0, @@ -103,7 +103,6 @@ impl Reaction { /// exists. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - #[inline] pub async fn channel(&self, cache_http: impl CacheHttp) -> Result { self.channel_id.to_channel(cache_http).await } @@ -116,36 +115,13 @@ impl Reaction { /// /// # Errors /// - /// If the `cache` is enabled, then returns a [`ModelError::InvalidPermissions`] if the current - /// user does not have the required [permissions]. - /// - /// Otherwise returns [`Error::Http`] if the current user lacks permission. + /// Returns [`Error::Http`] if the current user lacks the required [permissions]. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES /// [permissions]: crate::model::permissions - pub async fn delete(&self, cache_http: impl CacheHttp) -> Result<()> { - #[cfg_attr(not(feature = "cache"), allow(unused_mut))] - let mut user_id = self.user_id; - - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - if self.user_id == Some(cache.current_user().id) { - user_id = None; - } - - if user_id.is_some() { - crate::utils::user_has_perms_cache( - cache, - self.channel_id, - Permissions::MANAGE_MESSAGES, - )?; - } - } - } - + pub async fn delete(&self, http: &Http) -> Result<()> { self.channel_id - .delete_reaction(cache_http.http(), self.message_id, user_id, self.emoji.clone()) + .delete_reaction(http, self.message_id, self.user_id, self.emoji.clone()) .await } @@ -155,29 +131,12 @@ impl Reaction { /// /// # Errors /// - /// If the `cache` is enabled, then returns a [`ModelError::InvalidPermissions`] if the current - /// user does not have the required [permissions]. - /// - /// Otherwise returns [`Error::Http`] if the current user lacks permission. + /// Returns [`Error::Http`] if the current user lacks [permissions]. /// /// [Manage Messages]: Permissions::MANAGE_MESSAGES /// [permissions]: crate::model::permissions - pub async fn delete_all(&self, cache_http: impl CacheHttp) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - crate::utils::user_has_perms_cache( - cache, - self.channel_id, - Permissions::MANAGE_MESSAGES, - )?; - } - } - cache_http - .http() - .as_ref() - .delete_message_reaction_emoji(self.channel_id, self.message_id, &self.emoji) - .await + pub async fn delete_all(&self, http: &Http) -> Result<()> { + http.delete_message_reaction_emoji(self.channel_id, self.message_id, &self.emoji).await } /// Retrieves the [`Message`] associated with this reaction. @@ -190,7 +149,6 @@ impl Reaction { /// the message was deleted. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - #[inline] pub async fn message(&self, cache_http: impl CacheHttp) -> Result { self.channel_id.message(cache_http, self.message_id).await } @@ -236,49 +194,35 @@ impl Reaction { /// /// # Errors /// - /// Returns a [`ModelError::InvalidPermissions`] if the current user does not have the required - /// [permissions]. + /// Returns [`Error::Http`] if the current user lacks the required [permissions]. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY /// [permissions]: crate::model::permissions - #[inline] - pub async fn users( + pub async fn users( &self, - http: impl AsRef, - reaction_type: R, - limit: Option, - after: Option, - ) -> Result> - where - R: Into, - U: Into, - { - self._users(http, &reaction_type.into(), limit, after.map(Into::into)).await + http: &Http, + reaction_type: impl Into, + limit: Option, + after: Option, + ) -> Result> { + self._users(http, &reaction_type.into(), limit, after).await } async fn _users( &self, - http: impl AsRef, + http: &Http, reaction_type: &ReactionType, - limit: Option, + limit: Option, after: Option, ) -> Result> { - let mut limit = limit.unwrap_or(50); + let mut limit = limit.map_or(50, |limit| limit.get()); if limit > 100 { limit = 100; warn!("Reaction users limit clamped to 100! (API Restriction)"); } - http.as_ref() - .get_reaction_users( - self.channel_id, - self.message_id, - reaction_type, - limit, - after.map(UserId::get), - ) - .await + http.get_reaction_users(self.channel_id, self.message_id, reaction_type, limit, after).await } } @@ -295,10 +239,10 @@ pub enum ReactionType { id: EmojiId, /// The name of the custom emoji. This is primarily used for decoration and distinguishing /// the emoji client-side. - name: Option, + name: Option, }, /// A reaction with a twemoji. - Unicode(String), + Unicode(FixedString), } // Manual impl needed to decide enum variant by presence of `id` @@ -309,7 +253,7 @@ impl<'de> Deserialize<'de> for ReactionType { #[serde(default)] animated: bool, id: Option, - name: Option, + name: Option, } let emoji = PartialEmoji::deserialize(deserializer)?; Ok(match (emoji.id, emoji.name) { @@ -360,7 +304,6 @@ impl ReactionType { /// /// **Note**: This is mainly for use internally. There is otherwise most likely little use for /// it. - #[inline] #[must_use] #[cfg(feature = "http")] pub fn as_data(&self) -> String { @@ -383,7 +326,7 @@ impl ReactionType { #[must_use] pub fn unicode_eq(&self, other: &str) -> bool { if let ReactionType::Unicode(unicode) = &self { - unicode == other + &**unicode == other } else { // Always return false if not a unicode reaction false @@ -412,27 +355,27 @@ impl From for ReactionType { /// /// ```rust,no_run /// # #[cfg(feature = "http")] - /// # use serenity::http::CacheHttp; + /// # use serenity::http::Http; /// # use serenity::model::channel::Message; /// # use serenity::model::id::ChannelId; /// # /// # #[cfg(feature = "http")] - /// # async fn example(ctx: impl CacheHttp, message: Message) -> Result<(), Box> { - /// message.react(ctx, '🍎').await?; + /// # async fn example(http: &Http, message: Message) -> Result<(), Box> { + /// message.react(http, '🍎').await?; /// # Ok(()) /// # } /// # /// # fn main() {} /// ``` fn from(ch: char) -> ReactionType { - ReactionType::Unicode(ch.to_string()) + ReactionType::Unicode(ch.to_string().trunc_into()) } } impl From for ReactionType { fn from(emoji: Emoji) -> ReactionType { ReactionType::Custom { - animated: emoji.animated, + animated: emoji.animated(), id: emoji.id, name: Some(emoji.name), } @@ -444,7 +387,7 @@ impl From for ReactionType { ReactionType::Custom { animated: false, id: emoji_id, - name: Some("emoji".to_string()), + name: Some(FixedString::from_static_trunc("emoji")), } } } @@ -479,7 +422,7 @@ impl TryFrom for ReactionType { } if !emoji_string.starts_with('<') { - return Ok(ReactionType::Unicode(emoji_string)); + return Ok(ReactionType::Unicode(emoji_string.trunc_into())); } ReactionType::try_from(&emoji_string[..]) } @@ -513,13 +456,14 @@ impl<'a> TryFrom<&'a str> for ReactionType { /// ```rust /// use serenity::model::channel::ReactionType; /// use serenity::model::id::EmojiId; + /// use serenity::small_fixed_array::FixedString; /// /// let emoji_string = "<:customemoji:600404340292059257>"; /// let reaction = ReactionType::try_from(emoji_string).unwrap(); /// let reaction2 = ReactionType::Custom { /// animated: false, /// id: EmojiId::new(600404340292059257), - /// name: Some("customemoji".to_string()), + /// name: Some(FixedString::from_static_trunc("customemoji")), /// }; /// /// assert_eq!(reaction, reaction2); @@ -533,7 +477,7 @@ impl<'a> TryFrom<&'a str> for ReactionType { } if !emoji_str.starts_with('<') { - return Ok(ReactionType::Unicode(emoji_str.to_string())); + return Ok(ReactionType::Unicode(emoji_str.to_string().trunc_into())); } if !emoji_str.ends_with('>') { @@ -545,7 +489,7 @@ impl<'a> TryFrom<&'a str> for ReactionType { let mut split_iter = emoji_str.split(':'); let animated = split_iter.next().ok_or(ReactionConversionError)? == "a"; - let name = split_iter.next().ok_or(ReactionConversionError)?.to_string().into(); + let name = Some(split_iter.next().ok_or(ReactionConversionError)?.to_string().trunc_into()); let id = split_iter.next().and_then(|s| s.parse().ok()).ok_or(ReactionConversionError)?; Ok(ReactionType::Custom { diff --git a/src/model/colour.rs b/src/model/colour.rs index e11bf6ff6fc..5ec219b5ab8 100644 --- a/src/model/colour.rs +++ b/src/model/colour.rs @@ -13,7 +13,7 @@ /// Passing in a role's colour, and then retrieving its green component via [`Self::g`]: /// /// ```rust -/// # use serenity::json::{json, from_value}; +/// # use serde_json::{json, from_value}; /// # use serenity::model::guild::Role; /// # use serenity::model::id::RoleId; /// # use serenity::model::id::GuildId; @@ -66,7 +66,9 @@ /// ``` /// /// [`Role`]: crate::model::guild::Role -#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] +#[derive( + Clone, Copy, Debug, Default, Eq, Ord, Hash, PartialEq, PartialOrd, Deserialize, Serialize, +)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] pub struct Colour(pub u32); @@ -87,7 +89,6 @@ impl Colour { /// /// assert_eq!(colour.tuple(), (100, 76, 67)); /// ``` - #[inline] #[must_use] pub const fn new(value: u32) -> Colour { Colour(value) @@ -203,25 +204,6 @@ impl Colour { } } -impl From for Colour { - /// Constructs a Colour from a i32. - /// - /// This is used for functions that accept `Into`. - /// - /// This is useful when providing hex values. - /// - /// # Examples - /// - /// ```rust - /// use serenity::model::Colour; - /// - /// assert_eq!(Colour::from(0xDEA584).tuple(), (222, 165, 132)); - /// ``` - fn from(value: i32) -> Colour { - Colour(value as u32) - } -} - impl From for Colour { /// Constructs a Colour from a u32. /// @@ -235,24 +217,7 @@ impl From for Colour { /// assert_eq!(Colour::from(6573123u32).r(), 100); /// ``` fn from(value: u32) -> Colour { - Colour(value) - } -} - -impl From for Colour { - /// Constructs a Colour from a u32. - /// - /// This is used for functions that accept `Into`. - /// - /// # Examples - /// - /// ```rust - /// use serenity::model::Colour; - /// - /// assert_eq!(Colour::from(6573123u64).r(), 100); - /// ``` - fn from(value: u64) -> Colour { - Colour(value as u32) + Colour(value & 0xffffff) } } @@ -445,8 +410,6 @@ mod test { #[test] fn from() { - assert_eq!(Colour::from(7i32).0, 7); assert_eq!(Colour::from(7u32).0, 7); - assert_eq!(Colour::from(7u64).0, 7); } } diff --git a/src/model/connection.rs b/src/model/connection.rs index 7968fccdd50..c47d1f0cbd5 100644 --- a/src/model/connection.rs +++ b/src/model/connection.rs @@ -1,28 +1,30 @@ //! Models for user connections. use super::prelude::*; +use crate::internal::prelude::*; /// Information about a connection between the current user and a third party service. /// /// [Discord docs](https://discord.com/developers/docs/resources/user#connection-object-connection-structure). -#[derive(Clone, Debug, Deserialize, Serialize)] +#[bool_to_bitflags::bool_to_bitflags] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct Connection { /// The ID of the account on the other side of this connection. - pub id: String, + pub id: FixedString, /// The username of the account on the other side of this connection. - pub name: String, + pub name: FixedString, /// The service that this connection represents (e.g. twitch, youtube) /// /// [Discord docs](https://discord.com/developers/docs/resources/user#connection-object-services). #[serde(rename = "type")] - pub kind: String, + pub kind: FixedString, /// Whether this connection has been revoked and is no longer valid. #[serde(default)] pub revoked: bool, /// A list of partial guild [`Integration`]s that use this connection. #[serde(default)] - pub integrations: Vec, + pub integrations: FixedArray, /// Whether this connection has been verified and the user has proven they own the account. pub verified: bool, /// Whether friend sync is enabled for this connection. @@ -40,7 +42,6 @@ enum_number! { /// /// [Discord docs](https://discord.com/developers/docs/resources/user#connection-object-visibility-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ConnectionVisibility { /// Invisible to everyone except the user themselves diff --git a/src/model/error.rs b/src/model/error.rs index 22de21ce5fd..d8ab25d8d31 100644 --- a/src/model/error.rs +++ b/src/model/error.rs @@ -3,7 +3,96 @@ use std::error::Error as StdError; use std::fmt; -use super::Permissions; +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum Maximum { + EmbedLength, + EmbedCount, + MessageLength, + StickerCount, + WebhookName, + AuditLogReason, + DeleteMessageDays, + BulkDeleteAmount, +} + +impl Maximum { + pub(crate) fn check_overflow(self, value: usize) -> Result<(), Error> { + let max = self.value(); + if value > max { + Err(Error::TooLarge { + maximum: self, + value, + }) + } else { + Ok(()) + } + } + + pub(crate) fn value(self) -> usize { + match self { + Self::EmbedCount => crate::constants::EMBED_MAX_COUNT, + Self::EmbedLength => crate::constants::EMBED_MAX_LENGTH, + Self::MessageLength => crate::constants::MESSAGE_CODE_LIMIT, + Self::StickerCount => crate::constants::STICKER_MAX_COUNT, + Self::WebhookName | Self::BulkDeleteAmount => 100, + Self::AuditLogReason => 512, + Self::DeleteMessageDays => 7, + } + } +} + +impl fmt::Display for Maximum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmbedCount => f.write_str("Embed count"), + Self::EmbedLength => f.write_str("Embed length"), + Self::MessageLength => f.write_str("Message length"), + Self::StickerCount => f.write_str("Sticker count"), + Self::WebhookName => f.write_str("Webhook name"), + Self::AuditLogReason => f.write_str("Audit log reason"), + Self::DeleteMessageDays => f.write_str("Delete message days"), + Self::BulkDeleteAmount => f.write_str("Message bulk delete count"), + } + } +} + +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum Minimum { + WebhookName, + BulkDeleteAmount, +} + +impl Minimum { + pub(crate) fn check_underflow(self, value: usize) -> Result<(), Error> { + let min = self.value(); + if value < min { + Err(Error::TooSmall { + minimum: self, + value, + }) + } else { + Ok(()) + } + } + + pub(crate) fn value(self) -> usize { + match self { + Self::WebhookName => 2, + Self::BulkDeleteAmount => 1, + } + } +} + +impl fmt::Display for Minimum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::WebhookName => f.write_str("Webhook name"), + Self::BulkDeleteAmount => f.write_str("Bulk delete amount"), + } + } +} /// An error returned from the [`model`] module. /// @@ -25,16 +114,18 @@ use super::Permissions; /// #[serenity::async_trait] /// #[cfg(feature = "client")] /// impl EventHandler for Handler { -/// async fn guild_ban_removal(&self, context: Context, guild_id: GuildId, user: User) { -/// match guild_id.ban(&context, user, 8).await { +/// async fn guild_ban_removal(&self, ctx: Context, guild_id: GuildId, user: User) { +/// match guild_id.ban(&ctx.http, user.id, 8, Some("No unbanning people!")).await { /// Ok(()) => { /// // Ban successful. /// }, -/// Err(Error::Model(ModelError::DeleteMessageDaysAmount(amount))) => { -/// println!("Failed deleting {} days' worth of messages", amount); +/// Err(Error::Model(ModelError::TooLarge { +/// value, .. +/// })) => { +/// println!("Failed deleting {value} days' worth of messages"); /// }, /// Err(why) => { -/// println!("Unexpected error: {:?}", why); +/// println!("Unexpected error: {why:?}"); /// }, /// } /// } @@ -48,14 +139,10 @@ use super::Permissions; #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum Error { - /// When attempting to delete below or above the minimum or maximum allowed number of messages. - BulkDeleteAmount, - /// When attempting to delete a number of days' worth of messages that is not allowed. - DeleteMessageDaysAmount(u8), - /// When attempting to send a message with over 10 embeds. - EmbedAmount, - /// Indicates that the textual content of an embed exceeds the maximum length. - EmbedTooLarge(usize), + /// Indicates that the `minimum` has been missed by the `value`. + TooSmall { minimum: Minimum, value: usize }, + /// Indicates that the `maximum` has been exceeded by the `value`. + TooLarge { maximum: Maximum, value: usize }, /// An indication that a [`Guild`] could not be found by [Id][`GuildId`] in the [`Cache`]. /// /// [`Guild`]: super::guild::Guild @@ -100,13 +187,6 @@ pub enum Error { /// When editing a role, if the role is higher in position than the current user's highest /// role, then the role can not be edited. Hierarchy, - /// Indicates that you do not have the required permissions to perform an operation. - InvalidPermissions { - /// Which permissions were required for the operation - required: Permissions, - /// Which permissions the bot had - present: Permissions, - }, /// An indicator that the [current user] cannot perform an action. /// /// [current user]: super::user::CurrentUser @@ -116,17 +196,6 @@ pub enum Error { /// /// [`Cache`]: crate::cache::Cache ItemMissing, - /// Indicates that a member, role or channel from the wrong [`Guild`] was provided. - /// - /// [`Guild`]: super::guild::Guild - WrongGuild, - /// Indicates that a [`Message`]s content was too long and will not successfully send, as the - /// length is over 2000 codepoints. - /// - /// The number of code points larger than the limit is provided. - /// - /// [`Message`]: super::channel::Message - MessageTooLong(usize), /// Indicates that the current user is attempting to Direct Message another bot user, which is /// disallowed by the API. MessagingBot, @@ -134,21 +203,10 @@ pub enum Error { /// /// [`ChannelType`]: super::channel::ChannelType InvalidChannelType, - /// Indicates that the webhook name is under the 2 characters limit. - NameTooShort, - /// Indicates that the webhook name is over the 100 characters limit. - NameTooLong, - /// Indicates that the bot is not author of the message. This error is returned in - /// private/direct channels. - NotAuthor, /// Indicates that the webhook token is missing. NoTokenSet, /// When attempting to delete a built in nitro sticker instead of a guild sticker. DeleteNitroSticker, - /// Indicates that the sticker file is missing. - NoStickerFileSet, - /// When attempting to send a message with over 3 stickers. - StickerAmount, /// When attempting to edit a voice message. CannotEditVoiceMessage, } @@ -171,33 +229,27 @@ impl Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::BulkDeleteAmount => f.write_str("Too few/many messages to bulk delete."), - Self::DeleteMessageDaysAmount(_) => f.write_str("Invalid delete message days."), - Self::EmbedAmount => f.write_str("Too many embeds in a message."), - Self::EmbedTooLarge(_) => f.write_str("Embed too large."), + Self::TooSmall { + minimum, + value, + } => write!(f, "The {minimum} minimum has been missed by {value}"), + Self::TooLarge { + maximum, + value, + } => write!(f, "The {maximum} maximum has been overflowed by {value}"), Self::GuildNotFound => f.write_str("Guild not found in the cache."), Self::RoleNotFound => f.write_str("Role not found in the cache."), Self::MemberNotFound => f.write_str("Member not found in the cache."), Self::ChannelNotFound => f.write_str("Channel not found in the cache."), Self::Hierarchy => f.write_str("Role hierarchy prevents this action."), Self::InvalidChannelType => f.write_str("The channel cannot perform the action."), - Self::InvalidPermissions { - .. - } => f.write_str("Invalid permissions."), Self::InvalidUser => f.write_str("The current user cannot perform the action."), Self::ItemMissing => f.write_str("The required item is missing from the cache."), - Self::WrongGuild => f.write_str("Provided member or channel is from the wrong guild."), - Self::MessageTooLong(_) => f.write_str("Message too large."), Self::MessageAlreadyCrossposted => f.write_str("Message already crossposted."), Self::CannotCrosspostMessage => f.write_str("Cannot crosspost this message type."), Self::MessagingBot => f.write_str("Attempted to message another bot user."), - Self::NameTooShort => f.write_str("Name is under the character limit."), - Self::NameTooLong => f.write_str("Name is over the character limit."), - Self::NotAuthor => f.write_str("The bot is not author of this message."), Self::NoTokenSet => f.write_str("Token is not set."), Self::DeleteNitroSticker => f.write_str("Cannot delete an official sticker."), - Self::NoStickerFileSet => f.write_str("Sticker file is not set."), - Self::StickerAmount => f.write_str("Too many stickers in a message."), Self::CannotEditVoiceMessage => f.write_str("Cannot edit voice message."), } } diff --git a/src/model/event.rs b/src/model/event.rs index 085f465cf44..2b0b1ae2a38 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -6,19 +6,17 @@ // Just for MessageUpdateEvent (for some reason the #[allow] doesn't work when placed directly) #![allow(clippy::option_option)] +use nonmax::NonMaxU64; use serde::de::Error as DeError; use serde::Serialize; +use strum::{EnumCount, IntoStaticStr, VariantNames}; +use tracing::{debug, warn}; use crate::constants::Opcode; +use crate::internal::prelude::*; +use crate::internal::utils::lending_for_each; use crate::model::prelude::*; -use crate::model::utils::{ - deserialize_val, - emojis, - members, - remove_from_map, - remove_from_map_opt, - stickers, -}; +use crate::model::utils::{deserialize_val, remove_from_map, remove_from_map_opt}; /// Requires no gateway intents. /// @@ -175,9 +173,9 @@ pub struct GuildCreateEvent { impl<'de> Deserialize<'de> for GuildCreateEvent { fn deserialize>(deserializer: D) -> StdResult { let mut guild: Guild = Guild::deserialize(deserializer)?; - guild.channels.values_mut().for_each(|x| x.guild_id = guild.id); - guild.members.values_mut().for_each(|x| x.guild_id = guild.id); - guild.roles.values_mut().for_each(|x| x.guild_id = guild.id); + lending_for_each!(guild.channels.iter_mut(), |x| x.guild_id = guild.id); + lending_for_each!(guild.members.iter_mut(), |x| x.guild_id = guild.id); + lending_for_each!(guild.roles.iter_mut(), |x| x.guild_id = guild.id); Ok(Self { guild, }) @@ -202,8 +200,7 @@ pub struct GuildDeleteEvent { #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct GuildEmojisUpdateEvent { - #[serde(with = "emojis")] - pub emojis: HashMap, + pub emojis: ExtractMap, pub guild_id: GuildId, } @@ -242,14 +239,15 @@ pub struct GuildMemberRemoveEvent { /// Requires [`GatewayIntents::GUILD_MEMBERS`]. /// /// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#guild-member-update). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct GuildMemberUpdateEvent { pub guild_id: GuildId, - pub nick: Option, + pub nick: Option>, pub joined_at: Timestamp, - pub roles: Vec, + pub roles: FixedArray, pub user: User, pub premium_since: Option, #[serde(default)] @@ -274,8 +272,7 @@ pub struct GuildMembersChunkEvent { /// ID of the guild. pub guild_id: GuildId, /// Set of guild members. - #[serde(with = "members")] - pub members: HashMap, + pub members: ExtractMap, /// Chunk index in the expected chunks for this response (0 <= chunk_index < chunk_count). pub chunk_index: u32, /// Total number of expected chunks for this response. @@ -283,19 +280,19 @@ pub struct GuildMembersChunkEvent { /// When passing an invalid ID to [`crate::gateway::ShardRunnerMessage::ChunkGuild`], it will /// be returned here. #[serde(default)] - pub not_found: Vec, + pub not_found: FixedArray, /// When passing true to [`crate::gateway::ShardRunnerMessage::ChunkGuild`], presences of the /// returned members will be here. pub presences: Option>, /// Nonce used in the [`crate::gateway::ShardRunnerMessage::ChunkGuild`] request. - pub nonce: Option, + pub nonce: Option, } // Manual impl needed to insert guild_id fields in Member impl<'de> Deserialize<'de> for GuildMembersChunkEvent { fn deserialize>(deserializer: D) -> StdResult { let mut event = Self::deserialize(deserializer)?; // calls #[serde(remote)]-generated inherent method - event.members.values_mut().for_each(|m| m.guild_id = event.guild_id); + lending_for_each!(event.members.iter_mut(), |m| m.guild_id = event.guild_id); Ok(event) } } @@ -374,8 +371,7 @@ impl<'de> Deserialize<'de> for GuildRoleUpdateEvent { #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct GuildStickersUpdateEvent { - #[serde(with = "stickers")] - pub stickers: HashMap, + pub stickers: ExtractMap, pub guild_id: GuildId, } @@ -390,7 +386,7 @@ pub struct InviteCreateEvent { /// Channel the invite is for. pub channel_id: ChannelId, /// Unique invite [code](Invite::code). - pub code: String, + pub code: FixedString, /// Time at which the invite was created. pub created_at: Timestamp, /// Guild of the invite. @@ -422,7 +418,7 @@ pub struct InviteCreateEvent { pub struct InviteDeleteEvent { pub channel_id: ChannelId, pub guild_id: Option, - pub code: String, + pub code: FixedString, } /// Requires [`GatewayIntents::GUILDS`]. @@ -457,7 +453,7 @@ pub struct MessageCreateEvent { pub struct MessageDeleteBulkEvent { pub guild_id: Option, pub channel_id: ChannelId, - pub ids: Vec, + pub ids: FixedArray, } /// Requires [`GatewayIntents::GUILD_MESSAGES`] or [`GatewayIntents::DIRECT_MESSAGES`]. @@ -498,17 +494,17 @@ pub struct MessageUpdateEvent { pub id: MessageId, pub channel_id: ChannelId, pub author: Option, - pub content: Option, + pub content: Option>, pub timestamp: Option, pub edited_timestamp: Option, pub tts: Option, pub mention_everyone: Option, - pub mentions: Option>, - pub mention_roles: Option>, - pub mention_channels: Option>, - pub attachments: Option>, - pub embeds: Option>, - pub reactions: Option>, + pub mentions: Option>, + pub mention_roles: Option>, + pub mention_channels: Option>, + pub attachments: Option>, + pub embeds: Option>, + pub reactions: Option>, pub pinned: Option, #[serde(default, deserialize_with = "deserialize_some")] pub webhook_id: Option>, @@ -525,19 +521,16 @@ pub struct MessageUpdateEvent { pub flags: Option>, #[serde(default, deserialize_with = "deserialize_some")] pub referenced_message: Option>>, - #[cfg_attr( - all(not(ignore_serenity_deprecated), feature = "unstable_discord_api"), - deprecated = "Use interaction_metadata" - )] #[serde(default, deserialize_with = "deserialize_some")] + #[cfg(not(feature = "unstable"))] pub interaction: Option>>, - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] pub interaction_metadata: Option>>, #[serde(default, deserialize_with = "deserialize_some")] - pub thread: Option>, - pub components: Option>, - pub sticker_items: Option>, - pub position: Option>, + pub thread: Option>>, + pub components: Option>, + pub sticker_items: Option>, + pub position: Option>, pub role_subscription_data: Option>, pub guild_id: Option, pub member: Option>>, @@ -550,7 +543,6 @@ impl MessageUpdateEvent { pub fn apply_to_message(&self, message: &mut Message) { // Destructure, so we get an `unused` warning when we forget to process one of the fields // in this method - #[allow(deprecated)] // yes rust, exhaustive means exhaustive, even the deprecated ones let Self { id, channel_id, @@ -575,8 +567,9 @@ impl MessageUpdateEvent { message_reference, flags, referenced_message, + #[cfg(not(feature = "unstable"))] interaction, - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] interaction_metadata, thread, components, @@ -597,15 +590,15 @@ impl MessageUpdateEvent { if let Some(x) = content { message.content.clone_from(x) } if let Some(x) = timestamp { message.timestamp = x.clone() } message.edited_timestamp = *edited_timestamp; - if let Some(x) = tts { message.tts = x.clone() } - if let Some(x) = mention_everyone { message.mention_everyone = x.clone() } + if let Some(x) = tts { message.set_tts(*x) } + if let Some(x) = mention_everyone { message.set_mention_everyone(*x) } if let Some(x) = mentions { message.mentions.clone_from(x) } if let Some(x) = mention_roles { message.mention_roles.clone_from(x) } if let Some(x) = mention_channels { message.mention_channels.clone_from(x) } if let Some(x) = attachments { message.attachments.clone_from(x) } if let Some(x) = embeds { message.embeds.clone_from(x) } if let Some(x) = reactions { message.reactions.clone_from(x) } - if let Some(x) = pinned { message.pinned = x.clone() } + if let Some(x) = pinned { message.set_pinned(*x) } if let Some(x) = webhook_id { message.webhook_id.clone_from(x) } if let Some(x) = kind { message.kind = x.clone() } if let Some(x) = activity { message.activity.clone_from(x) } @@ -614,8 +607,9 @@ impl MessageUpdateEvent { if let Some(x) = message_reference { message.message_reference.clone_from(x) } if let Some(x) = flags { message.flags.clone_from(x) } if let Some(x) = referenced_message { message.referenced_message.clone_from(x) } + #[cfg(not(feature = "unstable"))] if let Some(x) = interaction { message.interaction.clone_from(x) } - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] if let Some(x) = interaction_metadata { message.interaction_metadata.clone_from(x) } if let Some(x) = thread { message.thread.clone_from(x) } if let Some(x) = components { message.components.clone_from(x) } @@ -638,16 +632,6 @@ pub struct PresenceUpdateEvent { pub presence: Presence, } -/// Not officially documented. -#[cfg_attr(not(ignore_serenity_deprecated), deprecated = "This event doesn't exist")] -#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(transparent)] -#[non_exhaustive] -pub struct PresencesReplaceEvent { - pub presences: Vec, -} - /// Requires [`GatewayIntents::GUILD_MESSAGE_REACTIONS`] or /// [`GatewayIntents::DIRECT_MESSAGE_REACTIONS`]. /// @@ -740,16 +724,6 @@ pub struct TypingStartEvent { pub member: Option, } -#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[non_exhaustive] -pub struct UnknownEvent { - #[serde(rename = "t")] - pub kind: String, - #[serde(rename = "d")] - pub value: Value, -} - /// Sent when properties about the current bot's user change. /// /// Requires no gateway intents. @@ -770,9 +744,9 @@ pub struct UserUpdateEvent { #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct VoiceServerUpdateEvent { - pub token: String, - pub guild_id: Option, - pub endpoint: Option, + pub token: FixedString, + pub guild_id: GuildId, + pub endpoint: Option, } /// Requires [`GatewayIntents::GUILD_VOICE_STATES`]. @@ -793,7 +767,7 @@ pub struct VoiceStateUpdateEvent { #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct VoiceChannelStatusUpdateEvent { - pub status: Option, + pub status: Option>, pub id: ChannelId, pub guild_id: GuildId, } @@ -934,10 +908,10 @@ pub struct ThreadListSyncEvent { /// well, so you know to clear that data. pub channel_ids: Option>, /// All active threads in the given channels that the current user can access. - pub threads: Vec, + pub threads: FixedArray, /// All thread member objects from the synced threads for the current user, indicating which /// threads the current user has been added to - pub members: Vec, + pub members: FixedArray, } /// Requires [`GatewayIntents::GUILDS`], and, to receive this event for other users, @@ -970,10 +944,10 @@ pub struct ThreadMembersUpdateEvent { pub member_count: i16, /// The users who were added to the thread. #[serde(default)] - pub added_members: Vec, + pub added_members: FixedArray, /// The ids of the users who were removed from the thread. #[serde(default)] - pub removed_member_ids: Vec, + pub removed_member_ids: FixedArray, } /// Requires [`GatewayIntents::GUILD_SCHEDULED_EVENTS`]. @@ -1098,12 +1072,17 @@ pub struct MessagePollVoteRemoveEvent { /// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#payload-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, Serialize)] #[non_exhaustive] #[serde(untagged)] pub enum GatewayEvent { - Dispatch(u64, Event), + Dispatch { + seq: u64, + // Avoid deserialising straight away to handle errors and get access to `seq`. + data: JsonMap, + // Used for debugging, if the data cannot be deserialised. + original_str: FixedString, + }, Heartbeat(u64), Reconnect, /// Whether the session can be resumed. @@ -1119,16 +1098,16 @@ impl<'de> Deserialize<'de> for GatewayEvent { let seq = remove_from_map_opt(&mut map, "s")?.flatten(); Ok(match remove_from_map(&mut map, "op")? { - Opcode::Dispatch => Self::Dispatch( - seq.ok_or_else(|| DeError::missing_field("s"))?, - deserialize_val(Value::from(map))?, - ), - Opcode::Heartbeat => { - GatewayEvent::Heartbeat(seq.ok_or_else(|| DeError::missing_field("s"))?) - }, - Opcode::InvalidSession => { - GatewayEvent::InvalidateSession(remove_from_map(&mut map, "d")?) + Opcode::Dispatch => { + Self::Dispatch { + seq: seq.ok_or_else(|| DeError::missing_field("s"))?, + // Filled in in recv_event + original_str: FixedString::new(), + data: map, + } }, + Opcode::Heartbeat => Self::Heartbeat(seq.ok_or_else(|| DeError::missing_field("s"))?), + Opcode::InvalidSession => Self::InvalidateSession(remove_from_map(&mut map, "d")?), Opcode::Hello => { #[derive(Deserialize)] struct HelloPayload { @@ -1136,10 +1115,10 @@ impl<'de> Deserialize<'de> for GatewayEvent { } let inner: HelloPayload = remove_from_map(&mut map, "d")?; - GatewayEvent::Hello(inner.heartbeat_interval) + Self::Hello(inner.heartbeat_interval) }, - Opcode::Reconnect => GatewayEvent::Reconnect, - Opcode::HeartbeatAck => GatewayEvent::HeartbeatAck, + Opcode::Reconnect => Self::Reconnect, + Opcode::HeartbeatAck => Self::HeartbeatAck, _ => return Err(DeError::custom("invalid opcode")), }) } @@ -1148,9 +1127,9 @@ impl<'de> Deserialize<'de> for GatewayEvent { /// Event received over a websocket connection /// /// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#receive-events). -#[allow(clippy::large_enum_variant)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, EnumCount, VariantNames, IntoStaticStr)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[serde(tag = "t", content = "d")] #[non_exhaustive] @@ -1256,9 +1235,6 @@ pub enum Event { MessageUpdate(MessageUpdateEvent), /// A member's presence state (or username or avatar) has changed PresenceUpdate(PresenceUpdateEvent), - /// The presence list of the user's friends should be replaced entirely - #[cfg_attr(not(ignore_serenity_deprecated), deprecated = "This event doesn't exist")] - PresencesReplace(PresencesReplaceEvent), /// A reaction was added to a message. /// /// Fires the [`EventHandler::reaction_add`] event handler. @@ -1353,21 +1329,39 @@ pub enum Event { MessagePollVoteAdd(MessagePollVoteAddEvent), /// A user has removed a previous vote on a Message Poll. MessagePollVoteRemove(MessagePollVoteRemoveEvent), - /// An event type not covered by the above - #[serde(untagged)] - Unknown(UnknownEvent), } impl Event { - /// Return the event name of this event. Returns [`None`] if the event is - /// [`Unknown`](Event::Unknown). + /// Returns the event name of this event. #[must_use] - pub fn name(&self) -> Option { - if let Self::Unknown(_) = self { - None - } else { - let map = serde_json::to_value(self).ok()?; - Some(map.get("t")?.as_str()?.to_string()) + pub fn name(&self) -> &'static str { + self.into() + } + + pub(crate) fn deserialize_and_log(map: JsonMap, original_str: &str) -> Result { + deserialize_val(Value::Object(map)) + .map_err(|err| log_deserialisation_err(original_str, err)) + } +} + +fn filter_unknown_variant(json_err_dbg: &str) -> bool { + if let Some(msg) = json_err_dbg.strip_prefix("Error(\"unknown variant `") { + if let Some((variant_name, _)) = msg.split_once('`') { + debug!("Unknown event: {variant_name}"); + return true; } } + + false +} + +#[cold] +fn log_deserialisation_err(json_str: &str, err: serde_json::Error) -> Error { + let json_err_dbg = format!("{err:?}"); + if !filter_unknown_variant(&json_err_dbg) { + warn!("Err deserializing text: {json_err_dbg}"); + } + + debug!("Failing text: {json_str}"); + Error::Json(err) } diff --git a/src/model/gateway.rs b/src/model/gateway.rs index 2c54eba7026..ebdd4451eee 100644 --- a/src/model/gateway.rs +++ b/src/model/gateway.rs @@ -1,12 +1,13 @@ //! Models pertaining to the gateway. -use std::num::NonZeroU16; +use std::num::{NonZeroU16, NonZeroU64}; use serde::ser::SerializeSeq; use url::Url; use super::prelude::*; use super::utils::*; +use crate::internal::prelude::*; /// A representation of the data retrieved from the bot gateway endpoint. /// @@ -20,9 +21,9 @@ use super::utils::*; #[non_exhaustive] pub struct BotGateway { /// The gateway to connect to. - pub url: String, + pub url: FixedString, /// The number of shards that is recommended to be used by the current bot user. - pub shards: u32, + pub shards: NonZeroU16, /// Information describing how many gateway sessions you can initiate within a ratelimit /// period. pub session_start_limit: SessionStartLimit, @@ -36,13 +37,11 @@ pub struct BotGateway { #[non_exhaustive] pub struct Activity { /// The ID of the application for the activity. - #[serde(deserialize_with = "deserialize_buggy_id")] - #[serde(default)] pub application_id: Option, /// Images for the presence and their texts. pub assets: Option, /// What the user is doing. - pub details: Option, + pub details: Option, /// Activity flags describing what the payload includes. pub flags: Option, /// Whether or not the activity is an instanced game session. @@ -51,32 +50,32 @@ pub struct Activity { #[serde(rename = "type")] pub kind: ActivityType, /// The name of the activity. - pub name: String, + pub name: FixedString, /// Information about the user's current party. pub party: Option, /// Secrets for Rich Presence joining and spectating. pub secrets: Option, /// The user's current party status. - pub state: Option, + pub state: Option, /// Emoji currently used in custom status pub emoji: Option, /// Unix timestamps for the start and/or end times of the activity. pub timestamps: Option, /// The sync ID of the activity. Mainly used by the Spotify activity type which uses this /// parameter to store the track ID. - #[cfg(feature = "unstable_discord_api")] - pub sync_id: Option, + #[cfg(feature = "unstable")] + pub sync_id: Option, /// The session ID of the activity. Reserved for specific activity types, such as the Activity /// that is transmitted when a user is listening to Spotify. - #[cfg(feature = "unstable_discord_api")] - pub session_id: Option, + #[cfg(feature = "unstable")] + pub session_id: Option, /// The Stream URL if [`Self::kind`] is [`ActivityType::Streaming`]. pub url: Option, /// The buttons of this activity. /// /// **Note**: There can only be up to 2 buttons. #[serde(default, deserialize_with = "deserialize_buttons")] - pub buttons: Vec, + pub buttons: FixedArray, /// Unix timestamp (in milliseconds) of when the activity was added to the user's session pub created_at: u64, } @@ -87,12 +86,12 @@ pub struct Activity { #[non_exhaustive] pub struct ActivityButton { /// The text shown on the button. - pub label: String, + pub label: FixedString, /// The url opened when clicking the button. /// /// **Note**: Bots cannot access activity button URL. #[serde(default)] - pub url: String, + pub url: FixedString, } /// The assets for an activity. @@ -103,13 +102,13 @@ pub struct ActivityButton { #[non_exhaustive] pub struct ActivityAssets { /// The ID for a large asset of the activity, usually a snowflake. - pub large_image: Option, + pub large_image: Option, /// Text displayed when hovering over the large image of the activity. - pub large_text: Option, + pub large_text: Option, /// The ID for a small asset of the activity, usually a snowflake. - pub small_image: Option, + pub small_image: Option, /// Text displayed when hovering over the small image of the activity. - pub small_text: Option, + pub small_text: Option, } bitflags! { @@ -148,7 +147,7 @@ bitflags! { #[non_exhaustive] pub struct ActivityParty { /// The ID of the party. - pub id: Option, + pub id: Option, /// Used to show the party's current and maximum size. pub size: Option<[u32; 2]>, } @@ -161,12 +160,12 @@ pub struct ActivityParty { #[non_exhaustive] pub struct ActivitySecrets { /// The secret for joining a party. - pub join: Option, + pub join: Option, /// The secret for a specific instanced match. #[serde(rename = "match")] - pub match_: Option, + pub match_: Option, /// The secret for spectating an activity. - pub spectate: Option, + pub spectate: Option, } /// Representation of an emoji used in a custom status @@ -177,7 +176,7 @@ pub struct ActivitySecrets { #[non_exhaustive] pub struct ActivityEmoji { /// The name of the emoji. - pub name: String, + pub name: FixedString, /// The id of the emoji. pub id: Option, /// Whether this emoji is animated. @@ -188,11 +187,9 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#activity-object-activity-types). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ActivityType { /// An indicator that the user is playing a game. - #[default] Playing = 0, /// An indicator that the user is streaming to a service. Streaming = 1, @@ -217,7 +214,7 @@ enum_number! { #[non_exhaustive] pub struct Gateway { /// The gateway to connect to. - pub url: String, + pub url: FixedString, } /// Information detailing the current active status of a [`User`]. @@ -239,8 +236,9 @@ pub struct ClientStatus { /// /// [Discord docs](https://discord.com/developers/docs/resources/user#user-object), /// [modification description](https://discord.com/developers/docs/topics/gateway-events#presence-update). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct PresenceUser { pub id: UserId, @@ -248,10 +246,10 @@ pub struct PresenceUser { pub bot: Option, #[serde(default, skip_serializing_if = "Option::is_none", with = "discriminator")] pub discriminator: Option, - pub email: Option, + pub email: Option, pub mfa_enabled: Option, #[serde(rename = "username")] - pub name: Option, + pub name: Option>, pub verified: Option, pub public_flags: Option, } @@ -262,9 +260,9 @@ impl PresenceUser { /// If one of [`User`]'s required fields is None in `self`, None is returned. #[must_use] pub fn into_user(self) -> Option { - Some(User { + let (bot, verified, mfa_enabled) = (self.bot()?, self.verified(), self.mfa_enabled()); + let mut user = User { avatar: self.avatar, - bot: self.bot?, discriminator: self.discriminator, global_name: None, id: self.id, @@ -273,14 +271,18 @@ impl PresenceUser { banner: None, accent_colour: None, member: None, - system: false, - mfa_enabled: self.mfa_enabled.unwrap_or_default(), locale: None, - verified: self.verified, email: self.email, flags: self.public_flags.unwrap_or_default(), premium_type: PremiumType::None, - }) + __generated_flags: UserGeneratedFlags::empty(), + }; + + user.set_bot(bot); + user.set_verified(verified); + user.set_mfa_enabled(mfa_enabled.unwrap_or_default()); + + Some(user) } /// Attempts to convert this [`PresenceUser`] instance into a [`User`]. @@ -292,20 +294,6 @@ impl PresenceUser { pub fn to_user(&self) -> Option { self.clone().into_user() } - - #[cfg(feature = "cache")] // method is only used with the cache feature enabled - pub(crate) fn update_with_user(&mut self, user: &User) { - self.id = user.id; - if let Some(avatar) = user.avatar { - self.avatar = Some(avatar); - } - self.bot = Some(user.bot); - self.discriminator = user.discriminator; - self.name = Some(user.name.clone()); - if let Some(public_flags) = user.public_flags { - self.public_flags = Some(public_flags); - } - } } /// Information detailing the current online status of a [`User`]. @@ -323,11 +311,17 @@ pub struct Presence { pub status: OnlineStatus, /// [`User`]'s current activities. #[serde(default)] - pub activities: Vec, + pub activities: FixedArray, /// The devices a user are currently active on, if available. pub client_status: Option, } +impl ExtractKey for Presence { + fn extract_key(&self) -> &UserId { + &self.user.id + } +} + /// An initial set of information given after IDENTIFYing to the gateway. /// /// [Discord docs](https://discord.com/developers/docs/topics/gateway#ready-ready-event-fields). @@ -341,11 +335,11 @@ pub struct Ready { /// Information about the user including email pub user: CurrentUser, /// Guilds the user is in - pub guilds: Vec, + pub guilds: FixedArray, /// Used for resuming connections - pub session_id: String, + pub session_id: FixedString, /// Gateway URL for resuming connections - pub resume_gateway_url: String, + pub resume_gateway_url: FixedString, /// Shard information associated with this session, if sent when identifying pub shard: Option, /// Contains id and flags @@ -365,19 +359,22 @@ pub struct SessionStartLimit { /// The total number of session starts within the ratelimit period allowed. pub total: u64, /// The number of identify requests allowed per 5 seconds. - pub max_concurrency: u64, + /// + /// This is almost always 1, but for large bots (in more than 150,000 servers) it can be + /// larger. + pub max_concurrency: NonZeroU16, } #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Copy, Debug)] pub struct ShardInfo { pub id: ShardId, - pub total: u32, + pub total: NonZeroU16, } impl ShardInfo { #[must_use] - pub(crate) fn new(id: ShardId, total: u32) -> Self { + pub(crate) fn new(id: ShardId, total: NonZeroU16) -> Self { Self { id, total, @@ -387,7 +384,7 @@ impl ShardInfo { impl<'de> serde::Deserialize<'de> for ShardInfo { fn deserialize>(deserializer: D) -> StdResult { - <(u32, u32)>::deserialize(deserializer).map(|(id, total)| ShardInfo { + <(u16, NonZeroU16)>::deserialize(deserializer).map(|(id, total)| ShardInfo { id: ShardId(id), total, }) @@ -410,8 +407,8 @@ impl serde::Serialize for ShardInfo { #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct ActivityTimestamps { - pub end: Option, - pub start: Option, + pub end: Option, + pub start: Option, } bitflags! { @@ -487,9 +484,6 @@ bitflags! { /// - GUILD_BAN_ADD /// - GUILD_BAN_REMOVE const GUILD_MODERATION = 1 << 2; - /// Backwards compatibility with old gateway event name. Same as GUILD_MODERATION - #[deprecated = "Use [`Self::GUILD_MODERATION`] instead"] - const GUILD_BANS = 1 << 2; /// Enables the following gateway events: /// - GUILD_EMOJIS_UPDATE @@ -631,18 +625,6 @@ impl GatewayIntents { self.contains(Self::GUILD_MEMBERS) } - /// Shorthand for checking that the set of intents contains the [GUILD_BANS] intent. - /// - /// [GUILD_BANS]: Self::GUILD_BANS - /// - /// This is the same as calling guild_moderation since Discord changed the name - #[must_use] - #[deprecated = "Use [`Self::guild_moderation`] instead"] - pub const fn guild_bans(self) -> bool { - #[allow(deprecated)] // this is a deprecated method itself - self.contains(Self::GUILD_BANS) - } - /// Shorthand for checking that the set of intents contains the [GUILD_MODERATION] intent. /// /// [GUILD_MODERATION]: Self::GUILD_MODERATION diff --git a/src/model/guild/audit_log/change.rs b/src/model/guild/audit_log/change.rs index f5a57ed747e..f54db74e997 100644 --- a/src/model/guild/audit_log/change.rs +++ b/src/model/guild/audit_log/change.rs @@ -1,4 +1,6 @@ -use crate::json::Value; +use nonmax::NonMaxU16; + +use crate::internal::prelude::*; use crate::model::channel::PermissionOverwrite; use crate::model::guild::automod::{Action, EventType, TriggerMetadata, TriggerType}; use crate::model::guild::{ @@ -20,7 +22,7 @@ use crate::model::{Permissions, Timestamp}; #[non_exhaustive] pub struct AffectedRole { pub id: RoleId, - pub name: String, + pub name: FixedString, } #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] @@ -29,7 +31,7 @@ pub struct AffectedRole { #[non_exhaustive] pub enum EntityType { Int(u64), - Str(String), + Str(FixedString), } impl<'de> serde::Deserialize<'de> for EntityType { @@ -43,11 +45,8 @@ macro_rules! generate_change { $( #[doc = $doc:literal] )? $key:literal => $name:ident ($type:ty), )* ) => { - #[cfg_attr(not(feature = "simd_json"), allow(clippy::derive_partial_eq_without_eq))] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] - // serde_json's Value impls Eq, simd-json's Value doesn't - #[cfg_attr(not(feature = "simd_json"), derive(Eq))] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] #[non_exhaustive] #[serde(tag = "key")] #[serde(rename_all = "snake_case")] @@ -71,25 +70,25 @@ macro_rules! generate_change { RolesAdded { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "old_value")] - old: Option>, + old: Option>, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "new_value")] - new: Option>, + new: Option>, }, /// Role was removed to a member. #[serde(rename = "$remove")] RolesRemove { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "old_value")] - old: Option>, + old: Option>, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "new_value")] - new: Option>, + new: Option>, }, /// Unknown key was changed. Other { - name: String, + name: FixedString, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "old_value")] old_value: Option, @@ -119,7 +118,7 @@ macro_rules! generate_change { } generate_change! { - "actions" => Actions(Vec), + "actions" => Actions(FixedArray), /// AFK channel was changed. "afk_channel_id" => AfkChannelId(ChannelId), /// AFK timeout duration was changed. @@ -130,7 +129,7 @@ generate_change! { "application_id" => ApplicationId(ApplicationId), /// Thread is now archived/unarchived. "archived" => Archived(bool), - "asset" => Asset(String), + "asset" => Asset(FixedString), /// Auto archive duration of a thread was changed. "auto_archive_duration" => AutoArchiveDuration(u16), /// Availability of a sticker was changed. @@ -144,7 +143,7 @@ generate_change! { /// Channel for invite code or guild scheduled event was changed. "channel_id" => ChannelId(ChannelId), /// Invite code was changed. - "code" => Code(String), + "code" => Code(FixedString), /// Role color was changed. "color" => Color(u32), /// Member timeout state was changed. @@ -158,7 +157,7 @@ generate_change! { /// Permission on a text or voice channel was denied for a role. "deny" => Deny(Permissions), /// Description for guild, sticker, or guild scheduled event was changed. - "description" => Description(String), + "description" => Description(FixedString), /// Guild's discovery splash was changed. "discovery_splash_hash" => DiscoverySplashHash(ImageHash), "enabled" => Enabled(bool), @@ -167,8 +166,8 @@ generate_change! { /// Entity type of guild scheduled event was changed. "entity_type" => EntityType(u64), "event_type" => EventType(EventType), - "exempt_channels" => ExemptChannels(Vec), - "exempt_roles" => ExemptRoles(Vec), + "exempt_channels" => ExemptChannels(FixedArray), + "exempt_roles" => ExemptRoles(FixedArray), /// Behavior of the expiration of an integration was changed. "expire_behavior" => ExpireBehavior(u64), /// Grace period of the expiration of an integration was changed. @@ -194,7 +193,7 @@ generate_change! { /// ID of the user who created the invite. "inviter_id" => InviterId(UserId), /// Location for a guild scheduled event was changed. - "location" => Location(String), + "location" => Location(FixedString), /// Thread was locked/unlocked. "locked" => Locked(bool), /// How long invite code lasts was changed. @@ -208,21 +207,21 @@ generate_change! { /// User was server muted/unmuted. "mute" => Mute(bool), /// Name of an entity was changed. - "name" => Name(String), + "name" => Name(FixedString), /// Nickname of a member was changed. - "nick" => Nick(String), + "nick" => Nick(FixedString), /// Channel NSFW restriction was changed. "nsfw" => Nsfw(bool), /// Owner of a guild was changed. "owner_id" => OwnerId(UserId), /// Permissions on a channel were changed. - "permission_overwrites" => PermissionOverwrites(Vec), + "permission_overwrites" => PermissionOverwrites(FixedArray), /// Permissions for a role were changed. "permissions" => Permissions(Permissions), /// Channel or role position was changed. "position" => Position(u32), /// Preferred locale of a guild was changed. - "preferred_locale" => PreferredLocale(String), + "preferred_locale" => PreferredLocale(FixedString), /// Privacy level of the stage instance was changed. "privacy_level" => PrivacyLevel(u64), /// Number of days after which inactive and role-unassigned members are kicked was changed. @@ -232,7 +231,7 @@ generate_change! { /// Ratelimit per user in a text channel was changed. "rate_limit_per_user" => RateLimitPerUser(u16), /// Region of a guild was changed. - "region" => Region(String), + "region" => Region(FixedString), /// ID of the rules channel was changed. "rules_channel_id" => RulesChannelId(ChannelId), /// Invite splash page artwork was changed. @@ -244,23 +243,23 @@ generate_change! { /// ID of the system channel was changed. "system_channel_id" => SystemChannelId(ChannelId), /// Related emoji of a sticker was changed. - "tags" => Tags(String), + "tags" => Tags(FixedString), /// Whether an invite is temporary or never expires was changed. "temporary" => Temporary(bool), /// Topic of a text channel or stage instance was changed. - "topic" => Topic(String), + "topic" => Topic(FixedString), "trigger_metadata" => TriggerMetadata(TriggerMetadata), "trigger_type" => TriggerType(TriggerType), /// Type of a created entity. "type" => Type(EntityType), /// Unicode emoji of a role icon was changed. - "unicode_emoji" => UnicodeEmoji(String), + "unicode_emoji" => UnicodeEmoji(FixedString), /// Maximum number of users in a voice channel was changed. - "user_limit" => UserLimit(u64), + "user_limit" => UserLimit(NonMaxU16), /// Number of uses of an invite was changed. "uses" => Uses(u64), /// Guild invite vanity url was changed. - "vanity_url_code" => VanityUrlCode(String), + "vanity_url_code" => VanityUrlCode(FixedString), /// Required verification level for new members was changed. "verification_level" => VerificationLevel(VerificationLevel), /// Channel of the server widget was changed. @@ -271,8 +270,10 @@ generate_change! { #[cfg(test)] mod tests { + use serde_json::json; + use super::*; - use crate::json::{assert_json, json}; + use crate::model::utils::assert_json; #[test] fn afk_channel_id_variant() { @@ -301,7 +302,7 @@ mod tests { fn entity_type_variant() { let value = Change::Type { old: Some(EntityType::Int(123)), - new: Some(EntityType::Str("discord".into())), + new: Some(EntityType::Str(FixedString::from_static_trunc("discord"))), }; assert_json(&value, json!({"key": "type", "old_value": 123, "new_value": "discord"})); } diff --git a/src/model/guild/audit_log/mod.rs b/src/model/guild/audit_log/mod.rs index c3cb7454cc7..8f88168f8cf 100644 --- a/src/model/guild/audit_log/mod.rs +++ b/src/model/guild/audit_log/mod.rs @@ -1,15 +1,15 @@ //! Audit log types for administrative actions within guilds. -use std::mem::transmute; - +use nonmax::{NonMaxU32, NonMaxU64}; use serde::ser::{Serialize, Serializer}; mod change; mod utils; pub use change::{AffectedRole, Change, EntityType}; -use utils::{optional_string, users, webhooks}; +use utils::optional_string; +use crate::internal::prelude::*; use crate::model::prelude::*; /// Determines the action that was done on a target. @@ -69,22 +69,62 @@ impl Action { pub fn from_value(value: u8) -> Action { match value { 1 => Action::GuildUpdate, - 10..=12 => Action::Channel(unsafe { transmute(value) }), - 13..=15 => Action::ChannelOverwrite(unsafe { transmute(value) }), - 20..=28 => Action::Member(unsafe { transmute(value) }), - 30..=32 => Action::Role(unsafe { transmute(value) }), - 40..=42 => Action::Invite(unsafe { transmute(value) }), - 50..=52 => Action::Webhook(unsafe { transmute(value) }), - 60..=62 => Action::Emoji(unsafe { transmute(value) }), - 72..=75 => Action::Message(unsafe { transmute(value) }), - 80..=82 => Action::Integration(unsafe { transmute(value) }), - 83..=85 => Action::StageInstance(unsafe { transmute(value) }), - 90..=92 => Action::Sticker(unsafe { transmute(value) }), - 100..=102 => Action::ScheduledEvent(unsafe { transmute(value) }), - 110..=112 => Action::Thread(unsafe { transmute(value) }), - 140..=145 => Action::AutoMod(unsafe { transmute(value) }), - 150..=151 => Action::CreatorMonetization(unsafe { transmute(value) }), - 192..=193 => Action::VoiceChannelStatus(unsafe { transmute(value) }), + 10 => Action::Channel(ChannelAction::Create), + 11 => Action::Channel(ChannelAction::Update), + 12 => Action::Channel(ChannelAction::Delete), + 13 => Action::ChannelOverwrite(ChannelOverwriteAction::Create), + 14 => Action::ChannelOverwrite(ChannelOverwriteAction::Update), + 15 => Action::ChannelOverwrite(ChannelOverwriteAction::Delete), + 20 => Action::Member(MemberAction::Kick), + 21 => Action::Member(MemberAction::Prune), + 22 => Action::Member(MemberAction::BanAdd), + 23 => Action::Member(MemberAction::BanRemove), + 24 => Action::Member(MemberAction::Update), + 25 => Action::Member(MemberAction::RoleUpdate), + 26 => Action::Member(MemberAction::MemberMove), + 27 => Action::Member(MemberAction::MemberDisconnect), + 28 => Action::Member(MemberAction::BotAdd), + 30 => Action::Role(RoleAction::Create), + 31 => Action::Role(RoleAction::Update), + 32 => Action::Role(RoleAction::Delete), + 40 => Action::Invite(InviteAction::Create), + 41 => Action::Invite(InviteAction::Update), + 42 => Action::Invite(InviteAction::Delete), + 50 => Action::Webhook(WebhookAction::Create), + 51 => Action::Webhook(WebhookAction::Update), + 52 => Action::Webhook(WebhookAction::Delete), + 60 => Action::Emoji(EmojiAction::Create), + 61 => Action::Emoji(EmojiAction::Update), + 62 => Action::Emoji(EmojiAction::Delete), + 72 => Action::Message(MessageAction::Delete), + 73 => Action::Message(MessageAction::BulkDelete), + 74 => Action::Message(MessageAction::Pin), + 75 => Action::Message(MessageAction::Unpin), + 80 => Action::Integration(IntegrationAction::Create), + 81 => Action::Integration(IntegrationAction::Update), + 82 => Action::Integration(IntegrationAction::Delete), + 83 => Action::StageInstance(StageInstanceAction::Create), + 84 => Action::StageInstance(StageInstanceAction::Update), + 85 => Action::StageInstance(StageInstanceAction::Delete), + 90 => Action::Sticker(StickerAction::Create), + 91 => Action::Sticker(StickerAction::Update), + 92 => Action::Sticker(StickerAction::Delete), + 100 => Action::ScheduledEvent(ScheduledEventAction::Create), + 101 => Action::ScheduledEvent(ScheduledEventAction::Update), + 102 => Action::ScheduledEvent(ScheduledEventAction::Delete), + 110 => Action::Thread(ThreadAction::Create), + 111 => Action::Thread(ThreadAction::Update), + 112 => Action::Thread(ThreadAction::Delete), + 140 => Action::AutoMod(AutoModAction::RuleCreate), + 141 => Action::AutoMod(AutoModAction::RuleUpdate), + 142 => Action::AutoMod(AutoModAction::RuleDelete), + 143 => Action::AutoMod(AutoModAction::BlockMessage), + 144 => Action::AutoMod(AutoModAction::FlagToChannel), + 145 => Action::AutoMod(AutoModAction::UserCommunicationDisabled), + 150 => Action::CreatorMonetization(CreatorMonetizationAction::RequestCreated), + 151 => Action::CreatorMonetization(CreatorMonetizationAction::TermsAccepted), + 192 => Action::VoiceChannelStatus(VoiceChannelStatusAction::StatusUpdate), + 193 => Action::VoiceChannelStatus(VoiceChannelStatusAction::StatusDelete), _ => Action::Unknown(value), } } @@ -295,26 +335,24 @@ pub enum VoiceChannelStatusAction { pub struct AuditLogs { /// List of audit log entries, sorted from most to least recent. #[serde(rename = "audit_log_entries")] - pub entries: Vec, + pub entries: FixedArray, /// List of auto moderation rules referenced in the audit log. - pub auto_moderation_rules: Vec, + pub auto_moderation_rules: FixedArray, /// List of application commands referenced in the audit log. - pub application_commands: Vec, + pub application_commands: FixedArray, /// List of guild scheduled events referenced in the audit log. - pub guild_scheduled_events: Vec, + pub guild_scheduled_events: FixedArray, /// List of partial integration objects. - pub integrations: Vec, + pub integrations: FixedArray, /// List of threads referenced in the audit log. /// /// Threads referenced in THREAD_CREATE and THREAD_UPDATE events are included in the threads /// map since archived threads might not be kept in memory by clients. - pub threads: Vec, + pub threads: FixedArray, /// List of users referenced in the audit log. - #[serde(with = "users")] - pub users: HashMap, + pub users: ExtractMap, /// List of webhooks referenced in the audit log. - #[serde(with = "webhooks")] - pub webhooks: HashMap, + pub webhooks: ExtractMap, } /// Partial version of [`Integration`], used in [`AuditLogs::integrations`]. @@ -325,9 +363,9 @@ pub struct AuditLogs { #[non_exhaustive] pub struct PartialIntegration { pub id: IntegrationId, - pub name: String, + pub name: FixedString, #[serde(rename = "type")] - pub kind: String, + pub kind: FixedString, pub account: IntegrationAccount, pub application: Option, } @@ -343,11 +381,12 @@ pub struct AuditLogEntry { #[serde(rename = "action_type")] pub action: Action, /// What was the reasoning by doing an action on a target? If there was one. - pub reason: Option, + pub reason: Option, /// The user that did this action on a target. - pub user_id: UserId, + pub user_id: Option, /// What changes were made. - pub changes: Option>, + #[serde(default)] + pub changes: Vec, /// The id of this entry. pub id: AuditLogEntryId, /// Some optional data associated with this entry. @@ -361,38 +400,38 @@ pub struct AuditLogEntry { #[non_exhaustive] pub struct Options { /// Name of the Auto Moderation rule that was triggered. - pub auto_moderation_rule_name: Option, + pub auto_moderation_rule_name: Option, /// Trigger type of the Auto Moderation rule that was triggered. - pub auto_moderation_rule_trigger_type: Option, + pub auto_moderation_rule_trigger_type: Option, /// ID of the app whose permissions were targeted. pub application_id: Option, /// Number of days after which inactive members were kicked. #[serde(default, with = "optional_string")] - pub delete_member_days: Option, + pub delete_member_days: Option, /// Number of members removed by the prune #[serde(default, with = "optional_string")] - pub members_removed: Option, + pub members_removed: Option, /// Channel in which the messages were deleted #[serde(default)] pub channel_id: Option, /// Number of deleted messages. #[serde(default, with = "optional_string")] - pub count: Option, + pub count: Option, /// Id of the overwritten entity #[serde(default)] pub id: Option, /// Type of overwritten entity ("member" or "role"). #[serde(default, rename = "type")] - pub kind: Option, + pub kind: Option, /// Message that was pinned or unpinned. #[serde(default)] pub message_id: Option, /// Name of the role if type is "role" #[serde(default)] - pub role_name: Option, + pub role_name: Option, /// The status of a voice channel when set. #[serde(default)] - pub status: Option, + pub status: Option, } #[cfg(test)] @@ -471,7 +510,7 @@ mod tests { #[test] fn action_serde() { - use crate::json::{self, json}; + use serde_json::{from_value, json}; #[derive(Debug, Deserialize, Serialize)] struct T { @@ -482,7 +521,7 @@ mod tests { "action": 234, }); - let value = json::from_value::(value).unwrap(); + let value = from_value::(value).unwrap(); assert_eq!(value.action.num(), 234); assert!(matches!(value.action, Action::Unknown(234))); diff --git a/src/model/guild/audit_log/utils.rs b/src/model/guild/audit_log/utils.rs index 353acc02adb..f91cab1d5ca 100644 --- a/src/model/guild/audit_log/utils.rs +++ b/src/model/guild/audit_log/utils.rs @@ -1,67 +1,71 @@ -/// Used with `#[serde(with = "users")]` -pub mod users { - use std::collections::HashMap; - - use serde::Deserializer; - - use crate::model::id::UserId; - use crate::model::user::User; - use crate::model::utils::SequenceToMapVisitor; - - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - deserializer.deserialize_seq(SequenceToMapVisitor::new(|u: &User| u.id)) - } - - pub use crate::model::utils::serialize_map_values as serialize; -} - -/// Used with `#[serde(with = "webhooks")]` -pub mod webhooks { - use std::collections::HashMap; - - use serde::Deserializer; - - use crate::model::id::WebhookId; - use crate::model::utils::SequenceToMapVisitor; - use crate::model::webhook::Webhook; - - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - deserializer.deserialize_seq(SequenceToMapVisitor::new(|h: &Webhook| h.id)) - } - - pub use crate::model::utils::serialize_map_values as serialize; -} - /// Deserializes an optional string containing a valid integer as `Option`. /// /// Used with `#[serde(with = "optional_string")]`. pub mod optional_string { use std::fmt; + use std::marker::PhantomData; + use std::str::FromStr; use serde::de::{Deserializer, Error, Visitor}; use serde::ser::Serializer; - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - deserializer.deserialize_option(OptionalStringVisitor) + // Workaround for https://github.com/LPGhatguy/nonmax/issues/17 + pub(crate) trait TryFromU64 + where + Self: Sized, + { + type Err: fmt::Display; + fn try_from_u64(value: u64) -> Result; } - pub fn serialize(value: &Option, serializer: S) -> Result { + impl TryFromU64 for u64 { + type Err = std::convert::Infallible; + fn try_from_u64(value: u64) -> Result { + Ok(value) + } + } + + impl TryFromU64 for nonmax::NonMaxU64 { + type Err = nonmax::TryFromIntError; + fn try_from_u64(value: u64) -> Result { + Self::try_from(value) + } + } + + impl TryFromU64 for nonmax::NonMaxU32 { + type Err = nonmax::TryFromIntError; + fn try_from_u64(value: u64) -> Result { + Self::try_from(u32::try_from(value)?) + } + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + T: FromStr + TryFromU64, + ::Err: fmt::Display, + { + deserializer.deserialize_option(OptionalStringVisitor::(PhantomData)) + } + + pub fn serialize( + value: &Option, + serializer: S, + ) -> Result { match value { Some(value) => serializer.serialize_some(&value.to_string()), None => serializer.serialize_none(), } } - struct OptionalStringVisitor; + struct OptionalStringVisitor(PhantomData); - impl<'de> Visitor<'de> for OptionalStringVisitor { - type Value = Option; + impl<'de, T> Visitor<'de> for OptionalStringVisitor + where + T: FromStr + TryFromU64, + ::Err: fmt::Display, + { + type Value = Option; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("an optional integer or a string with a valid number inside") @@ -71,23 +75,18 @@ pub mod optional_string { self, deserializer: D, ) -> Result { - deserializer.deserialize_any(OptionalStringVisitor) + deserializer.deserialize_any(OptionalStringVisitor(PhantomData)) } fn visit_none(self) -> Result { Ok(None) } - /// Called by the `simd_json` crate - fn visit_unit(self) -> Result { - Ok(None) + fn visit_u64(self, val: u64) -> Result { + T::try_from_u64(val).map(Some).map_err(Error::custom) } - fn visit_u64(self, val: u64) -> Result, E> { - Ok(Some(val)) - } - - fn visit_str(self, string: &str) -> Result, E> { + fn visit_str(self, string: &str) -> Result { string.parse().map(Some).map_err(Error::custom) } } @@ -95,8 +94,10 @@ pub mod optional_string { #[cfg(test)] mod tests { + use serde_json::json; + use super::optional_string; - use crate::json::{assert_json, json}; + use crate::model::utils::assert_json; #[test] fn optional_string_module() { diff --git a/src/model/guild/automod.rs b/src/model/guild/automod.rs index 7c169a8ca35..149dce8dbf6 100644 --- a/src/model/guild/automod.rs +++ b/src/model/guild/automod.rs @@ -8,6 +8,7 @@ use serde::de::{Deserializer, Error}; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; +use crate::internal::prelude::*; use crate::model::id::*; /// Configured auto moderation rule. @@ -23,7 +24,7 @@ pub struct Rule { /// ID of the guild this rule belongs to. pub guild_id: GuildId, /// Name of the rule. - pub name: String, + pub name: FixedString, /// ID of the user which created the rule. pub creator_id: UserId, /// Event context in which the rule should be checked. @@ -32,17 +33,17 @@ pub struct Rule { #[serde(flatten)] pub trigger: Trigger, /// Actions which will execute when the rule is triggered. - pub actions: Vec, + pub actions: FixedArray, /// Whether the rule is enabled. pub enabled: bool, /// Roles that should not be affected by the rule. /// /// Maximum of 20. - pub exempt_roles: Vec, + pub exempt_roles: FixedArray, /// Channels that should not be affected by the rule. /// /// Maximum of 50. - pub exempt_channels: Vec, + pub exempt_channels: FixedArray, } /// Indicates in what event context a rule should be checked. @@ -330,7 +331,7 @@ pub enum Action { /// Additional explanation that will be shown to members whenever their message is blocked /// /// Maximum of 150 characters - custom_message: Option, + custom_message: Option>, }, /// Logs user content to a specified channel. Alert(ChannelId), @@ -381,15 +382,15 @@ pub struct ActionExecution { /// Requires [`GatewayIntents::MESSAGE_CONTENT`] to receive non-empty values. /// /// [`GatewayIntents::MESSAGE_CONTENT`]: crate::model::gateway::GatewayIntents::MESSAGE_CONTENT - pub content: String, + pub content: FixedString, /// Word or phrase configured in the rule that triggered the rule. - pub matched_keyword: Option, + pub matched_keyword: Option, /// Substring in content that triggered the rule. /// /// Requires [`GatewayIntents::MESSAGE_CONTENT`] to receive non-empty values. /// /// [`GatewayIntents::MESSAGE_CONTENT`]: crate::model::gateway::GatewayIntents::MESSAGE_CONTENT - pub matched_content: Option, + pub matched_content: Option, } /// Helper struct for the (de)serialization of `Action`. @@ -400,7 +401,7 @@ struct RawActionMetadata { #[serde(skip_serializing_if = "Option::is_none")] duration_seconds: Option, #[serde(skip_serializing_if = "Option::is_none")] - custom_message: Option, + custom_message: Option>, } /// Helper struct for the (de)serialization of `Action`. @@ -437,7 +438,7 @@ impl<'de> Deserialize<'de> for Action { .duration_seconds .ok_or_else(|| Error::missing_field("duration_seconds"))?, )), - ActionType::Unknown(unknown) => Action::Unknown(unknown), + ActionType(unknown) => Action::Unknown(unknown), }) } } @@ -496,7 +497,6 @@ enum_number! { /// /// [Discord docs](https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-action-object-action-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ActionType { BlockMessage = 1, @@ -508,9 +508,10 @@ enum_number! { #[cfg(test)] mod tests { + use serde_json::json; use super::*; - use crate::json::{assert_json, json}; + use crate::model::utils::assert_json; #[test] fn rule_trigger_serde() { diff --git a/src/model/guild/emoji.rs b/src/model/guild/emoji.rs index 3ce0b01abf1..5f261888aca 100644 --- a/src/model/guild/emoji.rs +++ b/src/model/guild/emoji.rs @@ -1,25 +1,17 @@ use std::fmt; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::cache::Cache; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::http::CacheHttp; -#[cfg(all(feature = "cache", feature = "model"))] use crate::internal::prelude::*; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::model::id::GuildId; use crate::model::id::{EmojiId, RoleId}; use crate::model::user::User; use crate::model::utils::default_true; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::model::ModelError; /// Represents a custom guild emoji, which can either be created using the API, or via an /// integration. Emojis created using the API only work within the guild it was created in. /// /// [Discord docs](https://discord.com/developers/docs/resources/emoji#emoji-object). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct Emoji { /// Whether the emoji is animated. @@ -33,7 +25,7 @@ pub struct Emoji { pub id: EmojiId, /// The name of the emoji. It must be at least 2 characters long and can only contain /// alphanumeric characters and underscores. - pub name: String, + pub name: FixedString, /// Whether the emoji is managed via an [`Integration`] service. /// /// [`Integration`]: super::Integration @@ -47,122 +39,13 @@ pub struct Emoji { /// /// [`Role`]: super::Role #[serde(default)] - pub roles: Vec, + pub roles: FixedArray, /// The user who created the emoji. pub user: Option, } #[cfg(feature = "model")] impl Emoji { - /// Deletes the emoji. This method requires the cache to fetch the guild ID. - /// - /// **Note**: If the emoji was created by the current user, requires either the [Create Guild - /// Expressions] or the [Manage Guild Expressions] permission. Otherwise, the [Manage Guild - /// Expressions] permission is required. - /// - /// # Examples - /// - /// Delete a given emoji: - /// - /// ```rust,no_run - /// # use serenity::client::Context; - /// # use serenity::model::prelude::Emoji; - /// # - /// # async fn example(ctx: &Context, emoji: Emoji) -> Result<(), Box> { - /// // assuming emoji has been set already - /// match emoji.delete(&ctx).await { - /// Ok(()) => println!("Emoji deleted."), - /// Err(_) => println!("Could not delete emoji."), - /// } - /// # Ok(()) - /// # } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission, or may return - /// [`ModelError::ItemMissing`] if the emoji is not in the cache. - /// - /// [Create Guild Expressions]: crate::model::Permissions::CREATE_GUILD_EXPRESSIONS - /// [Manage Guild Expressions]: crate::model::Permissions::MANAGE_GUILD_EXPRESSIONS - #[deprecated = "Use Guild(Id)::delete_emoji, this performs a loop over all guilds!"] - #[cfg(feature = "cache")] - #[allow(deprecated)] - #[inline] - pub async fn delete(&self, cache_http: impl CacheHttp) -> Result<()> { - let guild_id = self.try_find_guild_id(&cache_http)?; - guild_id.delete_emoji(cache_http.http(), self).await - } - - /// Edits the emoji by updating it with a new name. This method requires the cache to fetch the - /// guild ID. - /// - /// **Note**: If the emoji was created by the current user, requires either the [Create Guild - /// Expressions] or the [Manage Guild Expressions] permission. Otherwise, the [Manage Guild - /// Expressions] permission is required. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission, or if an invalid name is - /// given. - /// - /// [Create Guild Expressions]: crate::model::Permissions::CREATE_GUILD_EXPRESSIONS - /// [Manage Guild Expressions]: crate::model::Permissions::MANAGE_GUILD_EXPRESSIONS - #[deprecated = "Use Guild(Id)::edit_emoji, this performs a loop over all guilds!"] - #[cfg(feature = "cache")] - #[allow(deprecated)] - pub async fn edit(&mut self, cache_http: impl CacheHttp, name: &str) -> Result<()> { - let guild_id = self.try_find_guild_id(&cache_http)?; - *self = guild_id.edit_emoji(cache_http.http(), self.id, name).await?; - Ok(()) - } - - /// Finds the [`Guild`] that owns the emoji by looking through the Cache. - /// - /// [`Guild`]: super::Guild - /// - /// # Examples - /// - /// Print the guild id that owns this emoji: - /// - /// ```rust,no_run - /// # use serenity::cache::Cache; - /// # use serenity::model::guild::Emoji; - /// # - /// # fn run(cache: Cache, emoji: Emoji) { - /// // assuming emoji has been set already - /// if let Some(guild_id) = emoji.find_guild_id(&cache) { - /// println!("{} is owned by {}", emoji.name, guild_id); - /// } - /// # } - /// ``` - #[deprecated = "This performs a loop over all guilds and should not be used."] - #[cfg(feature = "cache")] - #[allow(deprecated)] - #[must_use] - pub fn find_guild_id(&self, cache: impl AsRef) -> Option { - for guild_entry in cache.as_ref().guilds.iter() { - let guild = guild_entry.value(); - - if guild.emojis.contains_key(&self.id) { - return Some(guild.id); - } - } - - None - } - - #[deprecated = "This performs a loop over all guilds and should not be used."] - #[cfg(feature = "cache")] - #[allow(deprecated)] - #[inline] - fn try_find_guild_id(&self, cache_http: impl CacheHttp) -> Result { - cache_http - .cache() - .and_then(|c| self.find_guild_id(c)) - .ok_or(Error::Model(ModelError::ItemMissing)) - } - /// Generates a URL to the emoji's image. /// /// # Examples @@ -177,10 +60,9 @@ impl Emoji { /// println!("Direct link to emoji image: {}", emoji.url()); /// # } /// ``` - #[inline] #[must_use] pub fn url(&self) -> String { - let extension = if self.animated { "gif" } else { "png" }; + let extension = if self.animated() { "gif" } else { "png" }; cdn!("/emojis/{}.{}", self.id, extension) } } @@ -191,7 +73,7 @@ impl fmt::Display for Emoji { /// This is in the format of either `<:NAME:EMOJI_ID>` for normal emojis, or /// `` for animated emojis. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.animated { + if self.animated() { f.write_str(" for Emoji { + fn extract_key(&self) -> &EmojiId { + &self.id + } +} + impl From for EmojiId { /// Gets the Id of an [`Emoji`]. fn from(emoji: Emoji) -> EmojiId { diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index a810c4ef243..ead66460fc8 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -2,11 +2,11 @@ use std::fmt; #[cfg(feature = "model")] use futures::stream::Stream; +use nonmax::{NonMaxU16, NonMaxU8}; #[cfg(feature = "model")] use crate::builder::{ AddMember, - Builder, CreateChannel, CreateCommand, CreateScheduledEvent, @@ -31,8 +31,7 @@ use crate::gateway::ShardMessenger; use crate::http::{CacheHttp, Http, UserPagination}; #[cfg(feature = "model")] use crate::internal::prelude::*; -#[cfg(feature = "model")] -use crate::json::json; +use crate::model::error::Maximum; use crate::model::guild::SerializeIter; use crate::model::prelude::*; @@ -47,9 +46,8 @@ impl GuildId { /// Returns an [`Error::Http`] if the guild is unavailable. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn automod_rules(self, http: impl AsRef) -> Result> { - http.as_ref().get_automod_rules(self).await + pub async fn automod_rules(self, http: &Http) -> Result> { + http.get_automod_rules(self).await } /// Gets an auto moderation [`Rule`] of this guild by its ID via HTTP. @@ -61,13 +59,8 @@ impl GuildId { /// Returns an [`Error::Http`] if a rule with the given ID does not exist. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn automod_rule( - self, - http: impl AsRef, - rule_id: impl Into, - ) -> Result { - http.as_ref().get_automod_rule(self, rule_id.into()).await + pub async fn automod_rule(self, http: &Http, rule_id: RuleId) -> Result { + http.get_automod_rule(self, rule_id).await } /// Creates an auto moderation [`Rule`] in the guild. @@ -110,13 +103,12 @@ impl GuildId { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn create_automod_rule( self, - cache_http: impl CacheHttp, + http: &Http, builder: EditAutoModRule<'_>, ) -> Result { - builder.execute(cache_http, (self, None)).await + builder.execute(http, self, None).await } /// Edit an auto moderation [`Rule`], given its Id. @@ -128,14 +120,13 @@ impl GuildId { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn edit_automod_rule( self, - cache_http: impl CacheHttp, - rule_id: impl Into, + http: &Http, + rule_id: RuleId, builder: EditAutoModRule<'_>, ) -> Result { - builder.execute(cache_http, (self, Some(rule_id.into()))).await + builder.execute(http, self, Some(rule_id)).await } /// Deletes an auto moderation [`Rule`] from the guild. @@ -148,13 +139,13 @@ impl GuildId { /// does not exist. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn delete_automod_rule( self, - http: impl AsRef, - rule_id: impl Into, + http: &Http, + rule_id: RuleId, + reason: Option<&str>, ) -> Result<()> { - http.as_ref().delete_automod_rule(self, rule_id.into(), None).await + http.delete_automod_rule(self, rule_id, reason).await } /// Adds a [`User`] to this guild with a valid OAuth2 access token. @@ -165,14 +156,13 @@ impl GuildId { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - #[inline] pub async fn add_member( self, - cache_http: impl CacheHttp, - user_id: impl Into, - builder: AddMember, + http: &Http, + user_id: UserId, + builder: AddMember<'_>, ) -> Result> { - builder.execute(cache_http, (self, user_id.into())).await + builder.execute(http, self, user_id).await } /// Ban a [`User`] from the guild, deleting a number of days' worth of messages (`dmd`) between @@ -194,60 +184,26 @@ impl GuildId { /// # let http: Http = unimplemented!(); /// # let user = UserId::new(1); /// // assuming a `user` has already been bound - /// let _ = GuildId::new(81384788765712384).ban(&http, user, 4).await; + /// let _ = GuildId::new(81384788765712384).ban(&http, user, 4, None).await; /// # Ok(()) /// # } /// ``` /// /// # Errors /// - /// Returns a [`ModelError::DeleteMessageDaysAmount`] if the number of days' worth of messages + /// Returns a [`ModelError::TooLarge`] if the number of days' worth of messages /// to delete is over the maximum. /// /// Also can return [`Error::Http`] if the current user lacks permission. /// /// [Ban Members]: Permissions::BAN_MEMBERS - #[inline] - pub async fn ban(self, http: impl AsRef, user: impl Into, dmd: u8) -> Result<()> { - self._ban(http, user.into(), dmd, None).await - } - - /// Ban a [`User`] from the guild with a reason. Refer to [`Self::ban`] to further - /// documentation. - /// - /// # Errors - /// - /// In addition to the reasons [`Self::ban`] may return an error, may also return - /// [`Error::ExceededLimit`] if `reason` is too long. - #[inline] - pub async fn ban_with_reason( - self, - http: impl AsRef, - user: impl Into, - dmd: u8, - reason: impl AsRef, - ) -> Result<()> { - self._ban(http, user.into(), dmd, Some(reason.as_ref())).await - } - - async fn _ban( - self, - http: impl AsRef, - user: UserId, - dmd: u8, - reason: Option<&str>, - ) -> Result<()> { - if dmd > 7 { - return Err(Error::Model(ModelError::DeleteMessageDaysAmount(dmd))); - } - + pub async fn ban(self, http: &Http, user: UserId, dmd: u8, reason: Option<&str>) -> Result<()> { + Maximum::DeleteMessageDays.check_overflow(dmd.into())?; if let Some(reason) = reason { - if reason.chars().count() > 512 { - return Err(Error::ExceededLimit(reason.to_string(), 512)); - } + Maximum::AuditLogReason.check_overflow(reason.len())?; } - http.as_ref().ban_user(self, user, dmd, reason).await + http.ban_user(self, user, dmd, reason).await } /// Bans multiple users from the guild, returning the users that were and weren't banned, and @@ -291,14 +247,13 @@ impl GuildId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Ban Members]: Permissions::BAN_MEMBERS - #[inline] pub async fn bans( self, - http: impl AsRef, + http: &Http, target: Option, - limit: Option, + limit: Option, ) -> Result> { - http.as_ref().get_bans(self, target, limit).await + http.get_bans(self, target, limit).await } /// Gets a list of the guild's audit log entries @@ -311,16 +266,15 @@ impl GuildId { /// given. /// /// [View Audit Log]: Permissions::VIEW_AUDIT_LOG - #[inline] pub async fn audit_logs( self, - http: impl AsRef, + http: &Http, action_type: Option, user_id: Option, before: Option, - limit: Option, + limit: Option, ) -> Result { - http.as_ref().get_audit_logs(self, action_type, user_id, before, limit).await + http.get_audit_logs(self, action_type, user_id, before, limit).await } /// Gets all of the guild's channels over the REST API. @@ -328,13 +282,8 @@ impl GuildId { /// # Errors /// /// Returns [`Error::Http`] if the current user is not in the guild. - pub async fn channels( - self, - http: impl AsRef, - ) -> Result> { - let channels = http.as_ref().get_channels(self).await?; - - Ok(channels.into_iter().map(|c| (c.id, c)).collect()) + pub async fn channels(self, http: &Http) -> Result> { + http.get_channels(self).await } /// Creates a [`GuildChannel`] in the the guild. @@ -363,17 +312,15 @@ impl GuildId { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - #[inline] pub async fn create_channel( self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateChannel<'_>, ) -> Result { - builder.execute(cache_http, self).await + builder.execute(http, self).await } /// Creates an emoji in the guild with a name and base64-encoded image. @@ -395,19 +342,25 @@ impl GuildId { /// /// [`EditProfile::avatar`]: crate::builder::EditProfile::avatar /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS - #[inline] pub async fn create_emoji( self, - http: impl AsRef, + http: &Http, name: &str, image: &str, + reason: Option<&str>, ) -> Result { - let map = json!({ - "name": name, - "image": image, - }); + #[derive(serde::Serialize)] + struct CreateEmoji<'a> { + name: &'a str, + image: &'a str, + } - http.as_ref().create_emoji(self, &map, None).await + let body = CreateEmoji { + name, + image, + }; + + http.create_emoji(self, &body, reason).await } /// Creates an integration for the guild. @@ -419,20 +372,26 @@ impl GuildId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn create_integration( self, - http: impl AsRef, - integration_id: impl Into, + http: &Http, + integration_id: IntegrationId, kind: &str, + reason: Option<&str>, ) -> Result<()> { - let integration_id = integration_id.into(); - let map = json!({ - "id": integration_id, - "type": kind, - }); + #[derive(serde::Serialize)] + struct CreateIntegration<'a> { + id: IntegrationId, + #[serde(rename = "type")] + kind: &'a str, + } - http.as_ref().create_guild_integration(self, integration_id, &map, None).await + let body = CreateIntegration { + id: integration_id, + kind, + }; + + http.create_guild_integration(self, integration_id, &body, reason).await } /// Creates a new role in the guild with the data set, if any. @@ -443,17 +402,11 @@ impl GuildId { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] - pub async fn create_role( - self, - cache_http: impl CacheHttp, - builder: EditRole<'_>, - ) -> Result { - builder.execute(cache_http, (self, None)).await + pub async fn create_role(self, http: &Http, builder: EditRole<'_>) -> Result { + builder.execute(http, self, None).await } /// Creates a new scheduled event in the guild with the data set, if any. @@ -462,16 +415,15 @@ impl GuildId { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Events]: Permissions::CREATE_EVENTS pub async fn create_scheduled_event( self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateScheduledEvent<'_>, ) -> Result { - builder.execute(cache_http, self).await + builder.execute(http, self).await } /// Creates a new sticker in the guild with the data set, if any. @@ -480,17 +432,11 @@ impl GuildId { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS - #[inline] - pub async fn create_sticker( - self, - cache_http: impl CacheHttp, - builder: CreateSticker<'_>, - ) -> Result { - builder.execute(cache_http, self).await + pub async fn create_sticker(self, http: &Http, builder: CreateSticker<'_>) -> Result { + builder.execute(http, self).await } /// Deletes the current guild if the current account is the owner of the @@ -503,9 +449,8 @@ impl GuildId { /// # Errors /// /// Returns [`Error::Http`] if the current user is not the owner of the guild. - #[inline] - pub async fn delete(self, http: impl AsRef) -> Result<()> { - http.as_ref().delete_guild(self).await + pub async fn delete(self, http: &Http) -> Result<()> { + http.delete_guild(self).await } /// Deletes an [`Emoji`] from the guild. @@ -521,13 +466,13 @@ impl GuildId { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn delete_emoji( self, - http: impl AsRef, - emoji_id: impl Into, + http: &Http, + emoji_id: EmojiId, + reason: Option<&str>, ) -> Result<()> { - http.as_ref().delete_emoji(self, emoji_id.into(), None).await + http.delete_emoji(self, emoji_id, reason).await } /// Deletes an integration by Id from the guild. @@ -540,13 +485,13 @@ impl GuildId { /// that Id does not exist. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn delete_integration( self, - http: impl AsRef, - integration_id: impl Into, + http: &Http, + integration_id: IntegrationId, + reason: Option<&str>, ) -> Result<()> { - http.as_ref().delete_guild_integration(self, integration_id.into(), None).await + http.delete_guild_integration(self, integration_id, reason).await } /// Deletes a [`Role`] by Id from the guild. @@ -561,13 +506,13 @@ impl GuildId { /// does not exist. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] pub async fn delete_role( self, - http: impl AsRef, - role_id: impl Into, + http: &Http, + role_id: RoleId, + reason: Option<&str>, ) -> Result<()> { - http.as_ref().delete_role(self, role_id.into(), None).await + http.delete_role(self, role_id, reason).await } /// Deletes a specified scheduled event in the guild. @@ -581,13 +526,12 @@ impl GuildId { /// /// [Create Events]: Permissions::CREATE_EVENTS /// [Manage Events]: Permissions::MANAGE_EVENTS - #[inline] pub async fn delete_scheduled_event( self, - http: impl AsRef, - event_id: impl Into, + http: &Http, + event_id: ScheduledEventId, ) -> Result<()> { - http.as_ref().delete_scheduled_event(self, event_id.into()).await + http.delete_scheduled_event(self, event_id).await } /// Deletes a [`Sticker`] by id from the guild. @@ -603,13 +547,13 @@ impl GuildId { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn delete_sticker( self, - http: impl AsRef, - sticker_id: impl Into, + http: &Http, + sticker_id: StickerId, + reason: Option<&str>, ) -> Result<()> { - http.as_ref().delete_sticker(self, sticker_id.into(), None).await + http.delete_sticker(self, sticker_id, reason).await } /// Edits the current guild with new data where specified. @@ -618,23 +562,15 @@ impl GuildId { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn edit( - self, - cache_http: impl CacheHttp, - builder: EditGuild<'_>, - ) -> Result { - builder.execute(cache_http, self).await + pub async fn edit(self, http: &Http, builder: EditGuild<'_>) -> Result { + builder.execute(http, self).await } /// Edits an [`Emoji`]'s name in the guild. /// - /// Also see [`Emoji::edit`] if you have the `cache` and `methods` features enabled. - /// /// **Note**: If the emoji was created by the current user, requires either the [Create Guild /// Expressions] or the [Manage Guild Expressions] permission. Otherwise, the [Manage Guild /// Expressions] permission is required. @@ -646,18 +582,23 @@ impl GuildId { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn edit_emoji( self, - http: impl AsRef, - emoji_id: impl Into, + http: &Http, + emoji_id: EmojiId, name: &str, + reason: Option<&str>, ) -> Result { - let map = json!({ - "name": name, - }); + #[derive(serde::Serialize)] + struct EditEmoji<'a> { + name: &'a str, + } + + let map = EditEmoji { + name, + }; - http.as_ref().edit_emoji(self, emoji_id.into(), &map, None).await + http.edit_emoji(self, emoji_id, &map, reason).await } /// Edits the properties a guild member, such as muting or nicknaming them. Returns the new @@ -688,14 +629,13 @@ impl GuildId { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - #[inline] pub async fn edit_member( self, - cache_http: impl CacheHttp, - user_id: impl Into, + http: &Http, + user_id: UserId, builder: EditMember<'_>, ) -> Result { - builder.execute(cache_http, (self, user_id.into())).await + builder.execute(http, self, user_id).await } /// Edits the guild's MFA level. Returns the new level on success. @@ -707,14 +647,24 @@ impl GuildId { /// Returns [`Error::Http`] if the current user lacks permission. pub async fn edit_mfa_level( self, - http: impl AsRef, + http: &Http, mfa_level: MfaLevel, - audit_log_reason: Option<&str>, + reason: Option<&str>, ) -> Result { - let value = json!({ - "level": mfa_level, - }); - http.as_ref().edit_guild_mfa_level(self, &value, audit_log_reason).await + #[derive(serde::Serialize)] + struct EditMfaModel { + level: MfaLevel, + } + + if let Some(reason) = reason { + Maximum::AuditLogReason.check_overflow(reason.len())?; + } + + let map = EditMfaModel { + level: mfa_level, + }; + + http.edit_guild_mfa_level(self, &map, reason).await } /// Edits the current user's nickname for the guild. @@ -728,13 +678,22 @@ impl GuildId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Change Nickname]: Permissions::CHANGE_NICKNAME - #[inline] pub async fn edit_nickname( self, - http: impl AsRef, + http: &Http, new_nickname: Option<&str>, + reason: Option<&str>, ) -> Result<()> { - http.as_ref().edit_nickname(self, new_nickname, None).await + #[derive(serde::Serialize)] + struct EditNickname<'a> { + nick: Option<&'a str>, + } + + let map = EditNickname { + nick: new_nickname, + }; + + http.edit_nickname(self, &map, reason).await } /// Edits a [`Role`], optionally setting its new fields. @@ -765,18 +724,16 @@ impl GuildId { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] pub async fn edit_role( self, - cache_http: impl CacheHttp, - role_id: impl Into, + http: &Http, + role_id: RoleId, builder: EditRole<'_>, ) -> Result { - builder.execute(cache_http, (self, Some(role_id.into()))).await + builder.execute(http, self, Some(role_id)).await } /// Modifies a scheduled event in the guild with the data set, if any. @@ -786,18 +743,17 @@ impl GuildId { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Events]: Permissions::CREATE_EVENTS /// [Manage Events]: Permissions::MANAGE_EVENTS pub async fn edit_scheduled_event( self, - cache_http: impl CacheHttp, - event_id: impl Into, + http: &Http, + event_id: ScheduledEventId, builder: EditScheduledEvent<'_>, ) -> Result { - builder.execute(cache_http, (self, event_id.into())).await + builder.execute(http, self, event_id).await } /// Edits a sticker. @@ -829,14 +785,13 @@ impl GuildId { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn edit_sticker( self, - cache_http: impl CacheHttp, - sticker_id: impl Into, + http: &Http, + sticker_id: StickerId, builder: EditSticker<'_>, ) -> Result { - builder.execute(cache_http, (self, sticker_id.into())).await + builder.execute(http, self, sticker_id).await } /// Edit the position of a [`Role`] relative to all others in the [`Guild`]. @@ -855,14 +810,29 @@ impl GuildId { /// Returns an [`Error::Http`] if the current user lacks permission. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] pub async fn edit_role_position( self, - http: impl AsRef, - role_id: impl Into, - position: u16, + http: &Http, + role_id: RoleId, + position: i16, + reason: Option<&str>, ) -> Result> { - http.as_ref().edit_role_position(self, role_id.into(), position, None).await + #[derive(serde::Serialize)] + struct EditRole { + id: RoleId, + position: i16, + } + + if let Some(reason) = reason { + Maximum::AuditLogReason.check_overflow(reason.len())?; + } + + let map = EditRole { + id: role_id, + position, + }; + + http.edit_role_position(self, &map, reason).await } /// Edits the guild's welcome screen. @@ -876,10 +846,10 @@ impl GuildId { /// [Manage Guild]: Permissions::MANAGE_GUILD pub async fn edit_welcome_screen( self, - cache_http: impl CacheHttp, + http: &Http, builder: EditGuildWelcomeScreen<'_>, ) -> Result { - builder.execute(cache_http, self).await + builder.execute(http, self).await } /// Edits the guild's widget. @@ -893,10 +863,10 @@ impl GuildId { /// [Manage Guild]: Permissions::MANAGE_GUILD pub async fn edit_widget( self, - cache_http: impl CacheHttp, + http: &Http, builder: EditGuildWidget<'_>, ) -> Result { - builder.execute(cache_http, self).await + builder.execute(http, self).await } /// Gets all of the guild's roles over the REST API. @@ -905,14 +875,11 @@ impl GuildId { /// /// Returns [`Error::Http`] if the current user is not in /// the guild. - pub async fn roles(self, http: impl AsRef) -> Result> { - let roles = http.as_ref().get_guild_roles(self).await?; - - Ok(roles.into_iter().map(|r| (r.id, r)).collect()) + pub async fn roles(self, http: &Http) -> Result> { + http.get_guild_roles(self).await } /// Gets the default permission role (@everyone) from the guild. - #[inline] #[must_use] pub fn everyone_role(&self) -> RoleId { RoleId::from(self.get()) @@ -920,9 +887,8 @@ impl GuildId { /// Tries to find the [`Guild`] by its Id in the cache. #[cfg(feature = "cache")] - #[inline] - pub fn to_guild_cached(self, cache: &impl AsRef) -> Option> { - cache.as_ref().guild(self) + pub fn to_guild_cached(self, cache: &Cache) -> Option> { + cache.guild(self) } /// Requests [`PartialGuild`] over REST API. @@ -933,7 +899,6 @@ impl GuildId { /// # Errors /// /// Returns an [`Error::Http`] if the current user is not in the guild. - #[inline] pub async fn to_partial_guild(self, cache_http: impl CacheHttp) -> Result { #[cfg(feature = "cache")] { @@ -955,12 +920,8 @@ impl GuildId { /// # Errors /// /// Returns an [`Error::Http`] if the current user is not in the guild. - #[inline] - pub async fn to_partial_guild_with_counts( - self, - http: impl AsRef, - ) -> Result { - http.as_ref().get_guild_with_counts(self).await + pub async fn to_partial_guild_with_counts(self, http: &Http) -> Result { + http.get_guild_with_counts(self).await } /// Gets all [`Emoji`]s of this guild via HTTP. @@ -968,9 +929,8 @@ impl GuildId { /// # Errors /// /// Returns an [`Error::Http`] if the guild is unavailable. - #[inline] - pub async fn emojis(self, http: impl AsRef) -> Result> { - http.as_ref().get_emojis(self).await + pub async fn emojis(self, http: &Http) -> Result> { + http.get_emojis(self).await } /// Gets an [`Emoji`] of this guild by its ID via HTTP. @@ -978,9 +938,8 @@ impl GuildId { /// # Errors /// /// Returns an [`Error::Http`] if an emoji with that id does not exist. - #[inline] - pub async fn emoji(self, http: impl AsRef, emoji_id: EmojiId) -> Result { - http.as_ref().get_emoji(self, emoji_id).await + pub async fn emoji(self, http: &Http, emoji_id: EmojiId) -> Result { + http.get_emoji(self, emoji_id).await } /// Gets all [`Sticker`]s of this guild via HTTP. @@ -988,9 +947,8 @@ impl GuildId { /// # Errors /// /// Returns an [`Error::Http`] if the guild is unavailable. - #[inline] - pub async fn stickers(self, http: impl AsRef) -> Result> { - http.as_ref().get_guild_stickers(self).await + pub async fn stickers(self, http: &Http) -> Result> { + http.get_guild_stickers(self).await } /// Gets an [`Sticker`] of this guild by its ID via HTTP. @@ -998,9 +956,8 @@ impl GuildId { /// # Errors /// /// Returns an [`Error::Http`] if an sticker with that Id does not exist. - #[inline] - pub async fn sticker(self, http: impl AsRef, sticker_id: StickerId) -> Result { - http.as_ref().get_guild_sticker(self, sticker_id).await + pub async fn sticker(self, http: &Http, sticker_id: StickerId) -> Result { + http.get_guild_sticker(self, sticker_id).await } /// Gets all integration of the guild. @@ -1013,9 +970,8 @@ impl GuildId { /// [`Error::Json`] if there is an error in deserializing the API response. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn integrations(self, http: impl AsRef) -> Result> { - http.as_ref().get_guild_integrations(self).await + pub async fn integrations(self, http: &Http) -> Result> { + http.get_guild_integrations(self).await } /// Gets all of the guild's invites. @@ -1028,9 +984,8 @@ impl GuildId { /// [`Error::Json`] if there is an error in deserializing the API response. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn invites(self, http: impl AsRef) -> Result> { - http.as_ref().get_guild_invites(self).await + pub async fn invites(self, http: &Http) -> Result> { + http.get_guild_invites(self).await } /// Kicks a [`Member`] from the guild. @@ -1042,23 +997,12 @@ impl GuildId { /// Returns [`Error::Http`] if the member cannot be kicked by the current user. /// /// [Kick Members]: Permissions::KICK_MEMBERS - #[inline] - pub async fn kick(self, http: impl AsRef, user_id: impl Into) -> Result<()> { - http.as_ref().kick_member(self, user_id.into(), None).await - } + pub async fn kick(self, http: &Http, user_id: UserId, reason: Option<&str>) -> Result<()> { + if let Some(reason) = reason { + Maximum::AuditLogReason.check_overflow(reason.len())?; + } - /// # Errors - /// - /// In addition to the reasons [`Self::kick`] may return an error, may also return an error if - /// the reason is too long. - #[inline] - pub async fn kick_with_reason( - self, - http: impl AsRef, - user_id: impl Into, - reason: &str, - ) -> Result<()> { - http.as_ref().kick_member(self, user_id.into(), Some(reason)).await + http.kick_member(self, user_id, reason).await } /// Returns a guild [`Member`] object for the current user. @@ -1069,9 +1013,8 @@ impl GuildId { /// /// Returns an [`Error::Http`] if the current user is not in the guild or the access token /// lacks the necessary scope. - #[inline] - pub async fn current_user_member(self, http: impl AsRef) -> Result { - http.as_ref().get_current_user_guild_member(self).await + pub async fn current_user_member(self, http: &Http) -> Result { + http.get_current_user_guild_member(self).await } /// Leaves the guild. @@ -1080,9 +1023,8 @@ impl GuildId { /// /// May return an [`Error::Http`] if the current user cannot leave the guild, or currently is /// not in the guild. - #[inline] - pub async fn leave(self, http: impl AsRef) -> Result<()> { - http.as_ref().leave_guild(self).await + pub async fn leave(self, http: &Http) -> Result<()> { + http.leave_guild(self).await } /// Gets a user's [`Member`] for the guild by Id. @@ -1094,14 +1036,7 @@ impl GuildId { /// /// Returns an [`Error::Http`] if the user is not in the guild, or if the guild is otherwise /// unavailable - #[inline] - pub async fn member( - self, - cache_http: impl CacheHttp, - user_id: impl Into, - ) -> Result { - let user_id = user_id.into(); - + pub async fn member(self, cache_http: impl CacheHttp, user_id: UserId) -> Result { #[cfg(feature = "cache")] { if let Some(cache) = cache_http.cache() { @@ -1126,17 +1061,16 @@ impl GuildId { /// # Errors /// /// Returns an [`Error::Http`] if the API returns an error, may also return - /// [`Error::NotInRange`] if the input is not within range. + /// [`ModelError::TooSmall`] or [`ModelError::TooLarge`] if the limit is not within range. /// /// [`User`]: crate::model::user::User - #[inline] pub async fn members( self, - http: impl AsRef, - limit: Option, - after: impl Into>, + http: &Http, + limit: Option, + after: Option, ) -> Result> { - http.as_ref().get_guild_members(self, limit, after.into().map(UserId::get)).await + http.get_guild_members(self, limit, after).await } /// Streams over all the members in a guild. @@ -1164,8 +1098,8 @@ impl GuildId { /// } /// # } /// ``` - pub fn members_iter>(self, http: H) -> impl Stream> { - MembersIter::::stream(http, self) + pub fn members_iter(self, http: &Http) -> impl Stream> + '_ { + MembersIter::stream(http, self) } /// Moves a member to a specific voice channel. @@ -1178,22 +1112,21 @@ impl GuildId { /// currently in a voice channel for this [`Guild`]. /// /// [Move Members]: Permissions::MOVE_MEMBERS - #[inline] pub async fn move_member( self, - cache_http: impl CacheHttp, - user_id: impl Into, - channel_id: impl Into, + http: &Http, + user_id: UserId, + channel_id: ChannelId, ) -> Result { - let builder = EditMember::new().voice_channel(channel_id.into()); - self.edit_member(cache_http, user_id, builder).await + let builder = EditMember::new().voice_channel(channel_id); + self.edit_member(http, user_id, builder).await } /// Returns the name of whatever guild this id holds. #[cfg(feature = "cache")] #[must_use] - pub fn name(self, cache: impl AsRef) -> Option { - self.to_guild_cached(cache.as_ref()).map(|g| g.name.clone()) + pub fn name(self, cache: &Cache) -> Option { + self.to_guild_cached(cache).map(|g| g.name.to_string()) } /// Disconnects a member from a voice channel in the guild. @@ -1206,13 +1139,8 @@ impl GuildId { /// currently in a voice channel for this [`Guild`]. /// /// [Move Members]: Permissions::MOVE_MEMBERS - #[inline] - pub async fn disconnect_member( - self, - cache_http: impl CacheHttp, - user_id: impl Into, - ) -> Result { - self.edit_member(cache_http, user_id, EditMember::new().disconnect_member()).await + pub async fn disconnect_member(self, http: &Http, user_id: UserId) -> Result { + self.edit_member(http, user_id, EditMember::new().disconnect_member()).await } /// Gets the number of [`Member`]s that would be pruned with the given number of days. @@ -1224,9 +1152,8 @@ impl GuildId { /// Returns [`Error::Http`] if the current user does not have permission. /// /// [Kick Members]: Permissions::KICK_MEMBERS - #[inline] - pub async fn prune_count(self, http: impl AsRef, days: u8) -> Result { - http.as_ref().get_guild_prune_count(self, days).await + pub async fn prune_count(self, http: &Http, days: u8) -> Result { + http.get_guild_prune_count(self, days).await } /// Re-orders the channels of the guild. @@ -1243,24 +1170,23 @@ impl GuildId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - #[inline] pub async fn reorder_channels( self, - http: impl AsRef, + http: &Http, channels: impl IntoIterator, ) -> Result<()> { - let items = channels - .into_iter() - .map(|(id, pos)| { - json!({ - "id": id, - "position": pos, - }) - }) - .collect::>() - .into(); + #[derive(serde::Serialize)] + struct ChannelPosEdit { + id: ChannelId, + position: u64, + } + + let iter = channels.into_iter().map(|(id, position)| ChannelPosEdit { + id, + position, + }); - http.as_ref().edit_guild_channel_positions(self, &items).await + http.edit_guild_channel_positions(self, &SerializeIter::new(iter)).await } /// Returns a list of [`Member`]s in a [`Guild`] whose username or nickname starts with a @@ -1272,14 +1198,13 @@ impl GuildId { /// # Errors /// /// Returns an [`Error::Http`] if the API returns an error. - #[inline] pub async fn search_members( self, - http: impl AsRef, + http: &Http, query: &str, - limit: Option, + limit: Option, ) -> Result> { - http.as_ref().search_guild_members(self, query, limit).await + http.search_guild_members(self, query, limit).await } /// Fetches a specified scheduled event in the guild, by Id. If `with_user_count` is set to @@ -1296,11 +1221,11 @@ impl GuildId { /// [View Channel]: Permissions::VIEW_CHANNEL pub async fn scheduled_event( self, - http: impl AsRef, - event_id: impl Into, + http: &Http, + event_id: ScheduledEventId, with_user_count: bool, ) -> Result { - http.as_ref().get_scheduled_event(self, event_id.into(), with_user_count).await + http.get_scheduled_event(self, event_id, with_user_count).await } /// Fetches a list of all scheduled events in the guild. If `with_user_count` is set to `true`, @@ -1315,10 +1240,10 @@ impl GuildId { /// [View Channel]: Permissions::VIEW_CHANNEL pub async fn scheduled_events( self, - http: impl AsRef, + http: &Http, with_user_count: bool, ) -> Result> { - http.as_ref().get_scheduled_events(self, with_user_count).await + http.get_scheduled_events(self, with_user_count).await } /// Fetches a list of interested users for the specified event. @@ -1335,11 +1260,11 @@ impl GuildId { /// [View Channel]: Permissions::VIEW_CHANNEL pub async fn scheduled_event_users( self, - http: impl AsRef, - event_id: impl Into, - limit: Option, + http: &Http, + event_id: ScheduledEventId, + limit: Option, ) -> Result> { - http.as_ref().get_scheduled_event_users(self, event_id.into(), limit, None, None).await + http.get_scheduled_event_users(self, event_id, limit, None, None).await } /// Fetches a list of interested users for the specified event, with additional options and @@ -1355,55 +1280,24 @@ impl GuildId { /// [View Channel]: Permissions::VIEW_CHANNEL pub async fn scheduled_event_users_optioned( self, - http: impl AsRef, - event_id: impl Into, - limit: Option, + http: &Http, + event_id: ScheduledEventId, + limit: Option, target: Option, with_member: Option, ) -> Result> { - http.as_ref() - .get_scheduled_event_users(self, event_id.into(), limit, target, with_member) - .await + http.get_scheduled_event_users(self, event_id, limit, target, with_member).await } /// Returns the Id of the shard associated with the guild. /// - /// When the cache is enabled this will automatically retrieve the total number of shards. - /// - /// **Note**: When the cache is enabled, this function unlocks the cache to retrieve the total - /// number of shards in use. If you already have the total, consider using [`utils::shard_id`]. + /// This is just a shortcut for [`utils::shard_id`], the shard count should + /// be fetched using [`Cache::shard_count`] if possible. /// /// [`utils::shard_id`]: crate::utils::shard_id - #[cfg(all(feature = "cache", feature = "utils"))] - #[inline] - #[must_use] - pub fn shard_id(self, cache: impl AsRef) -> u32 { - crate::utils::shard_id(self, cache.as_ref().shard_count()) - } - - /// Returns the Id of the shard associated with the guild. - /// - /// When the cache is enabled this will automatically retrieve the total number of shards. - /// - /// When the cache is not enabled, the total number of shards being used will need to be - /// passed. - /// - /// # Examples - /// - /// Retrieve the Id of the shard for a guild with Id `81384788765712384`, using 17 shards: - /// - /// ```rust - /// use serenity::model::id::GuildId; - /// use serenity::utils; - /// - /// let guild_id = GuildId::new(81384788765712384); - /// - /// assert_eq!(guild_id.shard_id(17), 7); - /// ``` - #[cfg(all(feature = "utils", not(feature = "cache")))] - #[inline] #[must_use] - pub fn shard_id(self, shard_count: u32) -> u32 { + #[cfg(feature = "utils")] + pub fn shard_id(self, shard_count: std::num::NonZeroU16) -> u16 { crate::utils::shard_id(self, shard_count) } @@ -1417,13 +1311,12 @@ impl GuildId { /// that Id does not exist. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn start_integration_sync( self, - http: impl AsRef, - integration_id: impl Into, + http: &Http, + integration_id: IntegrationId, ) -> Result<()> { - http.as_ref().start_integration_sync(self, integration_id.into()).await + http.start_integration_sync(self, integration_id).await } /// Starts a prune of [`Member`]s. @@ -1438,9 +1331,13 @@ impl GuildId { /// /// [Kick Members]: Permissions::KICK_MEMBERS /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn start_prune(self, http: impl AsRef, days: u8) -> Result { - http.as_ref().start_guild_prune(self, days, None).await + pub async fn start_prune( + self, + http: &Http, + days: u8, + reason: Option<&str>, + ) -> Result { + http.start_guild_prune(self, days, reason).await } /// Unbans a [`User`] from the guild. @@ -1452,9 +1349,8 @@ impl GuildId { /// Returns [`Error::Http`] if the current user does not have permission. /// /// [Ban Members]: Permissions::BAN_MEMBERS - #[inline] - pub async fn unban(self, http: impl AsRef, user_id: impl Into) -> Result<()> { - http.as_ref().remove_ban(self, user_id.into(), None).await + pub async fn unban(self, http: &Http, user_id: UserId, reason: Option<&str>) -> Result<()> { + http.remove_ban(self, user_id, reason).await } /// Retrieve's the guild's vanity URL. @@ -1467,9 +1363,8 @@ impl GuildId { /// [`Error::Json`] if there is an error deserializing the API response. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn vanity_url(self, http: impl AsRef) -> Result { - http.as_ref().get_guild_vanity_url(self).await + pub async fn vanity_url(self, http: &Http) -> Result { + http.get_guild_vanity_url(self).await } /// Retrieves the guild's webhooks. @@ -1482,36 +1377,32 @@ impl GuildId { /// /// Will return an [`Error::Http`] if the bot is lacking permissions. Can also return an /// [`Error::Json`] if there is an error deserializing the API response. - #[inline] - pub async fn webhooks(self, http: impl AsRef) -> Result> { - http.as_ref().get_guild_webhooks(self).await + pub async fn webhooks(self, http: &Http) -> Result> { + http.get_guild_webhooks(self).await } /// Returns a builder which can be awaited to obtain a message or stream of messages in this /// guild. #[cfg(feature = "collector")] - pub fn await_reply(self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_reply(self, shard_messenger: ShardMessenger) -> MessageCollector { MessageCollector::new(shard_messenger).guild_id(self) } /// Same as [`Self::await_reply`]. #[cfg(feature = "collector")] - pub fn await_replies(&self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_replies(&self, shard_messenger: ShardMessenger) -> MessageCollector { self.await_reply(shard_messenger) } /// Returns a builder which can be awaited to obtain a message or stream of reactions sent in /// this guild. #[cfg(feature = "collector")] - pub fn await_reaction(self, shard_messenger: impl AsRef) -> ReactionCollector { + pub fn await_reaction(self, shard_messenger: ShardMessenger) -> ReactionCollector { ReactionCollector::new(shard_messenger).guild_id(self) } /// Same as [`Self::await_reaction`]. #[cfg(feature = "collector")] - pub fn await_reactions( - &self, - shard_messenger: impl AsRef, - ) -> ReactionCollector { + pub fn await_reactions(&self, shard_messenger: ShardMessenger) -> ReactionCollector { self.await_reaction(shard_messenger) } @@ -1522,12 +1413,8 @@ impl GuildId { /// # Errors /// /// See [`CreateCommand::execute`] for a list of possible errors. - pub async fn create_command( - self, - cache_http: impl CacheHttp, - builder: CreateCommand, - ) -> Result { - builder.execute(cache_http, (Some(self), None)).await + pub async fn create_command(self, http: &Http, builder: CreateCommand<'_>) -> Result { + builder.execute(http, Some(self), None).await } /// Override all guild application commands. @@ -1537,10 +1424,10 @@ impl GuildId { /// Returns the same errors as [`Self::create_command`]. pub async fn set_commands( self, - http: impl AsRef, - commands: Vec, + http: &Http, + commands: &[CreateCommand<'_>], ) -> Result> { - http.as_ref().create_guild_commands(self, &commands).await + http.create_guild_commands(self, &commands).await } /// Overwrites permissions for a specific command. @@ -1552,11 +1439,11 @@ impl GuildId { /// See [`EditCommandPermissions::execute`] for a list of possible errors. pub async fn edit_command_permissions( self, - cache_http: impl CacheHttp, + http: &Http, command_id: CommandId, - builder: EditCommandPermissions, + builder: EditCommandPermissions<'_>, ) -> Result { - builder.execute(cache_http, (self, command_id)).await + builder.execute(http, self, command_id).await } /// Get all guild application commands. @@ -1564,8 +1451,8 @@ impl GuildId { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_commands(self, http: impl AsRef) -> Result> { - http.as_ref().get_guild_commands(self).await + pub async fn get_commands(self, http: &Http) -> Result> { + http.get_guild_commands(self).await } /// Get all guild application commands with localizations. @@ -1573,11 +1460,8 @@ impl GuildId { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_commands_with_localizations( - self, - http: impl AsRef, - ) -> Result> { - http.as_ref().get_guild_commands_with_localizations(self).await + pub async fn get_commands_with_localizations(self, http: &Http) -> Result> { + http.get_guild_commands_with_localizations(self).await } /// Get a specific guild application command by its Id. @@ -1585,12 +1469,8 @@ impl GuildId { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_command( - self, - http: impl AsRef, - command_id: CommandId, - ) -> Result { - http.as_ref().get_guild_command(self, command_id).await + pub async fn get_command(self, http: &Http, command_id: CommandId) -> Result { + http.get_guild_command(self, command_id).await } /// Edit a guild application command, given its Id. @@ -1600,11 +1480,11 @@ impl GuildId { /// See [`CreateCommand::execute`] for a list of possible errors. pub async fn edit_command( self, - cache_http: impl CacheHttp, + http: &Http, command_id: CommandId, - builder: CreateCommand, + builder: CreateCommand<'_>, ) -> Result { - builder.execute(cache_http, (Some(self), Some(command_id))).await + builder.execute(http, Some(self), Some(command_id)).await } /// Delete guild application command by its Id. @@ -1612,8 +1492,8 @@ impl GuildId { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn delete_command(self, http: impl AsRef, command_id: CommandId) -> Result<()> { - http.as_ref().delete_guild_command(self, command_id).await + pub async fn delete_command(self, http: &Http, command_id: CommandId) -> Result<()> { + http.delete_guild_command(self, command_id).await } /// Get all guild application commands permissions only. @@ -1621,11 +1501,8 @@ impl GuildId { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_commands_permissions( - self, - http: impl AsRef, - ) -> Result> { - http.as_ref().get_guild_commands_permissions(self).await + pub async fn get_commands_permissions(self, http: &Http) -> Result> { + http.get_guild_commands_permissions(self).await } /// Get permissions for specific guild application command by its Id. @@ -1635,10 +1512,10 @@ impl GuildId { /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. pub async fn get_command_permissions( self, - http: impl AsRef, + http: &Http, command_id: CommandId, ) -> Result { - http.as_ref().get_guild_command_permissions(self, command_id).await + http.get_guild_command_permissions(self, command_id).await } /// Get the guild welcome screen. @@ -1646,8 +1523,8 @@ impl GuildId { /// # Errors /// /// Returns [`Error::Http`] if the guild does not have a welcome screen. - pub async fn get_welcome_screen(self, http: impl AsRef) -> Result { - http.as_ref().get_guild_welcome_screen(self).await + pub async fn get_welcome_screen(self, http: &Http) -> Result { + http.get_guild_welcome_screen(self).await } /// Get the guild preview. @@ -1658,8 +1535,8 @@ impl GuildId { /// # Errors /// /// Returns [`Error::Http`] if the bot cannot see the guild preview, see the note. - pub async fn get_preview(self, http: impl AsRef) -> Result { - http.as_ref().get_guild_preview(self).await + pub async fn get_preview(self, http: &Http) -> Result { + http.get_guild_preview(self).await } /// Get the guild widget. @@ -1667,8 +1544,8 @@ impl GuildId { /// # Errors /// /// Returns [`Error::Http`] if the bot does not have `MANAGE_MESSAGES` permission. - pub async fn get_widget(self, http: impl AsRef) -> Result { - http.as_ref().get_guild_widget(self).await + pub async fn get_widget(self, http: &Http) -> Result { + http.get_guild_widget(self).await } /// Get the widget image URL. @@ -1683,8 +1560,8 @@ impl GuildId { /// /// Returns [`Error::Http`] if there is an error in the deserialization, or if the bot issuing /// the request is not in the guild. - pub async fn get_active_threads(self, http: impl AsRef) -> Result { - http.as_ref().get_guild_active_threads(self).await + pub async fn get_active_threads(self, http: &Http) -> Result { + http.get_guild_active_threads(self).await } } @@ -1761,18 +1638,18 @@ impl<'a> From<&'a WebhookGuild> for GuildId { /// A helper class returned by [`GuildId::members_iter`] #[derive(Clone, Debug)] #[cfg(feature = "model")] -pub struct MembersIter> { +pub struct MembersIter<'a> { guild_id: GuildId, - http: H, + http: &'a Http, buffer: Vec, after: Option, tried_fetch: bool, } #[cfg(feature = "model")] -impl> MembersIter { - fn new(guild_id: GuildId, http: H) -> MembersIter { - MembersIter { +impl<'a> MembersIter<'a> { + fn new(guild_id: GuildId, http: &'a Http) -> Self { + Self { guild_id, http, buffer: Vec::new(), @@ -1788,13 +1665,13 @@ impl> MembersIter { /// not return duplicate items. If there are no more members to be fetched, then this marks /// `self.after` as None, indicating that no more calls ought to be made. async fn refresh(&mut self) -> Result<()> { - // Number of profiles to fetch - let grab_size: u64 = 1000; + let grab_size = crate::constants::MEMBER_FETCH_LIMIT; - self.buffer = self.guild_id.members(&self.http, Some(grab_size), self.after).await?; + // Number of profiles to fetch + self.buffer = self.guild_id.members(self.http, Some(grab_size), self.after).await?; // Get the last member. If shorter than 1000, there are no more results anyway - self.after = self.buffer.get(grab_size as usize - 1).map(|member| member.user.id); + self.after = self.buffer.get(grab_size.get() as usize - 1).map(|member| member.user.id); // Reverse to optimize pop() self.buffer.reverse(); @@ -1817,11 +1694,11 @@ impl> MembersIter { /// # /// # async fn run() { /// # let guild_id = GuildId::new(1); - /// # let ctx: Http = unimplemented!(); + /// # let http: Http = unimplemented!(); /// use serenity::futures::StreamExt; /// use serenity::model::guild::MembersIter; /// - /// let mut members = MembersIter::::stream(&ctx, guild_id).boxed(); + /// let mut members = MembersIter::stream(&http, guild_id).boxed(); /// while let Some(member_result) = members.next().await { /// match member_result { /// Ok(member) => println!("{} is {}", member, member.display_name(),), @@ -1830,7 +1707,7 @@ impl> MembersIter { /// } /// # } /// ``` - pub fn stream(http: impl AsRef, guild_id: GuildId) -> impl Stream> { + pub fn stream(http: &Http, guild_id: GuildId) -> impl Stream> + '_ { let init_state = MembersIter::new(guild_id, http); futures::stream::unfold(init_state, |mut state| async { diff --git a/src/model/guild/guild_preview.rs b/src/model/guild/guild_preview.rs index 75aedcbb736..5514311848e 100644 --- a/src/model/guild/guild_preview.rs +++ b/src/model/guild/guild_preview.rs @@ -1,3 +1,4 @@ +use crate::internal::prelude::*; use crate::model::guild::Emoji; use crate::model::id::GuildId; use crate::model::misc::ImageHash; @@ -14,7 +15,7 @@ pub struct GuildPreview { /// The guild Id. pub id: GuildId, /// The guild name. - pub name: String, + pub name: FixedString, /// The guild icon hash if it has one. pub icon: Option, /// The guild splash hash if it has one. @@ -22,17 +23,17 @@ pub struct GuildPreview { /// The guild discovery splash hash it it has one. pub discovery_splash: Option, /// The custom guild emojis. - pub emojis: Vec, + pub emojis: FixedArray, /// The guild features. See [`Guild::features`] /// /// [`Guild::features`]: super::Guild::features - pub features: Vec, + pub features: FixedArray, /// Approximate number of members in this guild. pub approximate_member_count: u64, /// Approximate number of online members in this guild. pub approximate_presence_count: u64, /// The description for the guild, if the guild has the `DISCOVERABLE` feature. - pub description: Option, + pub description: Option, /// Custom guild stickers. - pub stickers: Vec, + pub stickers: FixedArray, } diff --git a/src/model/guild/integration.rs b/src/model/guild/integration.rs index 2c1c6e29540..af4025ffa62 100644 --- a/src/model/guild/integration.rs +++ b/src/model/guild/integration.rs @@ -1,3 +1,5 @@ +use nonmax::{NonMaxU32, NonMaxU64}; + use crate::model::prelude::*; /// Various information about integrations. @@ -5,25 +7,26 @@ use crate::model::prelude::*; /// [Discord docs](https://discord.com/developers/docs/resources/guild#integration-object), /// [extra fields 1](https://discord.com/developers/docs/topics/gateway-events#integration-create), /// [extra fields 2](https://discord.com/developers/docs/topics/gateway-events#integration-update), +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct Integration { pub id: IntegrationId, - pub name: String, + pub name: FixedString, #[serde(rename = "type")] - pub kind: String, + pub kind: FixedString, pub enabled: bool, pub syncing: Option, pub role_id: Option, pub enable_emoticons: Option, #[serde(rename = "expire_behavior")] pub expire_behaviour: Option, - pub expire_grace_period: Option, + pub expire_grace_period: Option, pub user: Option, pub account: IntegrationAccount, pub synced_at: Option, - pub subscriber_count: Option, + pub subscriber_count: Option, pub revoked: Option, pub application: Option, pub scopes: Option>, @@ -37,7 +40,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/guild#integration-object-integration-expire-behaviors). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum IntegrationExpireBehaviour { RemoveRole = 0, @@ -60,8 +62,8 @@ impl From for IntegrationId { #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct IntegrationAccount { - pub id: String, - pub name: String, + pub id: FixedString, + pub name: FixedString, } /// Integration application object. @@ -72,8 +74,8 @@ pub struct IntegrationAccount { #[non_exhaustive] pub struct IntegrationApplication { pub id: ApplicationId, - pub name: String, + pub name: FixedString, pub icon: Option, - pub description: String, + pub description: FixedString, pub bot: Option, } diff --git a/src/model/guild/member.rs b/src/model/guild/member.rs index 44d4a5cf41c..19e48235e9b 100644 --- a/src/model/guild/member.rs +++ b/src/model/guild/member.rs @@ -7,8 +7,7 @@ use crate::builder::EditMember; #[cfg(feature = "cache")] use crate::cache::Cache; #[cfg(feature = "model")] -use crate::http::{CacheHttp, Http}; -#[cfg(all(feature = "cache", feature = "model"))] +use crate::http::Http; use crate::internal::prelude::*; use crate::model::prelude::*; #[cfg(feature = "model")] @@ -18,8 +17,9 @@ use crate::model::utils::avatar_url; /// /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-member-object), /// [extra fields](https://discord.com/developers/docs/topics/gateway-events#guild-member-add-guild-member-add-extra-fields). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct Member { /// Attached User struct. @@ -27,11 +27,11 @@ pub struct Member { /// The member's nickname, if present. /// /// Can't be longer than 32 characters. - pub nick: Option, + pub nick: Option>, /// The guild avatar hash pub avatar: Option, /// Vector of Ids of [`Role`]s given to the member. - pub roles: Vec, + pub roles: FixedArray, /// Timestamp representing the date when the member joined. pub joined_at: Option, /// Timestamp representing the date since the member is boosting the guild. @@ -96,9 +96,8 @@ impl Member { /// Id does not exist. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] - pub async fn add_role(&self, http: impl AsRef, role_id: impl Into) -> Result<()> { - http.as_ref().add_member_role(self.guild_id, self.user.id, role_id.into(), None).await + pub async fn add_role(&self, http: &Http, role_id: RoleId, reason: Option<&str>) -> Result<()> { + http.add_member_role(self.guild_id, self.user.id, role_id, reason).await } /// Adds one or multiple [`Role`]s to the member. @@ -111,9 +110,14 @@ impl Member { /// does not exist. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - pub async fn add_roles(&self, http: impl AsRef, role_ids: &[RoleId]) -> Result<()> { - for role_id in role_ids { - self.add_role(http.as_ref(), role_id).await?; + pub async fn add_roles( + &self, + http: &Http, + role_ids: &[RoleId], + reason: Option<&str>, + ) -> Result<()> { + for &role_id in role_ids { + self.add_role(http, role_id, reason).await?; } Ok(()) @@ -126,36 +130,18 @@ impl Member { /// /// # Errors /// - /// Returns a [`ModelError::DeleteMessageDaysAmount`] if the `dmd` is greater than 7. Can also + /// Returns a [`ModelError::TooLarge`] if the `dmd` is greater than 7. Can also /// return [`Error::Http`] if the current user lacks permission to ban this member. /// /// [Ban Members]: Permissions::BAN_MEMBERS - #[inline] - pub async fn ban(&self, http: impl AsRef, dmd: u8) -> Result<()> { - self.ban_with_reason(http, dmd, "").await - } - - /// Ban the member from the guild with a reason. Refer to [`Self::ban`] to further - /// documentation. - /// - /// # Errors - /// - /// In addition to the errors [`Self::ban`] may return, can also return - /// [`Error::ExceededLimit`] if the length of the reason is greater than 512. - #[inline] - pub async fn ban_with_reason( - &self, - http: impl AsRef, - dmd: u8, - reason: impl AsRef, - ) -> Result<()> { - self.guild_id.ban_with_reason(http, self.user.id, dmd, reason).await + pub async fn ban(&self, http: &Http, dmd: u8, audit_log_reason: Option<&str>) -> Result<()> { + self.guild_id.ban(http, self.user.id, dmd, audit_log_reason).await } /// Determines the member's colour. #[cfg(feature = "cache")] - pub fn colour(&self, cache: impl AsRef) -> Option { - let guild = cache.as_ref().guild(self.guild_id)?; + pub fn colour(&self, cache: &Cache) -> Option { + let guild = cache.guild(self.guild_id)?; let mut roles = self .roles @@ -173,12 +159,12 @@ impl Member { /// Returns the "default channel" of the guild for the member. (This returns the first channel /// that can be read by the member, if there isn't one returns [`None`]) #[cfg(feature = "cache")] - pub fn default_channel(&self, cache: impl AsRef) -> Option { - let guild = self.guild_id.to_guild_cached(&cache)?; + pub fn default_channel(&self, cache: &Cache) -> Option { + let guild = self.guild_id.to_guild_cached(cache)?; let member = guild.members.get(&self.user.id)?; - for channel in guild.channels.values() { + for channel in &guild.channels { if channel.kind != ChannelType::Category && guild.user_permissions_in(channel, member).view_channel() { @@ -202,13 +188,13 @@ impl Member { /// /// [Moderate Members]: Permissions::MODERATE_MEMBERS #[doc(alias = "timeout")] - pub async fn disable_communication_until_datetime( + pub async fn disable_communication_until( &mut self, - cache_http: impl CacheHttp, + http: &Http, time: Timestamp, ) -> Result<()> { - let builder = EditMember::new().disable_communication_until_datetime(time); - match self.guild_id.edit_member(cache_http, self.user.id, builder).await { + let builder = EditMember::new().disable_communication_until(time); + match self.guild_id.edit_member(http, self.user.id, builder).await { Ok(_) => { self.communication_disabled_until = Some(time); Ok(()) @@ -220,14 +206,12 @@ impl Member { /// Calculates the member's display name. /// /// The nickname takes priority over the member's username if it exists. - #[inline] #[must_use] pub fn display_name(&self) -> &str { self.nick.as_ref().or(self.user.global_name.as_ref()).unwrap_or(&self.user.name) } /// Returns the DiscordTag of a Member, taking possible nickname into account. - #[inline] #[must_use] pub fn distinct(&self) -> String { if let Some(discriminator) = self.user.discriminator { @@ -249,12 +233,8 @@ impl Member { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks necessary permissions. - pub async fn edit( - &mut self, - cache_http: impl CacheHttp, - builder: EditMember<'_>, - ) -> Result<()> { - *self = self.guild_id.edit_member(cache_http, self.user.id, builder).await?; + pub async fn edit(&mut self, http: &Http, builder: EditMember<'_>) -> Result<()> { + *self = self.guild_id.edit_member(http, self.user.id, builder).await?; Ok(()) } @@ -268,31 +248,12 @@ impl Member { /// /// [Moderate Members]: Permissions::MODERATE_MEMBERS #[doc(alias = "timeout")] - pub async fn enable_communication(&mut self, cache_http: impl CacheHttp) -> Result<()> { + pub async fn enable_communication(&mut self, http: &Http) -> Result<()> { let builder = EditMember::new().enable_communication(); - *self = self.guild_id.edit_member(cache_http, self.user.id, builder).await?; + *self = self.guild_id.edit_member(http, self.user.id, builder).await?; Ok(()) } - /// Retrieves the ID and position of the member's highest role in the hierarchy, if they have - /// one. - /// - /// This _may_ return [`None`] if the user has roles, but they are not present in the cache for - /// cache inconsistency reasons. - /// - /// The "highest role in hierarchy" is defined as the role with the highest position. If two or - /// more roles have the same highest position, then the role with the lowest ID is the highest. - #[cfg(feature = "cache")] - #[deprecated = "Use Guild::member_highest_role"] - pub fn highest_role_info(&self, cache: impl AsRef) -> Option<(RoleId, u16)> { - cache - .as_ref() - .guild(self.guild_id) - .as_ref() - .and_then(|g| g.member_highest_role(self)) - .map(|r| (r.id, r.position)) - } - /// Kick the member from the guild. /// /// **Note**: Requires the [Kick Members] permission. @@ -303,14 +264,11 @@ impl Member { /// /// ```rust,ignore /// // assuming a `member` has already been bound - /// match member.kick().await { + /// match member.kick(None).await { /// Ok(()) => println!("Successfully kicked member"), /// Err(Error::Model(ModelError::GuildNotFound)) => { /// println!("Couldn't determine guild of member"); /// }, - /// Err(Error::Model(ModelError::InvalidPermissions(missing_perms))) => { - /// println!("Didn't have permissions; missing: {:?}", missing_perms); - /// }, /// _ => {}, /// } /// ``` @@ -320,58 +278,11 @@ impl Member { /// Returns a [`ModelError::GuildNotFound`] if the Id of the member's guild could not be /// determined. /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to perform the kick. - /// - /// Otherwise will return [`Error::Http`] if the current user lacks permission. - /// - /// [Kick Members]: Permissions::KICK_MEMBERS - #[inline] - pub async fn kick(&self, cache_http: impl CacheHttp) -> Result<()> { - self.kick_with_reason(cache_http, "").await - } - - /// Kicks the member from the guild, with a reason. - /// - /// **Note**: Requires the [Kick Members] permission. - /// - /// # Examples - /// - /// Kicks a member from it's guild, with an optional reason: - /// - /// ```rust,ignore - /// match member.kick(&ctx.http, "A Reason").await { - /// Ok(()) => println!("Successfully kicked member"), - /// Err(Error::Model(ModelError::GuildNotFound)) => { - /// println!("Couldn't determine guild of member"); - /// }, - /// Err(Error::Model(ModelError::InvalidPermissions(missing_perms))) => { - /// println!("Didn't have permissions; missing: {:?}", missing_perms); - /// }, - /// _ => {}, - /// } - /// ``` - /// - /// # Errors - /// - /// In addition to the reasons [`Self::kick`] may return an error, can also return an error if - /// the given reason is too long. + /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Kick Members]: Permissions::KICK_MEMBERS - pub async fn kick_with_reason(&self, cache_http: impl CacheHttp, reason: &str) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - let lookup = cache.guild(self.guild_id).as_deref().cloned(); - if let Some(guild) = lookup { - guild.require_perms(cache, Permissions::KICK_MEMBERS)?; - - guild.check_hierarchy(cache, self.user.id)?; - } - } - } - - self.guild_id.kick_with_reason(cache_http.http(), self.user.id, reason).await + pub async fn kick(&self, http: &Http, reason: Option<&str>) -> Result<()> { + self.guild_id.kick(http, self.user.id, reason).await } /// Moves the member to a voice channel. @@ -384,12 +295,8 @@ impl Member { /// current user lacks permission. /// /// [Move Members]: Permissions::MOVE_MEMBERS - pub async fn move_to_voice_channel( - &self, - cache_http: impl CacheHttp, - channel: impl Into, - ) -> Result { - self.guild_id.move_member(cache_http, self.user.id, channel).await + pub async fn move_to_voice_channel(&self, http: &Http, channel: ChannelId) -> Result { + self.guild_id.move_member(http, self.user.id, channel).await } /// Disconnects the member from their voice channel if any. @@ -402,8 +309,8 @@ impl Member { /// current user lacks permission. /// /// [Move Members]: Permissions::MOVE_MEMBERS - pub async fn disconnect_from_voice(&self, cache_http: impl CacheHttp) -> Result { - self.guild_id.disconnect_member(cache_http, self.user.id).await + pub async fn disconnect_from_voice(&self, http: &Http) -> Result { + self.guild_id.disconnect_member(http, self.user.id).await } /// Returns the guild-level permissions for the member. @@ -424,8 +331,8 @@ impl Member { /// And/or returns [`ModelError::ItemMissing`] if the "default channel" of the guild is not /// found. #[cfg(feature = "cache")] - pub fn permissions(&self, cache: impl AsRef) -> Result { - let guild = cache.as_ref().guild(self.guild_id).ok_or(ModelError::GuildNotFound)?; + pub fn permissions(&self, cache: &Cache) -> Result { + let guild = cache.guild(self.guild_id).ok_or(ModelError::GuildNotFound)?; Ok(guild.member_permissions(self)) } @@ -441,10 +348,11 @@ impl Member { /// [Manage Roles]: Permissions::MANAGE_ROLES pub async fn remove_role( &self, - http: impl AsRef, - role_id: impl Into, + http: &Http, + role_id: RoleId, + reason: Option<&str>, ) -> Result<()> { - http.as_ref().remove_member_role(self.guild_id, self.user.id, role_id.into(), None).await + http.remove_member_role(self.guild_id, self.user.id, role_id, reason).await } /// Removes one or multiple [`Role`]s from the member. @@ -457,9 +365,14 @@ impl Member { /// lacks permission. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - pub async fn remove_roles(&self, http: impl AsRef, role_ids: &[RoleId]) -> Result<()> { - for role_id in role_ids { - self.remove_role(http.as_ref(), role_id).await?; + pub async fn remove_roles( + &self, + http: &Http, + role_ids: &[RoleId], + reason: Option<&str>, + ) -> Result<()> { + for &role_id in role_ids { + self.remove_role(http, role_id, reason).await?; } Ok(()) @@ -471,15 +384,14 @@ impl Member { /// /// If role data can not be found for the member, then [`None`] is returned. #[cfg(feature = "cache")] - pub fn roles(&self, cache: impl AsRef) -> Option> { + pub fn roles(&self, cache: &Cache) -> Option> { Some( cache - .as_ref() .guild(self.guild_id)? .roles .iter() - .filter(|(id, _)| self.roles.contains(id)) - .map(|(_, role)| role.clone()) + .filter(|r| self.roles.contains(&r.id)) + .cloned() .collect(), ) } @@ -490,19 +402,16 @@ impl Member { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to perform bans. + /// Returns [`Error::Http`] if the current user does not have permission to perform bans. /// /// [Ban Members]: Permissions::BAN_MEMBERS - #[inline] - pub async fn unban(&self, http: impl AsRef) -> Result<()> { - http.as_ref().remove_ban(self.guild_id, self.user.id, None).await + pub async fn unban(&self, http: &Http, reason: Option<&str>) -> Result<()> { + http.remove_ban(self.guild_id, self.user.id, reason).await } /// Returns the formatted URL of the member's per guild avatar, if one exists. /// /// This will produce a WEBP image URL, or GIF if the member has a GIF avatar. - #[inline] #[must_use] pub fn avatar_url(&self) -> Option { avatar_url(Some(self.guild_id), self.user.id, self.avatar.as_ref()) @@ -513,7 +422,6 @@ impl Member { /// /// This will call [`Self::avatar_url`] first, and if that returns [`None`], it then falls back /// to [`User::face()`]. - #[inline] #[must_use] pub fn face(&self) -> String { self.avatar_url().unwrap_or_else(|| self.user.face()) @@ -536,6 +444,12 @@ impl fmt::Display for Member { } } +impl ExtractKey for Member { + fn extract_key(&self) -> &UserId { + &self.user.id + } +} + /// A partial amount of data for a member. /// /// This is used in [`Message`]s from [`Guild`]s. @@ -547,8 +461,9 @@ impl fmt::Display for Member { /// [link](https://discord.com/developers/docs/topics/gateway-events#message-create), /// [link](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure), /// [link](https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object)) +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct PartialMember { /// Indicator of whether the member can hear in voice channels. @@ -562,9 +477,9 @@ pub struct PartialMember { /// The member's nickname, if present. /// /// Can't be longer than 32 characters. - pub nick: Option, + pub nick: Option>, /// Vector of Ids of [`Role`]s given to the member. - pub roles: Vec, + pub roles: FixedArray, /// Indicator that the member hasn't accepted the rules of the guild yet. #[serde(default)] pub pending: bool, @@ -591,40 +506,48 @@ pub struct PartialMember { impl From for Member { fn from(partial: PartialMember) -> Self { - Member { + let (pending, deaf, mute) = (partial.pending(), partial.deaf(), partial.mute()); + let mut member = Member { + __generated_flags: MemberGeneratedFlags::empty(), user: partial.user.unwrap_or_default(), nick: partial.nick, avatar: None, roles: partial.roles, joined_at: partial.joined_at, premium_since: partial.premium_since, - deaf: partial.deaf, - mute: partial.mute, flags: GuildMemberFlags::default(), - pending: partial.pending, permissions: partial.permissions, communication_disabled_until: None, guild_id: partial.guild_id.unwrap_or_default(), unusual_dm_activity_until: partial.unusual_dm_activity_until, - } + }; + + member.set_pending(pending); + member.set_deaf(deaf); + member.set_mute(mute); + member } } impl From for PartialMember { fn from(member: Member) -> Self { - PartialMember { - deaf: member.deaf, + let (pending, deaf, mute) = (member.pending(), member.deaf(), member.mute()); + let mut partial = PartialMember { + __generated_flags: PartialMemberGeneratedFlags::empty(), joined_at: member.joined_at, - mute: member.mute, nick: member.nick, roles: member.roles, - pending: member.pending, premium_since: member.premium_since, guild_id: Some(member.guild_id), user: Some(member.user), permissions: member.permissions, unusual_dm_activity_until: member.unusual_dm_activity_until, - } + }; + + partial.set_deaf(deaf); + partial.set_mute(mute); + partial.set_pending(pending); + partial } } diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index 1d00445a65f..122dd2139c0 100644 --- a/src/model/guild/mod.rs +++ b/src/model/guild/mod.rs @@ -17,6 +17,7 @@ mod welcome_screen; #[cfg(feature = "model")] use std::borrow::Cow; +use nonmax::{NonMaxU16, NonMaxU64, NonMaxU8}; #[cfg(feature = "model")] use tracing::{error, warn}; @@ -48,18 +49,15 @@ use crate::builder::{ EditScheduledEvent, EditSticker, }; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::cache::Cache; #[cfg(feature = "collector")] use crate::collector::{MessageCollector, ReactionCollector}; -#[cfg(feature = "model")] +#[cfg(doc)] use crate::constants::LARGE_THRESHOLD; #[cfg(feature = "collector")] use crate::gateway::ShardMessenger; #[cfg(feature = "model")] use crate::http::{CacheHttp, Http, UserPagination}; -#[cfg(feature = "model")] -use crate::json::json; +use crate::internal::prelude::*; use crate::model::prelude::*; use crate::model::utils::*; @@ -70,7 +68,7 @@ use crate::model::utils::*; #[non_exhaustive] pub struct Ban { /// The reason given for this ban. - pub reason: Option, + pub reason: Option, /// The user that was banned. pub user: User, } @@ -101,8 +99,9 @@ pub struct AfkMetadata { /// /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-object) plus /// [extension](https://discord.com/developers/docs/topics/gateway-events#guild-create). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct Guild { /// The unique Id identifying the guild. @@ -110,7 +109,7 @@ pub struct Guild { /// This is equivalent to the Id of the default role (`@everyone`). pub id: GuildId, /// The name of the guild. - pub name: String, + pub name: FixedString, /// The hash of the icon used by the guild. /// /// In the client, this appears on the guild list on the left-hand side. @@ -146,11 +145,9 @@ pub struct Guild { /// Default explicit content filter level. pub explicit_content_filter: ExplicitContentFilter, /// A mapping of the guild's roles. - #[serde(with = "roles")] - pub roles: HashMap, + pub roles: ExtractMap, /// All of the guild's custom emojis. - #[serde(with = "emojis")] - pub emojis: HashMap, + pub emojis: ExtractMap, /// The guild features. More information available at [`discord documentation`]. /// /// The following is a list of known features: @@ -182,7 +179,7 @@ pub struct Guild { /// /// /// [`discord documentation`]: https://discord.com/developers/docs/resources/guild#guild-object-guild-features - pub features: Vec, + pub features: FixedArray, /// Indicator of whether the guild requires multi-factor authentication for [`Role`]s or /// [`User`]s with moderation permissions. pub mfa_level: MfaLevel, @@ -199,35 +196,35 @@ pub struct Guild { /// The maximum number of presences for the guild. The default value is currently 25000. /// /// **Note**: It is in effect when it is `None`. - pub max_presences: Option, + pub max_presences: Option, /// The maximum number of members for the guild. - pub max_members: Option, + pub max_members: Option, /// The vanity url code for the guild, if it has one. - pub vanity_url_code: Option, + pub vanity_url_code: Option, /// The server's description, if it has one. - pub description: Option, + pub description: Option, /// The guild's banner, if it has one. - pub banner: Option, + pub banner: Option, /// The server's premium boosting level. pub premium_tier: PremiumTier, /// The total number of users currently boosting this server. - pub premium_subscription_count: Option, + pub premium_subscription_count: Option, /// The preferred locale of this guild only set if guild has the "DISCOVERABLE" feature, /// defaults to en-US. - pub preferred_locale: String, + pub preferred_locale: FixedString, /// The id of the channel where admins and moderators of Community guilds receive notices from /// Discord. /// /// **Note**: Only available on `COMMUNITY` guild, see [`Self::features`]. pub public_updates_channel_id: Option, /// The maximum amount of users in a video channel. - pub max_video_channel_users: Option, + pub max_video_channel_users: Option, /// The maximum amount of users in a stage video channel - pub max_stage_video_channel_users: Option, + pub max_stage_video_channel_users: Option, /// Approximate number of members in this guild. - pub approximate_member_count: Option, + pub approximate_member_count: Option, /// Approximate number of non-offline members in this guild. - pub approximate_presence_count: Option, + pub approximate_presence_count: Option, /// The welcome screen of the guild. /// /// **Note**: Only available on `COMMUNITY` guild, see [`Self::features`]. @@ -237,8 +234,7 @@ pub struct Guild { /// [`discord support article`]: https://support.discord.com/hc/en-us/articles/1500005389362-NSFW-Server-Designation pub nsfw_level: NsfwLevel, /// All of the guild's custom stickers. - #[serde(with = "stickers")] - pub stickers: HashMap, + pub stickers: ExtractMap, /// Whether the guild has the boost progress bar enabled pub premium_progress_bar_enabled: bool, @@ -255,34 +251,28 @@ pub struct Guild { /// The number of members in the guild. pub member_count: u64, /// A mapping of [`User`]s to their current voice state. - #[serde(serialize_with = "serialize_map_values")] - #[serde(deserialize_with = "deserialize_voice_states")] - pub voice_states: HashMap, + pub voice_states: ExtractMap, /// Users who are members of the guild. /// /// Members might not all be available when the [`ReadyEvent`] is received if the /// [`Self::member_count`] is greater than the [`LARGE_THRESHOLD`] set by the library. - #[serde(with = "members")] - pub members: HashMap, + pub members: ExtractMap, /// All voice and text channels contained within a guild. /// /// This contains all channels regardless of permissions (i.e. the ability of the bot to read /// from or connect to them). - #[serde(serialize_with = "serialize_map_values")] - #[serde(deserialize_with = "deserialize_guild_channels")] - pub channels: HashMap, + pub channels: ExtractMap, /// All active threads in this guild that current user has permission to view. - pub threads: Vec, + pub threads: FixedArray, /// A mapping of [`User`]s' Ids to their current presences. /// /// **Note**: This will be empty unless the "guild presences" privileged intent is enabled. - #[serde(with = "presences")] - pub presences: HashMap, + pub presences: ExtractMap, /// The stage instances in this guild. - pub stage_instances: Vec, + pub stage_instances: FixedArray, /// The stage instances in this guild. #[serde(rename = "guild_scheduled_events")] - pub scheduled_events: Vec, + pub scheduled_events: FixedArray, } #[cfg(feature = "model")] @@ -296,8 +286,7 @@ impl Guild { /// Returns an [`Error::Http`] if the guild is unavailable. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn automod_rules(&self, http: impl AsRef) -> Result> { + pub async fn automod_rules(&self, http: &Http) -> Result> { self.id.automod_rules(http).await } @@ -310,12 +299,7 @@ impl Guild { /// Returns an [`Error::Http`] if a rule with the given ID does not exist. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn automod_rule( - &self, - http: impl AsRef, - rule_id: impl Into, - ) -> Result { + pub async fn automod_rule(&self, http: &Http, rule_id: RuleId) -> Result { self.id.automod_rule(http, rule_id).await } @@ -332,13 +316,12 @@ impl Guild { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn create_automod_rule( &self, - cache_http: impl CacheHttp, + http: &Http, builder: EditAutoModRule<'_>, ) -> Result { - self.id.create_automod_rule(cache_http, builder).await + self.id.create_automod_rule(http, builder).await } /// Edit an auto moderation [`Rule`], given its Id. @@ -350,14 +333,13 @@ impl Guild { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn edit_automod_rule( &self, - cache_http: impl CacheHttp, - rule_id: impl Into, + http: &Http, + rule_id: RuleId, builder: EditAutoModRule<'_>, ) -> Result { - self.id.edit_automod_rule(cache_http, rule_id, builder).await + self.id.edit_automod_rule(http, rule_id, builder).await } /// Deletes an auto moderation [`Rule`] from the guild. @@ -370,26 +352,13 @@ impl Guild { /// does not exist. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn delete_automod_rule( &self, - http: impl AsRef, - rule_id: impl Into, + http: &Http, + rule_id: RuleId, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_automod_rule(http, rule_id).await - } - - #[cfg(feature = "cache")] - fn check_hierarchy(&self, cache: &Cache, other_user: UserId) -> Result<()> { - let current_id = cache.as_ref().current_user().id; - - if let Some(higher) = self.greater_member_hierarchy(cache, other_user, current_id) { - if higher != current_id { - return Err(Error::Model(ModelError::Hierarchy)); - } - } - - Ok(()) + self.id.delete_automod_rule(http, rule_id, reason).await } /// Returns the "default" channel of the guild for the passed user id. (This returns the first @@ -397,7 +366,7 @@ impl Guild { #[must_use] pub fn default_channel(&self, uid: UserId) -> Option<&GuildChannel> { let member = self.members.get(&uid)?; - self.channels.values().find(|&channel| { + self.channels.iter().find(|&channel| { channel.kind != ChannelType::Category && self.user_permissions_in(channel, member).view_channel() }) @@ -409,47 +378,16 @@ impl Guild { /// **Note**: This is very costly if used in a server with lots of channels, members, or both. #[must_use] pub fn default_channel_guaranteed(&self) -> Option<&GuildChannel> { - self.channels.values().find(|&channel| { + self.channels.iter().find(|&channel| { channel.kind != ChannelType::Category && self .members - .values() + .iter() .map(|member| self.user_permissions_in(channel, member)) .all(Permissions::view_channel) }) } - /// Intentionally not async. Retrieving anything from HTTP here is overkill/undesired - #[cfg(feature = "cache")] - pub(crate) fn require_perms( - &self, - cache: &Cache, - required_permissions: Permissions, - ) -> Result<(), Error> { - if let Some(member) = self.members.get(&cache.current_user().id) { - let bot_permissions = self.member_permissions(member); - if !bot_permissions.contains(required_permissions) { - return Err(Error::Model(ModelError::InvalidPermissions { - required: required_permissions, - present: bot_permissions, - })); - } - } - Ok(()) - } - - #[cfg(feature = "cache")] - #[deprecated = "Iterate through Guild::channels and use Iterator::find"] - pub fn channel_id_from_name( - &self, - #[allow(unused_variables)] cache: impl AsRef, - name: impl AsRef, - ) -> Option { - let name = name.as_ref(); - - self.channels.values().find(|c| c.name == name).map(|c| c.id) - } - /// Ban a [`User`] from the guild, deleting a number of days' worth of messages (`dmd`) between /// the range 0 and 7. /// @@ -463,66 +401,25 @@ impl Guild { /// /// ```rust,ignore /// // assumes a `user` and `guild` have already been bound - /// let _ = guild.ban(user, 4); + /// let _ = guild.ban(user, 4, None); /// ``` /// /// # Errors /// - /// Returns a [`ModelError::DeleteMessageDaysAmount`] if the number of days' worth of messages + /// Returns a [`ModelError::TooLarge`] if the number of days' worth of messages /// to delete is over the maximum. /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to perform bans, or may return a [`ModelError::Hierarchy`] if the - /// member to be banned has a higher role than the current user. - /// - /// Otherwise returns [`Error::Http`] if the member cannot be banned. + /// Returns [`Error::Http`] if the current user lacks permission to ban the member. /// /// [Ban Members]: Permissions::BAN_MEMBERS - #[inline] pub async fn ban( &self, - cache_http: impl CacheHttp, - user: impl Into, - dmd: u8, - ) -> Result<()> { - self._ban_with_reason(cache_http, user.into(), dmd, "").await - } - - /// Ban a [`User`] from the guild with a reason. Refer to [`Self::ban`] to further - /// documentation. - /// - /// # Errors - /// - /// In addition to the possible reasons [`Self::ban`] may return an error, an - /// [`Error::ExceededLimit`] may also be returned if the reason is too long. - #[inline] - pub async fn ban_with_reason( - &self, - cache_http: impl CacheHttp, - user: impl Into, - dmd: u8, - reason: impl AsRef, - ) -> Result<()> { - self._ban_with_reason(cache_http, user.into(), dmd, reason.as_ref()).await - } - - async fn _ban_with_reason( - &self, - cache_http: impl CacheHttp, + http: &Http, user: UserId, dmd: u8, - reason: &str, + reason: Option<&str>, ) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - self.require_perms(cache, Permissions::BAN_MEMBERS)?; - - self.check_hierarchy(cache, user)?; - } - } - - self.id.ban_with_reason(cache_http.http(), user, dmd, reason).await + self.id.ban(http, user, dmd, reason).await } /// Bans multiple users from the guild, returning the users that were and weren't banned. @@ -532,19 +429,12 @@ impl Guild { /// See [`GuildId::bulk_ban`] for more information. pub async fn bulk_ban( &self, - cache_http: impl CacheHttp, + http: &Http, users: impl IntoIterator, delete_message_seconds: u32, reason: Option<&str>, ) -> Result { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - self.require_perms(cache, Permissions::BAN_MEMBERS & Permissions::MANAGE_GUILD)?; - } - } - - self.id.bulk_ban(cache_http.http(), users, delete_message_seconds, reason).await + self.id.bulk_ban(http, users, delete_message_seconds, reason).await } /// Returns the formatted URL of the guild's banner image, if one exists. @@ -560,24 +450,16 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to perform bans. + /// Returns [`Error::Http`] if the current user lacks permission to perform bans. /// /// [Ban Members]: Permissions::BAN_MEMBERS pub async fn bans( &self, - cache_http: impl CacheHttp, + http: &Http, target: Option, - limit: Option, + limit: Option, ) -> Result> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - self.require_perms(cache, Permissions::BAN_MEMBERS)?; - } - } - - self.id.bans(cache_http.http(), target, limit).await + self.id.bans(http, target, limit).await } /// Adds a [`User`] to this guild with a valid OAuth2 access token. @@ -588,14 +470,13 @@ impl Guild { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - #[inline] pub async fn add_member( &self, - cache_http: impl CacheHttp, - user_id: impl Into, - builder: AddMember, + http: &Http, + user_id: UserId, + builder: AddMember<'_>, ) -> Result> { - self.id.add_member(cache_http, user_id, builder).await + self.id.add_member(http, user_id, builder).await } /// Retrieves a list of [`AuditLogs`] for the guild. @@ -608,14 +489,13 @@ impl Guild { /// or if an invalid value is given. /// /// [View Audit Log]: Permissions::VIEW_AUDIT_LOG - #[inline] pub async fn audit_logs( &self, - http: impl AsRef, + http: &Http, action_type: Option, user_id: Option, before: Option, - limit: Option, + limit: Option, ) -> Result { self.id.audit_logs(http, action_type, user_id, before, limit).await } @@ -625,11 +505,7 @@ impl Guild { /// # Errors /// /// Returns [`Error::Http`] if the guild is currently unavailable. - #[inline] - pub async fn channels( - &self, - http: impl AsRef, - ) -> Result> { + pub async fn channels(&self, http: &Http) -> Result> { self.id.channels(http).await } @@ -658,17 +534,19 @@ impl Guild { /// /// [`Shard`]: crate::gateway::Shard /// [whitelist]: https://discord.com/developers/docs/resources/guild#create-guild - pub async fn create( - http: impl AsRef, - name: &str, - icon: Option, - ) -> Result { - let map = json!({ - "icon": icon, - "name": name, - }); + pub async fn create(http: &Http, name: &str, icon: Option) -> Result { + #[derive(serde::Serialize)] + struct CreateGuild<'a> { + name: &'a str, + icon: Option, + } + + let body = CreateGuild { + name, + icon, + }; - http.as_ref().create_guild(&map).await + http.create_guild(&body).await } /// Creates a new [`Channel`] in the guild. @@ -697,16 +575,15 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS pub async fn create_channel( &self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateChannel<'_>, ) -> Result { - self.id.create_channel(cache_http, builder).await + self.id.create_channel(http, builder).await } /// Creates an emoji in the guild with a name and base64-encoded image. The @@ -731,14 +608,14 @@ impl Guild { /// [`EditProfile::avatar`]: crate::builder::EditProfile::avatar /// [`CreateAttachment`]: crate::builder::CreateAttachment /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS - #[inline] pub async fn create_emoji( &self, - http: impl AsRef, + http: &Http, name: &str, image: &str, + reason: Option<&str>, ) -> Result { - self.id.create_emoji(http, name, image).await + self.id.create_emoji(http, name, image, reason).await } /// Creates an integration for the guild. @@ -750,14 +627,14 @@ impl Guild { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn create_integration( &self, - http: impl AsRef, - integration_id: impl Into, + http: &Http, + integration_id: IntegrationId, kind: &str, + reason: Option<&str>, ) -> Result<()> { - self.id.create_integration(http, integration_id, kind).await + self.id.create_integration(http, integration_id, kind, reason).await } /// Create a guild specific application [`Command`]. @@ -769,13 +646,8 @@ impl Guild { /// See [`CreateCommand::execute`] for a list of possible errors. /// /// [`CreateCommand::execute`]: ../../builder/struct.CreateCommand.html#method.execute - #[inline] - pub async fn create_command( - &self, - cache_http: impl CacheHttp, - builder: CreateCommand, - ) -> Result { - self.id.create_command(cache_http, builder).await + pub async fn create_command(&self, http: &Http, builder: CreateCommand<'_>) -> Result { + self.id.create_command(http, builder).await } /// Override all guild application commands. @@ -785,8 +657,8 @@ impl Guild { /// Returns the same errors as [`Self::create_command`]. pub async fn set_commands( &self, - http: impl AsRef, - commands: Vec, + http: &Http, + commands: &[CreateCommand<'_>], ) -> Result> { self.id.set_commands(http, commands).await } @@ -802,11 +674,11 @@ impl Guild { /// [`CreateCommandPermissionsData::execute`]: ../../builder/struct.CreateCommandPermissionsData.html#method.execute pub async fn edit_command_permissions( &self, - cache_http: impl CacheHttp, + http: &Http, command_id: CommandId, - builder: EditCommandPermissions, + builder: EditCommandPermissions<'_>, ) -> Result { - self.id.edit_command_permissions(cache_http, command_id, builder).await + self.id.edit_command_permissions(http, command_id, builder).await } /// Get all guild application commands. @@ -814,7 +686,7 @@ impl Guild { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_commands(&self, http: impl AsRef) -> Result> { + pub async fn get_commands(&self, http: &Http) -> Result> { self.id.get_commands(http).await } @@ -823,10 +695,7 @@ impl Guild { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_commands_with_localizations( - &self, - http: impl AsRef, - ) -> Result> { + pub async fn get_commands_with_localizations(&self, http: &Http) -> Result> { self.id.get_commands_with_localizations(http).await } @@ -835,11 +704,7 @@ impl Guild { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_command( - &self, - http: impl AsRef, - command_id: CommandId, - ) -> Result { + pub async fn get_command(&self, http: &Http, command_id: CommandId) -> Result { self.id.get_command(http, command_id).await } @@ -852,11 +717,11 @@ impl Guild { /// [`CreateCommand::execute`]: ../../builder/struct.CreateCommand.html#method.execute pub async fn edit_command( &self, - cache_http: impl CacheHttp, + http: &Http, command_id: CommandId, - builder: CreateCommand, + builder: CreateCommand<'_>, ) -> Result { - self.id.edit_command(cache_http, command_id, builder).await + self.id.edit_command(http, command_id, builder).await } /// Delete guild application command by its Id. @@ -864,11 +729,7 @@ impl Guild { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn delete_command( - &self, - http: impl AsRef, - command_id: CommandId, - ) -> Result<()> { + pub async fn delete_command(&self, http: &Http, command_id: CommandId) -> Result<()> { self.id.delete_command(http, command_id).await } @@ -877,10 +738,7 @@ impl Guild { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_commands_permissions( - &self, - http: impl AsRef, - ) -> Result> { + pub async fn get_commands_permissions(&self, http: &Http) -> Result> { self.id.get_commands_permissions(http).await } @@ -891,7 +749,7 @@ impl Guild { /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. pub async fn get_command_permissions( &self, - http: impl AsRef, + http: &Http, command_id: CommandId, ) -> Result { self.id.get_command_permissions(http, command_id).await @@ -907,16 +765,11 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - pub async fn create_role( - &self, - cache_http: impl CacheHttp, - builder: EditRole<'_>, - ) -> Result { - self.id.create_role(cache_http, builder).await + pub async fn create_role(&self, http: &Http, builder: EditRole<'_>) -> Result { + self.id.create_role(http, builder).await } /// Creates a new scheduled event in the guild with the data set, if any. @@ -925,16 +778,15 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Events]: Permissions::CREATE_EVENTS pub async fn create_scheduled_event( &self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateScheduledEvent<'_>, ) -> Result { - self.id.create_scheduled_event(cache_http, builder).await + self.id.create_scheduled_event(http, builder).await } /// Creates a new sticker in the guild with the data set, if any. @@ -943,16 +795,15 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS pub async fn create_sticker<'a>( &self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateSticker<'a>, ) -> Result { - self.id.create_sticker(cache_http.http(), builder).await + self.id.create_sticker(http, builder).await } /// Deletes the current guild if the current user is the owner of the @@ -992,13 +843,13 @@ impl Guild { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn delete_emoji( &self, - http: impl AsRef, - emoji_id: impl Into, + http: &Http, + emoji_id: EmojiId, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_emoji(http, emoji_id).await + self.id.delete_emoji(http, emoji_id, reason).await } /// Deletes an integration by Id from the guild. @@ -1011,13 +862,13 @@ impl Guild { /// that Id does not exist. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn delete_integration( &self, - http: impl AsRef, - integration_id: impl Into, + http: &Http, + integration_id: IntegrationId, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_integration(http, integration_id).await + self.id.delete_integration(http, integration_id, reason).await } /// Deletes a [`Role`] by Id from the guild. @@ -1031,13 +882,13 @@ impl Guild { /// Returns [`Error::Http`] if the current user lacks permission to delete the role. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] pub async fn delete_role( &self, - http: impl AsRef, - role_id: impl Into, + http: &Http, + role_id: RoleId, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_role(http, role_id).await + self.id.delete_role(http, role_id, reason).await } /// Deletes a [`ScheduledEvent`] by id from the guild. @@ -1051,11 +902,10 @@ impl Guild { /// /// [Create Events]: Permissions::CREATE_EVENTS /// [Manage Events]: Permissions::MANAGE_EVENTS - #[inline] pub async fn delete_scheduled_event( &self, - http: impl AsRef, - event_id: impl Into, + http: &Http, + event_id: ScheduledEventId, ) -> Result<()> { self.id.delete_scheduled_event(http, event_id).await } @@ -1073,13 +923,13 @@ impl Guild { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn delete_sticker( &self, - http: impl AsRef, - sticker_id: impl Into, + http: &Http, + sticker_id: StickerId, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_sticker(http, sticker_id).await + self.id.delete_sticker(http, sticker_id, reason).await } /// Edits the current guild with new data where specified. @@ -1108,12 +958,11 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - pub async fn edit(&mut self, cache_http: impl CacheHttp, builder: EditGuild<'_>) -> Result<()> { - let guild = self.id.edit(cache_http, builder).await?; + pub async fn edit(&mut self, http: &Http, builder: EditGuild<'_>) -> Result<()> { + let guild = self.id.edit(http, builder).await?; self.afk_metadata = guild.afk_metadata; self.default_message_notifications = guild.default_message_notifications; @@ -1132,8 +981,6 @@ impl Guild { /// Edits an [`Emoji`]'s name in the guild. /// - /// Also see [`Emoji::edit`] if you have the `cache` and `model` features enabled. - /// /// **Note**: If the emoji was created by the current user, requires either the [Create Guild /// Expressions] or the [Manage Guild Expressions] permission. Otherwise, the [Manage Guild /// Expressions] permission is required. @@ -1145,14 +992,14 @@ impl Guild { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn edit_emoji( &self, - http: impl AsRef, - emoji_id: impl Into, + http: &Http, + emoji_id: EmojiId, name: &str, + reason: Option<&str>, ) -> Result { - self.id.edit_emoji(http, emoji_id, name).await + self.id.edit_emoji(http, emoji_id, name, reason).await } /// Edits the properties a guild member, such as muting or nicknaming them. Returns the new @@ -1168,14 +1015,13 @@ impl Guild { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - #[inline] pub async fn edit_member( &self, - cache_http: impl CacheHttp, - user_id: impl Into, + http: &Http, + user_id: UserId, builder: EditMember<'_>, ) -> Result { - self.id.edit_member(cache_http, user_id, builder).await + self.id.edit_member(http, user_id, builder).await } /// Edits the guild's MFA level. Returns the new level on success. @@ -1187,7 +1033,7 @@ impl Guild { /// Returns [`Error::Http`] if the current user lacks permission. pub async fn edit_mfa_level( &self, - http: impl AsRef, + http: &Http, mfa_level: MfaLevel, audit_log_reason: Option<&str>, ) -> Result { @@ -1202,8 +1048,7 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to change their own nickname. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// Otherwise will return [`Error::Http`] if the current user lacks permission. /// @@ -1212,15 +1057,9 @@ impl Guild { &self, cache_http: impl CacheHttp, new_nickname: Option<&str>, + reason: Option<&str>, ) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - self.require_perms(cache, Permissions::CHANGE_NICKNAME)?; - } - } - - self.id.edit_nickname(cache_http.http(), new_nickname).await + self.id.edit_nickname(cache_http.http(), new_nickname, reason).await } /// Edits a role, optionally setting its fields. @@ -1233,18 +1072,16 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] pub async fn edit_role( &self, - cache_http: impl CacheHttp, - role_id: impl Into, + http: &Http, + role_id: RoleId, builder: EditRole<'_>, ) -> Result { - self.id.edit_role(cache_http, role_id, builder).await + self.id.edit_role(http, role_id, builder).await } /// Edits the order of [`Role`]s. Requires the [Manage Roles] permission. @@ -1263,14 +1100,14 @@ impl Guild { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] pub async fn edit_role_position( &self, - http: impl AsRef, - role_id: impl Into, - position: u16, + http: &Http, + role_id: RoleId, + position: i16, + audit_log_reason: Option<&str>, ) -> Result> { - self.id.edit_role_position(http, role_id, position).await + self.id.edit_role_position(http, role_id, position, audit_log_reason).await } /// Modifies a scheduled event in the guild with the data set, if any. @@ -1280,18 +1117,17 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Events]: Permissions::CREATE_EVENTS /// [Manage Events]: Permissions::MANAGE_EVENTS pub async fn edit_scheduled_event( &self, - cache_http: impl CacheHttp, - event_id: impl Into, + http: &Http, + event_id: ScheduledEventId, builder: EditScheduledEvent<'_>, ) -> Result { - self.id.edit_scheduled_event(cache_http, event_id, builder).await + self.id.edit_scheduled_event(http, event_id, builder).await } /// Edits a sticker. @@ -1326,14 +1162,13 @@ impl Guild { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn edit_sticker( &self, - cache_http: impl CacheHttp, - sticker_id: impl Into, + http: &Http, + sticker_id: StickerId, builder: EditSticker<'_>, ) -> Result { - self.id.edit_sticker(cache_http, sticker_id, builder).await + self.id.edit_sticker(http, sticker_id, builder).await } /// Edits the guild's welcome screen. @@ -1347,10 +1182,10 @@ impl Guild { /// [Manage Guild]: Permissions::MANAGE_GUILD pub async fn edit_welcome_screen( &self, - cache_http: impl CacheHttp, + http: &Http, builder: EditGuildWelcomeScreen<'_>, ) -> Result { - self.id.edit_welcome_screen(cache_http, builder).await + self.id.edit_welcome_screen(http, builder).await } /// Edits the guild's widget. @@ -1364,10 +1199,10 @@ impl Guild { /// [Manage Guild]: Permissions::MANAGE_GUILD pub async fn edit_widget( &self, - cache_http: impl CacheHttp, + http: &Http, builder: EditGuildWidget<'_>, ) -> Result { - self.id.edit_widget(cache_http, builder).await + self.id.edit_widget(http, builder).await } /// Gets a partial amount of guild data by its Id. @@ -1378,12 +1213,8 @@ impl Guild { /// # Errors /// /// Returns an [`Error::Http`] if the current user is not in the guild. - #[inline] - pub async fn get( - cache_http: impl CacheHttp, - guild_id: impl Into, - ) -> Result { - guild_id.into().to_partial_guild(cache_http).await + pub async fn get(cache_http: impl CacheHttp, guild_id: GuildId) -> Result { + guild_id.to_partial_guild(cache_http).await } /// Gets the highest role a [`Member`] of this Guild has. @@ -1391,10 +1222,18 @@ impl Guild { /// Returns None if the member has no roles or the member from this guild. #[must_use] pub fn member_highest_role(&self, member: &Member) -> Option<&Role> { + Self::_member_highest_role_in(&self.roles, member) + } + + /// Helper function that can also be used from [`PartialGuild`]. + pub(crate) fn _member_highest_role_in<'a>( + roles: &'a ExtractMap, + member: &Member, + ) -> Option<&'a Role> { let mut highest: Option<&Role> = None; for role_id in &member.roles { - if let Some(role) = self.roles.get(role_id) { + if let Some(role) = roles.get(role_id) { // Skip this role if this role in iteration has: // - a position less than the recorded highest // - a position equal to the recorded, but a higher ID @@ -1425,62 +1264,70 @@ impl Guild { /// owner, their ID is returned. /// /// [`position`]: Role::position - #[cfg(feature = "cache")] - #[inline] - pub fn greater_member_hierarchy( - &self, - #[allow(unused_variables)] _cache: impl AsRef, - lhs_id: impl Into, - rhs_id: impl Into, - ) -> Option { - self._greater_member_hierarchy(lhs_id.into(), rhs_id.into()) + #[must_use] + pub fn greater_member_hierarchy(&self, lhs_id: UserId, rhs_id: UserId) -> Option { + let lhs = self.members.get(&lhs_id)?; + let rhs = self.members.get(&rhs_id)?; + let lhs_highest_role = self.member_highest_role(lhs); + let rhs_highest_role = self.member_highest_role(rhs); + + Self::_greater_member_hierarchy_in( + lhs_highest_role, + rhs_highest_role, + self.owner_id, + lhs, + rhs, + ) } - #[cfg(feature = "cache")] - fn _greater_member_hierarchy(&self, lhs_id: UserId, rhs_id: UserId) -> Option { + /// Helper function that can also be used from [`PartialGuild`]. + #[must_use] + pub(crate) fn _greater_member_hierarchy_in( + lhs_highest_role: Option<&Role>, + rhs_highest_role: Option<&Role>, + owner_id: UserId, + lhs: &Member, + rhs: &Member, + ) -> Option { // Check that the IDs are the same. If they are, neither is greater. - if lhs_id == rhs_id { + if lhs.user.id == rhs.user.id { return None; } // Check if either user is the guild owner. - if lhs_id == self.owner_id { - return Some(lhs_id); - } else if rhs_id == self.owner_id { - return Some(rhs_id); + if lhs.user.id == owner_id { + return Some(lhs.user.id); + } else if rhs.user.id == owner_id { + return Some(rhs.user.id); } - let lhs = self - .member_highest_role(self.members.get(&lhs_id)?) - .map_or((RoleId::new(1), 0), |r| (r.id, r.position)); + let lhs_role = lhs_highest_role.map_or((RoleId::new(1), 0), |r| (r.id, r.position)); - let rhs = self - .member_highest_role(self.members.get(&rhs_id)?) - .map_or((RoleId::new(1), 0), |r| (r.id, r.position)); + let rhs_role = rhs_highest_role.map_or((RoleId::new(1), 0), |r| (r.id, r.position)); // If LHS and RHS both have no top position or have the same role ID, then no one wins. - if (lhs.1 == 0 && rhs.1 == 0) || (lhs.0 == rhs.0) { + if (lhs_role.1 == 0 && rhs_role.1 == 0) || (lhs_role.0 == rhs_role.0) { return None; } // If LHS's top position is higher than RHS, then LHS wins. - if lhs.1 > rhs.1 { - return Some(lhs_id); + if lhs_role.1 > rhs_role.1 { + return Some(lhs.user.id); } // If RHS's top position is higher than LHS, then RHS wins. - if rhs.1 > lhs.1 { - return Some(rhs_id); + if rhs_role.1 > lhs_role.1 { + return Some(rhs.user.id); } // If LHS and RHS both have the same position, but LHS has the lower role ID, then LHS // wins. // // If RHS has the higher role ID, then RHS wins. - if lhs.1 == rhs.1 && lhs.0 < rhs.0 { - Some(lhs_id) + if lhs_role.1 == rhs_role.1 && lhs_role.0 < rhs_role.0 { + Some(lhs.user.id) } else { - Some(rhs_id) + Some(rhs.user.id) } } @@ -1497,8 +1344,7 @@ impl Guild { /// # Errors /// /// Returns [`Error::Http`] if the guild is unavailable - #[inline] - pub async fn emojis(&self, http: impl AsRef) -> Result> { + pub async fn emojis(&self, http: &Http) -> Result> { self.id.emojis(http).await } @@ -1510,8 +1356,7 @@ impl Guild { /// guild is unavailable. /// /// May also return [`Error::Json`] if there is an error in deserializing the API response. - #[inline] - pub async fn emoji(&self, http: impl AsRef, emoji_id: EmojiId) -> Result { + pub async fn emoji(&self, http: &Http, emoji_id: EmojiId) -> Result { self.id.emoji(http, emoji_id).await } @@ -1526,8 +1371,7 @@ impl Guild { /// May also return [`Error::Json`] if there is an error in deserializing the API response. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn integrations(&self, http: impl AsRef) -> Result> { + pub async fn integrations(&self, http: &Http) -> Result> { self.id.integrations(http).await } @@ -1537,31 +1381,11 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to see invites. - /// - /// Otherwise will return [`Error::Http`] if the current user does not have permission. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - pub async fn invites(&self, cache_http: impl CacheHttp) -> Result> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - self.require_perms(cache, Permissions::MANAGE_GUILD)?; - } - } - - self.id.invites(cache_http.http()).await - } - - /// Checks if the guild is 'large'. - /// - /// A guild is considered large if it has more than 250 members. - #[inline] - #[must_use] - #[deprecated = "Use Guild::large"] - pub fn is_large(&self) -> bool { - self.member_count > u64::from(LARGE_THRESHOLD) + pub async fn invites(&self, http: &Http) -> Result> { + self.id.invites(http).await } /// Kicks a [`Member`] from the guild. @@ -1573,23 +1397,8 @@ impl Guild { /// Returns [`Error::Http`] if the member cannot be kicked by the current user. /// /// [Kick Members]: Permissions::KICK_MEMBERS - #[inline] - pub async fn kick(&self, http: impl AsRef, user_id: impl Into) -> Result<()> { - self.id.kick(http, user_id).await - } - - /// # Errors - /// - /// In addition to the reasons [`Self::kick`] may return an error, may also return an error if - /// the reason is too long. - #[inline] - pub async fn kick_with_reason( - &self, - http: impl AsRef, - user_id: impl Into, - reason: &str, - ) -> Result<()> { - self.id.kick_with_reason(http, user_id, reason).await + pub async fn kick(&self, http: &Http, user_id: UserId, reason: Option<&str>) -> Result<()> { + self.id.kick(http, user_id, reason).await } /// Returns a guild [`Member`] object for the current user. @@ -1600,8 +1409,7 @@ impl Guild { /// /// Returns an [`Error::Http`] if the current user is not in the guild or the access token /// lacks the necessary scope. - #[inline] - pub async fn current_user_member(&self, http: impl AsRef) -> Result { + pub async fn current_user_member(&self, http: &Http) -> Result { self.id.current_user_member(http).await } @@ -1611,8 +1419,7 @@ impl Guild { /// /// May return an [`Error::Http`] if the current user cannot leave the guild, or currently is /// not in the guild. - #[inline] - pub async fn leave(&self, http: impl AsRef) -> Result<()> { + pub async fn leave(&self, http: &Http) -> Result<()> { self.id.leave(http).await } @@ -1625,18 +1432,11 @@ impl Guild { /// /// Returns an [`Error::Http`] if the user is not in the guild or if the guild is otherwise /// unavailable. - #[inline] - pub async fn member( - &self, - cache_http: impl CacheHttp, - user_id: impl Into, - ) -> Result> { - let user_id = user_id.into(); - + pub async fn member(&self, http: &Http, user_id: UserId) -> Result> { if let Some(member) = self.members.get(&user_id) { Ok(Cow::Borrowed(member)) } else { - cache_http.http().get_member(self.id, user_id).await.map(Cow::Owned) + http.get_member(self.id, user_id).await.map(Cow::Owned) } } @@ -1650,15 +1450,14 @@ impl Guild { /// # Errors /// /// Returns an [`Error::Http`] if the API returns an error, may also return - /// [`Error::NotInRange`] if the input is not within range. + /// [`ModelError::TooSmall`] or [`ModelError::TooLarge`] if the limit is not within range. /// /// [`User`]: crate::model::user::User - #[inline] pub async fn members( &self, - http: impl AsRef, - limit: Option, - after: impl Into>, + http: &Http, + limit: Option, + after: Option, ) -> Result> { self.id.members(http, limit, after).await } @@ -1666,9 +1465,8 @@ impl Guild { /// Gets a list of all the members (satisfying the status provided to the function) in this /// guild. pub fn members_with_status(&self, status: OnlineStatus) -> impl Iterator { - self.members.iter().filter_map(move |(id, member)| match self.presences.get(id) { - Some(presence) if presence.status == status => Some(member), - _ => None, + self.members.iter().filter(move |member| { + self.presences.get(&member.user.id).is_some_and(|p| p.status == status) }) } @@ -1695,15 +1493,15 @@ impl Guild { None => (name, None), }; - for member in self.members.values() { - if member.user.name == username + for member in &self.members { + if &*member.user.name == username && discrim.map_or(true, |d| member.user.discriminator == d) { return Some(member); } } - self.members.values().find(|member| member.nick.as_ref().is_some_and(|nick| nick == name)) + self.members.iter().find(|member| member.nick.as_deref().is_some_and(|nick| nick == name)) } /// Retrieves all [`Member`] that start with a given [`String`]. @@ -1725,7 +1523,7 @@ impl Guild { prefix: &str, case_sensitive: bool, sorted: bool, - ) -> Vec<(&Member, String)> { + ) -> Vec<(&Member, &str)> { fn starts_with(name: &str, prefix: &str, case_sensitive: bool) -> bool { if case_sensitive { name.starts_with(prefix) @@ -1736,24 +1534,24 @@ impl Guild { let mut members = self .members - .values() + .iter() .filter_map(|member| { let username = &member.user.name; if starts_with(username, prefix, case_sensitive) { - Some((member, username.clone())) + Some((member, username.as_str())) } else { match &member.nick { Some(nick) => starts_with(nick, prefix, case_sensitive) - .then(|| (member, nick.clone())), + .then(|| (member, nick.as_str())), None => None, } } }) - .collect::>(); + .collect::>(); if sorted { - members.sort_by(|a, b| closest_to_origin(prefix, &a.1[..], &b.1[..])); + members.sort_by(|a, b| closest_to_origin(prefix, a.1, b.1)); } members @@ -1793,16 +1591,16 @@ impl Guild { ) -> Vec<(&Member, String)> { let mut members = self .members - .values() + .iter() .filter_map(|member| { let username = &member.user.name; if contains(username, substring, case_sensitive) { - Some((member, username.clone())) + Some((member, username.clone().into())) } else { match &member.nick { Some(nick) => contains(nick, substring, case_sensitive) - .then(|| (member, nick.clone())), + .then(|| (member, nick.clone().into())), None => None, } } @@ -1845,10 +1643,10 @@ impl Guild { ) -> Vec<(&Member, String)> { let mut members = self .members - .values() + .iter() .filter_map(|member| { let name = &member.user.name; - contains(name, substring, case_sensitive).then(|| (member, name.clone())) + contains(name, substring, case_sensitive).then(|| (member, name.clone().into())) }) .collect::>(); @@ -1890,10 +1688,10 @@ impl Guild { ) -> Vec<(&Member, String)> { let mut members = self .members - .values() + .iter() .filter_map(|member| { let nick = member.nick.as_ref().unwrap_or(&member.user.name); - contains(nick, substring, case_sensitive).then(|| (member, nick.clone())) + contains(nick, substring, case_sensitive).then(|| (member, nick.clone().into())) }) .collect::>(); @@ -1905,7 +1703,6 @@ impl Guild { } /// Calculate a [`Member`]'s permissions in the guild. - #[inline] #[cfg(feature = "cache")] #[must_use] pub fn member_permissions(&self, member: &Member) -> Permissions { @@ -1929,18 +1726,16 @@ impl Guild { /// currently in a voice channel for this [`Guild`]. /// /// [Move Members]: Permissions::MOVE_MEMBERS - #[inline] pub async fn move_member( &self, - cache_http: impl CacheHttp, - user_id: impl Into, - channel_id: impl Into, + http: &Http, + user_id: UserId, + channel_id: ChannelId, ) -> Result { - self.id.move_member(cache_http, user_id, channel_id).await + self.id.move_member(http, user_id, channel_id).await } /// Calculate a [`Member`]'s permissions in a given channel in the guild. - #[inline] #[must_use] pub fn user_permissions_in(&self, channel: &GuildChannel, member: &Member) -> Permissions { Self::_user_permissions_in( @@ -1985,7 +1780,7 @@ impl Guild { member_user_id: UserId, member_roles: &[RoleId], guild_id: GuildId, - guild_roles: &HashMap, + guild_roles: &ExtractMap, guild_owner_id: UserId, ) -> Permissions { let mut everyone_allow_overwrites = Permissions::empty(); @@ -2049,117 +1844,24 @@ impl Guild { }) } - /// Calculate a [`Role`]'s permissions in a given channel in the guild. - /// - /// # Errors - /// - /// Will return an [`Error::Model`] if the [`Role`] or [`Channel`] is not from this [`Guild`]. - #[inline] - #[deprecated = "this function ignores other roles the user may have as well as user-specific permissions; use user_permissions_in instead"] - pub fn role_permissions_in(&self, channel: &GuildChannel, role: &Role) -> Result { - Self::_role_permissions_in(channel, role, self.id) - } - - /// Helper function that can also be used from [`PartialGuild`]. - pub(crate) fn _role_permissions_in( - channel: &GuildChannel, - role: &Role, - guild_id: GuildId, - ) -> Result { - // Fail if the role or channel is not from this guild. - if role.guild_id != guild_id || channel.guild_id != guild_id { - return Err(Error::Model(ModelError::WrongGuild)); - } - - let mut permissions = role.permissions; - - if permissions.contains(Permissions::ADMINISTRATOR) { - return Ok(Self::remove_unnecessary_voice_permissions(channel, Permissions::all())); - } - - for overwrite in &channel.permission_overwrites { - if let PermissionOverwriteType::Role(permissions_role_id) = overwrite.kind { - if permissions_role_id == role.id { - permissions = (permissions & !overwrite.deny) | overwrite.allow; - - break; - } - } - } - - Self::remove_unusable_permissions(&mut permissions); - - Ok(permissions) - } - /// Retrieves the count of the number of [`Member`]s that would be pruned with the number of /// given days. /// /// See the documentation on [`GuildPrune`] for more information. /// - /// **Note**: Requires the [Kick Members] permission. + /// **Note**: Requires [Manage Guild] and [Kick Members] permission. /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to kick members. - /// /// Otherwise may return [`Error::Http`] if the current user does not have permission. Can also /// return [`Error::Json`] if there is an error in deserializing the API response. /// /// [Kick Members]: Permissions::KICK_MEMBERS + /// [Manage Guild]: Permissions::MANAGE_GUILD /// [`Error::Http`]: crate::error::Error::Http /// [`Error::Json`]: crate::error::Error::Json - pub async fn prune_count(&self, cache_http: impl CacheHttp, days: u8) -> Result { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - self.require_perms(cache, Permissions::KICK_MEMBERS)?; - } - } - - self.id.prune_count(cache_http.http(), days).await - } - - pub(crate) fn remove_unusable_permissions(permissions: &mut Permissions) { - // No SEND_MESSAGES => no message-sending-related actions - // If the member does not have the `SEND_MESSAGES` permission, then throw out message-able - // permissions. - if !permissions.contains(Permissions::SEND_MESSAGES) { - *permissions &= !(Permissions::SEND_TTS_MESSAGES - | Permissions::MENTION_EVERYONE - | Permissions::EMBED_LINKS - | Permissions::ATTACH_FILES); - } - - // If the permission does not have the `VIEW_CHANNEL` permission, then throw out actionable - // permissions. - if !permissions.contains(Permissions::VIEW_CHANNEL) { - *permissions &= !(Permissions::KICK_MEMBERS - | Permissions::BAN_MEMBERS - | Permissions::ADMINISTRATOR - | Permissions::MANAGE_GUILD - | Permissions::CHANGE_NICKNAME - | Permissions::MANAGE_NICKNAMES); - } - } - - pub(crate) fn remove_unnecessary_voice_permissions( - channel: &GuildChannel, - mut permissions: Permissions, - ) -> Permissions { - // If this is a text channel, then throw out voice permissions. - if channel.kind == ChannelType::Text { - permissions &= !(Permissions::CONNECT - | Permissions::SPEAK - | Permissions::MUTE_MEMBERS - | Permissions::DEAFEN_MEMBERS - | Permissions::MOVE_MEMBERS - | Permissions::USE_VAD - | Permissions::STREAM); - } - - permissions + pub async fn prune_count(&self, http: &Http, days: u8) -> Result { + self.id.prune_count(http, days).await } /// Re-orders the channels of the guild. @@ -2174,10 +1876,9 @@ impl Guild { /// Returns an [`Error::Http`] if the current user is lacking permission. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - #[inline] pub async fn reorder_channels( &self, - http: impl AsRef, + http: &Http, channels: impl IntoIterator, ) -> Result<()> { self.id.reorder_channels(http, channels).await @@ -2194,12 +1895,11 @@ impl Guild { /// # Errors /// /// Returns an [`Error::Http`] if the API returns an error. - #[inline] pub async fn search_members( &self, - http: impl AsRef, + http: &Http, query: &str, - limit: Option, + limit: Option, ) -> Result> { self.id.search_members(http, query, limit).await } @@ -2218,8 +1918,8 @@ impl Guild { /// [View Channel]: Permissions::VIEW_CHANNEL pub async fn scheduled_event( &self, - http: impl AsRef, - event_id: impl Into, + http: &Http, + event_id: ScheduledEventId, with_user_count: bool, ) -> Result { self.id.scheduled_event(http, event_id, with_user_count).await @@ -2237,7 +1937,7 @@ impl Guild { /// [View Channel]: Permissions::VIEW_CHANNEL pub async fn scheduled_events( &self, - http: impl AsRef, + http: &Http, with_user_count: bool, ) -> Result> { self.id.scheduled_events(http, with_user_count).await @@ -2257,9 +1957,9 @@ impl Guild { /// [View Channel]: Permissions::VIEW_CHANNEL pub async fn scheduled_event_users( &self, - http: impl AsRef, - event_id: impl Into, - limit: Option, + http: &Http, + event_id: ScheduledEventId, + limit: Option, ) -> Result> { self.id.scheduled_event_users(http, event_id, limit).await } @@ -2277,9 +1977,9 @@ impl Guild { /// [View Channel]: Permissions::VIEW_CHANNEL pub async fn scheduled_event_users_optioned( &self, - http: impl AsRef, - event_id: impl Into, - limit: Option, + http: &Http, + event_id: ScheduledEventId, + limit: Option, target: Option, with_member: Option, ) -> Result> { @@ -2288,41 +1988,11 @@ impl Guild { /// Returns the Id of the shard associated with the guild. /// - /// When the cache is enabled this will automatically retrieve the total number of shards. - /// - /// **Note**: When the cache is enabled, this function unlocks the cache to retrieve the total - /// number of shards in use. If you already have the total, consider using [`utils::shard_id`]. - /// - /// [`utils::shard_id`]: crate::utils::shard_id - #[cfg(all(feature = "cache", feature = "utils"))] - #[inline] - pub fn shard_id(&self, cache: impl AsRef) -> u32 { - self.id.shard_id(&cache) - } - - /// Returns the Id of the shard associated with the guild. - /// - /// When the cache is enabled this will automatically retrieve the total number of shards. - /// - /// When the cache is not enabled, the total number of shards being used will need to be - /// passed. - /// - /// # Examples - /// - /// Retrieve the Id of the shard for a guild with Id `81384788765712384`, using 17 shards: - /// - /// ```rust,ignore - /// use serenity::utils; - /// - /// // assumes a `guild` has already been bound - /// - /// assert_eq!(guild.shard_id(17), 7); - /// ``` - #[cfg(all(feature = "utils", not(feature = "cache")))] - #[inline] + /// See the documentation for [`GuildId::shard_id`]. #[must_use] - pub fn shard_id(&self, shard_count: u32) -> u32 { - self.id.shard_id(shard_count) + #[cfg(feature = "utils")] + pub fn shard_id(&self, shard_total: std::num::NonZeroU16) -> u16 { + self.id.shard_id(shard_total) } /// Returns the formatted URL of the guild's splash image, if one exists. @@ -2341,11 +2011,10 @@ impl Guild { /// [`Integration`] with that Id does not exist. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn start_integration_sync( &self, - http: impl AsRef, - integration_id: impl Into, + http: &Http, + integration_id: IntegrationId, ) -> Result<()> { self.id.start_integration_sync(http, integration_id).await } @@ -2358,9 +2027,6 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to kick members. - /// /// Otherwise will return [`Error::Http`] if the current user does not have permission. /// /// Can also return an [`Error::Json`] if there is an error deserializing the API response. @@ -2369,15 +2035,13 @@ impl Guild { /// [Manage Guild]: Permissions::MANAGE_GUILD /// [`Error::Http`]: crate::error::Error::Http /// [`Error::Json`]: crate::error::Error::Json - pub async fn start_prune(&self, cache_http: impl CacheHttp, days: u8) -> Result { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - self.require_perms(cache, Permissions::KICK_MEMBERS | Permissions::MANAGE_GUILD)?; - } - } - - self.id.start_prune(cache_http.http(), days).await + pub async fn start_prune( + &self, + cache_http: impl CacheHttp, + days: u8, + reason: Option<&str>, + ) -> Result { + self.id.start_prune(cache_http.http(), days, reason).await } /// Unbans the given [`User`] from the guild. @@ -2386,25 +2050,16 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to perform bans. - /// - /// Otherwise will return an [`Error::Http`] if the current user does not have permission. + /// Returns [`Error::Http`] if the current user does not have permission to perform bans. /// /// [Ban Members]: Permissions::BAN_MEMBERS pub async fn unban( &self, cache_http: impl CacheHttp, - user_id: impl Into, + user_id: UserId, + reason: Option<&str>, ) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - self.require_perms(cache, Permissions::BAN_MEMBERS)?; - } - } - - self.id.unban(cache_http.http(), user_id).await + self.id.unban(cache_http.http(), user_id, reason).await } /// Retrieve's the guild's vanity URL. @@ -2417,8 +2072,7 @@ impl Guild { /// /// Will return [`Error::Http`] if the current user is lacking permissions. Can also return an /// [`Error::Json`] if there is an error deserializing the API response. - #[inline] - pub async fn vanity_url(&self, http: impl AsRef) -> Result { + pub async fn vanity_url(&self, http: &Http) -> Result { self.id.vanity_url(http).await } @@ -2432,8 +2086,7 @@ impl Guild { /// /// Will return an [`Error::Http`] if the current user is lacking permissions. Can also return /// an [`Error::Json`] if there is an error deserializing the API response. - #[inline] - pub async fn webhooks(&self, http: impl AsRef) -> Result> { + pub async fn webhooks(&self, http: &Http) -> Result> { self.id.webhooks(http).await } @@ -2455,7 +2108,7 @@ impl Guild { /// impl EventHandler for Handler { /// async fn message(&self, ctx: Context, msg: Message) { /// if let Some(guild_id) = msg.guild_id { - /// if let Some(guild) = guild_id.to_guild_cached(&ctx) { + /// if let Some(guild) = guild_id.to_guild_cached(&ctx.cache) { /// if let Some(role) = guild.role_by_name("role_name") { /// println!("{:?}", role); /// } @@ -2466,35 +2119,32 @@ impl Guild { /// ``` #[must_use] pub fn role_by_name(&self, role_name: &str) -> Option<&Role> { - self.roles.values().find(|role| role_name == role.name) + self.roles.iter().find(|role| role_name == &*role.name) } /// Returns a builder which can be awaited to obtain a message or stream of messages in this /// guild. #[cfg(feature = "collector")] - pub fn await_reply(&self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_reply(&self, shard_messenger: ShardMessenger) -> MessageCollector { MessageCollector::new(shard_messenger).guild_id(self.id) } /// Same as [`Self::await_reply`]. #[cfg(feature = "collector")] - pub fn await_replies(&self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_replies(&self, shard_messenger: ShardMessenger) -> MessageCollector { self.await_reply(shard_messenger) } /// Returns a builder which can be awaited to obtain a message or stream of reactions sent in /// this guild. #[cfg(feature = "collector")] - pub fn await_reaction(&self, shard_messenger: impl AsRef) -> ReactionCollector { + pub fn await_reaction(&self, shard_messenger: ShardMessenger) -> ReactionCollector { ReactionCollector::new(shard_messenger).guild_id(self.id) } /// Same as [`Self::await_reaction`]. #[cfg(feature = "collector")] - pub fn await_reactions( - &self, - shard_messenger: impl AsRef, - ) -> ReactionCollector { + pub fn await_reactions(&self, shard_messenger: ShardMessenger) -> ReactionCollector { self.await_reaction(shard_messenger) } @@ -2504,7 +2154,7 @@ impl Guild { /// /// Returns [`Error::Http`] if there is an error in the deserialization, or if the bot issuing /// the request is not in the guild. - pub async fn get_active_threads(&self, http: impl AsRef) -> Result { + pub async fn get_active_threads(&self, http: &Http) -> Result { self.id.get_active_threads(http).await } } @@ -2660,7 +2310,7 @@ pub struct GuildInfo { /// Can be used to calculate creation date. pub id: GuildId, /// The name of the guild. - pub name: String, + pub name: FixedString, /// The hash of the icon of the guild. /// /// This can be used to generate a URL to the guild's icon image. @@ -2670,7 +2320,7 @@ pub struct GuildInfo { /// The permissions that the current user has. pub permissions: Permissions, /// See [`Guild::features`]. - pub features: Vec, + pub features: FixedArray, } #[cfg(feature = "model")] @@ -2713,11 +2363,9 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum DefaultMessageNotificationLevel { /// Receive notifications for everything. - #[default] All = 0, /// Receive only mentions. Mentions = 1, @@ -2731,11 +2379,9 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-object-explicit-content-filter-level). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ExplicitContentFilter { /// Don't scan any messages. - #[default] None = 0, /// Scan messages from members without a role. WithoutRole = 1, @@ -2751,11 +2397,9 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-object-mfa-level). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum MfaLevel { /// MFA is disabled. - #[default] None = 0, /// MFA is enabled. Elevated = 1, @@ -2770,11 +2414,9 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-object-verification-level). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum VerificationLevel { /// Does not require any verification. - #[default] None = 0, /// Must have a verified email on the user's Discord account. Low = 1, @@ -2794,11 +2436,9 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum NsfwLevel { /// The nsfw level is not specified. - #[default] Default = 0, /// The guild is considered as explicit. Explicit = 1, @@ -2816,7 +2456,6 @@ enum_number! { /// See [AfkMetadata::afk_timeout]. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u16", into = "u16")] #[non_exhaustive] pub enum AfkTimeout { OneMinute = 60, @@ -2832,16 +2471,15 @@ enum_number! { mod test { #[cfg(feature = "model")] mod model { - use std::collections::*; use std::num::NonZeroU16; use crate::model::prelude::*; fn gen_member() -> Member { Member { - nick: Some("aaaa".to_string()), + nick: Some(FixedString::from_static_trunc("aaaa")), user: User { - name: "test".into(), + name: FixedString::from_static_trunc("test"), discriminator: NonZeroU16::new(1432), ..User::default() }, @@ -2853,7 +2491,7 @@ mod test { let m = gen_member(); Guild { - members: HashMap::from([(m.user.id, m)]), + members: ExtractMap::from_iter([m]), ..Default::default() } } diff --git a/src/model/guild/partial_guild.rs b/src/model/guild/partial_guild.rs index fcb440f50ac..b7464b112a2 100644 --- a/src/model/guild/partial_guild.rs +++ b/src/model/guild/partial_guild.rs @@ -1,3 +1,4 @@ +use nonmax::{NonMaxU16, NonMaxU64, NonMaxU8}; use serde::Serialize; #[cfg(feature = "model")] @@ -14,24 +15,24 @@ use crate::builder::{ EditRole, EditSticker, }; -#[cfg(all(feature = "cache", feature = "utils", feature = "client"))] -use crate::cache::Cache; #[cfg(feature = "collector")] use crate::collector::{MessageCollector, ReactionCollector}; #[cfg(feature = "collector")] use crate::gateway::ShardMessenger; #[cfg(feature = "model")] use crate::http::{CacheHttp, Http, UserPagination}; +use crate::internal::prelude::*; +use crate::internal::utils::lending_for_each; use crate::model::prelude::*; #[cfg(feature = "model")] use crate::model::utils::icon_url; -use crate::model::utils::{emojis, roles, stickers}; /// Partial information about a [`Guild`]. This does not include information like member data. /// /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-object). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(remote = "Self")] #[non_exhaustive] pub struct PartialGuild { @@ -43,7 +44,7 @@ pub struct PartialGuild { /// This is equivalent to the Id of the default role (`@everyone`). pub id: GuildId, /// The name of the guild. - pub name: String, + pub name: FixedString, /// The hash of the icon used by the guild. /// /// In the client, this appears on the guild list on the left-hand side. @@ -79,11 +80,9 @@ pub struct PartialGuild { /// Default explicit content filter level. pub explicit_content_filter: ExplicitContentFilter, /// A mapping of the guild's roles. - #[serde(with = "roles")] - pub roles: HashMap, + pub roles: ExtractMap, /// All of the guild's custom emojis. - #[serde(with = "emojis")] - pub emojis: HashMap, + pub emojis: ExtractMap, /// The guild features. More information available at [`discord documentation`]. /// /// The following is a list of known features: @@ -115,7 +114,7 @@ pub struct PartialGuild { /// /// /// [`discord documentation`]: https://discord.com/developers/docs/resources/guild#guild-object-guild-features - pub features: Vec, + pub features: FixedArray, /// Indicator of whether the guild requires multi-factor authentication for [`Role`]s or /// [`User`]s with moderation permissions. pub mfa_level: MfaLevel, @@ -132,35 +131,35 @@ pub struct PartialGuild { /// The maximum number of presences for the guild. The default value is currently 25000. /// /// **Note**: It is in effect when it is `None`. - pub max_presences: Option, + pub max_presences: Option, /// The maximum number of members for the guild. - pub max_members: Option, + pub max_members: Option, /// The vanity url code for the guild, if it has one. - pub vanity_url_code: Option, + pub vanity_url_code: Option, /// The server's description, if it has one. - pub description: Option, + pub description: Option, /// The guild's banner, if it has one. - pub banner: Option, + pub banner: Option, /// The server's premium boosting level. pub premium_tier: PremiumTier, /// The total number of users currently boosting this server. - pub premium_subscription_count: Option, + pub premium_subscription_count: Option, /// The preferred locale of this guild only set if guild has the "DISCOVERABLE" feature, /// defaults to en-US. - pub preferred_locale: String, + pub preferred_locale: FixedString, /// The id of the channel where admins and moderators of Community guilds receive notices from /// Discord. /// /// **Note**: Only available on `COMMUNITY` guild, see [`Self::features`]. pub public_updates_channel_id: Option, /// The maximum amount of users in a video channel. - pub max_video_channel_users: Option, + pub max_video_channel_users: Option, /// The maximum amount of users in a stage video channel - pub max_stage_video_channel_users: Option, + pub max_stage_video_channel_users: Option, /// Approximate number of members in this guild. - pub approximate_member_count: Option, + pub approximate_member_count: Option, /// Approximate number of non-offline members in this guild. - pub approximate_presence_count: Option, + pub approximate_presence_count: Option, /// The welcome screen of the guild. /// /// **Note**: Only available on `COMMUNITY` guild, see [`Self::features`]. @@ -170,8 +169,7 @@ pub struct PartialGuild { /// [`discord support article`]: https://support.discord.com/hc/en-us/articles/1500005389362-NSFW-Server-Designation pub nsfw_level: NsfwLevel, /// All of the guild's custom stickers. - #[serde(with = "stickers")] - pub stickers: HashMap, + pub stickers: ExtractMap, /// Whether the guild has the boost progress bar enabled pub premium_progress_bar_enabled: bool, } @@ -187,8 +185,7 @@ impl PartialGuild { /// Returns [`Error::Http`] if the guild is unavailable. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn automod_rules(self, http: impl AsRef) -> Result> { + pub async fn automod_rules(self, http: &Http) -> Result> { self.id.automod_rules(http).await } @@ -201,12 +198,7 @@ impl PartialGuild { /// Returns [`Error::Http`] if a rule with the given ID does not exist. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn automod_rule( - &self, - http: impl AsRef, - rule_id: impl Into, - ) -> Result { + pub async fn automod_rule(&self, http: &Http, rule_id: RuleId) -> Result { self.id.automod_rule(http, rule_id).await } @@ -223,13 +215,12 @@ impl PartialGuild { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn create_automod_rule( &self, - cache_http: impl CacheHttp, + http: &Http, builder: EditAutoModRule<'_>, ) -> Result { - self.id.create_automod_rule(cache_http, builder).await + self.id.create_automod_rule(http, builder).await } /// Edit an auto moderation [`Rule`], given its Id. @@ -241,14 +232,13 @@ impl PartialGuild { /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn edit_automod_rule( &self, - cache_http: impl CacheHttp, - rule_id: impl Into, + http: &Http, + rule_id: RuleId, builder: EditAutoModRule<'_>, ) -> Result { - self.id.edit_automod_rule(cache_http, rule_id, builder).await + self.id.edit_automod_rule(http, rule_id, builder).await } /// Deletes an auto moderation [`Rule`] from the guild. @@ -261,13 +251,13 @@ impl PartialGuild { /// does not exist. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn delete_automod_rule( &self, - http: impl AsRef, - rule_id: impl Into, + http: &Http, + rule_id: RuleId, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_automod_rule(http, rule_id).await + self.id.delete_automod_rule(http, rule_id, reason).await } /// Ban a [`User`] from the guild, deleting a number of days' worth of messages (`dmd`) between @@ -286,38 +276,20 @@ impl PartialGuild { /// /// # Errors /// - /// Returns a [`ModelError::DeleteMessageDaysAmount`] if the number of days' worth of messages + /// Returns a [`ModelError::TooLarge`] if the number of days' worth of messages /// to delete is over the maximum. /// /// Also may return [`Error::Http`] if the current user lacks permission. /// /// [Ban Members]: Permissions::BAN_MEMBERS - #[inline] pub async fn ban( &self, - http: impl AsRef, - user: impl Into, + http: &Http, + user: UserId, dmd: u8, + reason: Option<&str>, ) -> Result<()> { - self.ban_with_reason(http, user, dmd, "").await - } - - /// Ban a [`User`] from the guild with a reason. Refer to [`Self::ban`] to further - /// documentation. - /// - /// # Errors - /// - /// In addition to the reasons [`Self::ban`] may return an error, can also return an error if - /// the reason is too long. - #[inline] - pub async fn ban_with_reason( - &self, - http: impl AsRef, - user: impl Into, - dmd: u8, - reason: impl AsRef, - ) -> Result<()> { - self.id.ban_with_reason(http, user, dmd, reason).await + self.id.ban(http, user, dmd, reason).await } /// Gets a list of the guild's bans, with additional options and filtering. See @@ -330,12 +302,11 @@ impl PartialGuild { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Ban Members]: Permissions::BAN_MEMBERS - #[inline] pub async fn bans( &self, - http: impl AsRef, + http: &Http, target: Option, - limit: Option, + limit: Option, ) -> Result> { self.id.bans(http, target, limit).await } @@ -350,14 +321,13 @@ impl PartialGuild { /// given. /// /// [View Audit Log]: Permissions::VIEW_AUDIT_LOG - #[inline] pub async fn audit_logs( &self, - http: impl AsRef, + http: &Http, action_type: Option, user_id: Option, before: Option, - limit: Option, + limit: Option, ) -> Result { self.id.audit_logs(http, action_type, user_id, before, limit).await } @@ -368,27 +338,10 @@ impl PartialGuild { /// /// Returns [`Error::Http`] if the current user is not in the guild or if the guild is /// otherwise unavailable. - #[inline] - pub async fn channels( - &self, - http: impl AsRef, - ) -> Result> { + pub async fn channels(&self, http: &Http) -> Result> { self.id.channels(http).await } - #[cfg(feature = "cache")] - #[deprecated = "Use Cache::guild and Guild::channels"] - pub fn channel_id_from_name( - &self, - cache: impl AsRef, - name: impl AsRef, - ) -> Option { - let cache = cache.as_ref(); - let guild = cache.guild(self.id)?; - #[allow(deprecated)] - guild.channel_id_from_name(cache, name) - } - /// Creates a [`GuildChannel`] in the guild. /// /// Refer to [`Http::create_channel`] for more information. @@ -419,16 +372,15 @@ impl PartialGuild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS pub async fn create_channel( &self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateChannel<'_>, ) -> Result { - self.id.create_channel(cache_http, builder).await + self.id.create_channel(http, builder).await } /// Creates an emoji in the guild with a name and base64-encoded image. @@ -451,14 +403,14 @@ impl PartialGuild { /// [`EditProfile::avatar`]: crate::builder::EditProfile::avatar /// [`utils::read_image`]: crate::utils::read_image /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS - #[inline] pub async fn create_emoji( &self, - http: impl AsRef, + http: &Http, name: &str, image: &str, + reason: Option<&str>, ) -> Result { - self.id.create_emoji(http, name, image).await + self.id.create_emoji(http, name, image, reason).await } /// Creates an integration for the guild. @@ -470,14 +422,14 @@ impl PartialGuild { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn create_integration( &self, - http: impl AsRef, - integration_id: impl Into, + http: &Http, + integration_id: IntegrationId, kind: &str, + reason: Option<&str>, ) -> Result<()> { - self.id.create_integration(http, integration_id, kind).await + self.id.create_integration(http, integration_id, kind, reason).await } /// Create a guild specific application [`Command`]. @@ -489,13 +441,8 @@ impl PartialGuild { /// See [`CreateCommand::execute`] for a list of possible errors. /// /// [`CreateCommand::execute`]: ../../builder/struct.CreateCommand.html#method.execute - #[inline] - pub async fn create_command( - &self, - cache_http: impl CacheHttp, - builder: CreateCommand, - ) -> Result { - self.id.create_command(cache_http, builder).await + pub async fn create_command(&self, http: &Http, builder: CreateCommand<'_>) -> Result { + self.id.create_command(http, builder).await } /// Override all guild application commands. @@ -505,8 +452,8 @@ impl PartialGuild { /// Returns the same errors as [`Self::create_command`]. pub async fn set_commands( &self, - http: impl AsRef, - commands: Vec, + http: &Http, + commands: &[CreateCommand<'_>], ) -> Result> { self.id.set_commands(http, commands).await } @@ -522,11 +469,11 @@ impl PartialGuild { /// [`CreateCommandPermissionsData::execute`]: ../../builder/struct.CreateCommandPermissionsData.html#method.execute pub async fn edit_command_permissions( &self, - cache_http: impl CacheHttp, + http: &Http, command_id: CommandId, - builder: EditCommandPermissions, + builder: EditCommandPermissions<'_>, ) -> Result { - self.id.edit_command_permissions(cache_http, command_id, builder).await + self.id.edit_command_permissions(http, command_id, builder).await } /// Get all guild application commands. @@ -534,7 +481,7 @@ impl PartialGuild { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_commands(&self, http: impl AsRef) -> Result> { + pub async fn get_commands(&self, http: &Http) -> Result> { self.id.get_commands(http).await } @@ -543,10 +490,7 @@ impl PartialGuild { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_commands_with_localizations( - &self, - http: impl AsRef, - ) -> Result> { + pub async fn get_commands_with_localizations(&self, http: &Http) -> Result> { self.id.get_commands_with_localizations(http).await } @@ -555,11 +499,7 @@ impl PartialGuild { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_command( - &self, - http: impl AsRef, - command_id: CommandId, - ) -> Result { + pub async fn get_command(&self, http: &Http, command_id: CommandId) -> Result { self.id.get_command(http, command_id).await } @@ -572,11 +512,11 @@ impl PartialGuild { /// [`CreateCommand::execute`]: ../../builder/struct.CreateCommand.html#method.execute pub async fn edit_command( &self, - cache_http: impl CacheHttp, + http: &Http, command_id: CommandId, - builder: CreateCommand, + builder: CreateCommand<'_>, ) -> Result { - self.id.edit_command(cache_http, command_id, builder).await + self.id.edit_command(http, command_id, builder).await } /// Delete guild application command by its Id. @@ -584,11 +524,7 @@ impl PartialGuild { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn delete_command( - &self, - http: impl AsRef, - command_id: CommandId, - ) -> Result<()> { + pub async fn delete_command(&self, http: &Http, command_id: CommandId) -> Result<()> { self.id.delete_command(http, command_id).await } @@ -597,10 +533,7 @@ impl PartialGuild { /// # Errors /// /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn get_commands_permissions( - &self, - http: impl AsRef, - ) -> Result> { + pub async fn get_commands_permissions(&self, http: &Http) -> Result> { self.id.get_commands_permissions(http).await } @@ -611,7 +544,7 @@ impl PartialGuild { /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. pub async fn get_command_permissions( &self, - http: impl AsRef, + http: &Http, command_id: CommandId, ) -> Result { self.id.get_command_permissions(http, command_id).await @@ -625,17 +558,11 @@ impl PartialGuild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] - pub async fn create_role( - &self, - cache_http: impl CacheHttp, - builder: EditRole<'_>, - ) -> Result { - self.id.create_role(cache_http, builder).await + pub async fn create_role(&self, http: &Http, builder: EditRole<'_>) -> Result { + self.id.create_role(http, builder).await } /// Creates a new sticker in the guild with the data set, if any. @@ -644,16 +571,15 @@ impl PartialGuild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS pub async fn create_sticker<'a>( &self, - cache_http: impl CacheHttp, + http: &Http, builder: CreateSticker<'a>, ) -> Result { - self.id.create_sticker(cache_http, builder).await + self.id.create_sticker(http, builder).await } /// Deletes the current guild if the current user is the owner of the @@ -665,8 +591,7 @@ impl PartialGuild { /// /// Returns [`Error::Http`] if the current user is not the owner of /// the guild. - #[inline] - pub async fn delete(&self, http: impl AsRef) -> Result<()> { + pub async fn delete(&self, http: &Http) -> Result<()> { self.id.delete(http).await } @@ -683,13 +608,13 @@ impl PartialGuild { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn delete_emoji( &self, - http: impl AsRef, - emoji_id: impl Into, + http: &Http, + emoji_id: EmojiId, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_emoji(http, emoji_id).await + self.id.delete_emoji(http, emoji_id, reason).await } /// Deletes an integration by Id from the guild. @@ -702,13 +627,13 @@ impl PartialGuild { /// that Id does not exist in the guild. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] pub async fn delete_integration( &self, - http: impl AsRef, - integration_id: impl Into, + http: &Http, + integration_id: IntegrationId, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_integration(http, integration_id).await + self.id.delete_integration(http, integration_id, reason).await } /// Deletes a [`Role`] by Id from the guild. @@ -723,13 +648,13 @@ impl PartialGuild { /// does not exist in the Guild. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] pub async fn delete_role( &self, - http: impl AsRef, - role_id: impl Into, + http: &Http, + role_id: RoleId, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_role(http, role_id).await + self.id.delete_role(http, role_id, reason).await } /// Deletes a [`Sticker`] by Id from the guild. @@ -745,13 +670,13 @@ impl PartialGuild { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn delete_sticker( &self, - http: impl AsRef, - sticker_id: impl Into, + http: &Http, + sticker_id: StickerId, + reason: Option<&str>, ) -> Result<()> { - self.id.delete_sticker(http, sticker_id).await + self.id.delete_sticker(http, sticker_id, reason).await } /// Edits the current guild with new data where specified. @@ -760,12 +685,11 @@ impl PartialGuild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - pub async fn edit(&mut self, cache_http: impl CacheHttp, builder: EditGuild<'_>) -> Result<()> { - let guild = self.id.edit(cache_http, builder).await?; + pub async fn edit(&mut self, http: &Http, builder: EditGuild<'_>) -> Result<()> { + let guild = self.id.edit(http, builder).await?; self.afk_metadata = guild.afk_metadata; self.default_message_notifications = guild.default_message_notifications; @@ -784,8 +708,6 @@ impl PartialGuild { /// Edits an [`Emoji`]'s name in the guild. /// - /// Also see [`Emoji::edit`] if you have the `cache` and `methods` features enabled. - /// /// **Note**: If the emoji was created by the current user, requires either the [Create Guild /// Expressions] or the [Manage Guild Expressions] permission. Otherwise, the [Manage Guild /// Expressions] permission is required. @@ -797,14 +719,14 @@ impl PartialGuild { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn edit_emoji( &self, - http: impl AsRef, - emoji_id: impl Into, + http: &Http, + emoji_id: EmojiId, name: &str, + reason: Option<&str>, ) -> Result { - self.id.edit_emoji(http, emoji_id, name).await + self.id.edit_emoji(http, emoji_id, name, reason).await } /// Edits the properties a guild member, such as muting or nicknaming them. Returns the new @@ -820,14 +742,13 @@ impl PartialGuild { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - #[inline] pub async fn edit_member( &self, - cache_http: impl CacheHttp, - user_id: impl Into, + http: &Http, + user_id: UserId, builder: EditMember<'_>, ) -> Result { - self.id.edit_member(cache_http, user_id, builder).await + self.id.edit_member(http, user_id, builder).await } /// Edits the guild's MFA level. Returns the new level on success. @@ -839,7 +760,7 @@ impl PartialGuild { /// Returns [`Error::Http`] if the current user lacks permission. pub async fn edit_mfa_level( &self, - http: impl AsRef, + http: &Http, mfa_level: MfaLevel, audit_log_reason: Option<&str>, ) -> Result { @@ -857,13 +778,13 @@ impl PartialGuild { /// Returns [`Error::Http`] if the current user lacks permission to change their nickname. /// /// [Change Nickname]: Permissions::CHANGE_NICKNAME - #[inline] pub async fn edit_nickname( &self, - http: impl AsRef, + http: &Http, new_nickname: Option<&str>, + reason: Option<&str>, ) -> Result<()> { - self.id.edit_nickname(http, new_nickname).await + self.id.edit_nickname(http, new_nickname, reason).await } /// Edits a role, optionally setting its fields. @@ -876,18 +797,16 @@ impl PartialGuild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] pub async fn edit_role( &self, - cache_http: impl CacheHttp, - role_id: impl Into, + http: &Http, + role_id: RoleId, builder: EditRole<'_>, ) -> Result { - self.id.edit_role(cache_http, role_id, builder).await + self.id.edit_role(http, role_id, builder).await } /// Edits the order of [`Role`]s. Requires the [Manage Roles] permission. @@ -906,14 +825,14 @@ impl PartialGuild { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] pub async fn edit_role_position( &self, - http: impl AsRef, - role_id: impl Into, - position: u16, + http: &Http, + role_id: RoleId, + position: i16, + audit_log_reason: Option<&str>, ) -> Result> { - self.id.edit_role_position(http, role_id, position).await + self.id.edit_role_position(http, role_id, position, audit_log_reason).await } /// Edits a sticker. @@ -948,14 +867,13 @@ impl PartialGuild { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] pub async fn edit_sticker( &self, - cache_http: impl CacheHttp, - sticker_id: impl Into, + http: &Http, + sticker_id: StickerId, builder: EditSticker<'_>, ) -> Result { - self.id.edit_sticker(cache_http, sticker_id, builder).await + self.id.edit_sticker(http, sticker_id, builder).await } /// Edits the guild's welcome screen. @@ -969,10 +887,10 @@ impl PartialGuild { /// [Manage Guild]: Permissions::MANAGE_GUILD pub async fn edit_welcome_screen( &self, - cache_http: impl CacheHttp, + http: &Http, builder: EditGuildWelcomeScreen<'_>, ) -> Result { - self.id.edit_welcome_screen(cache_http, builder).await + self.id.edit_welcome_screen(http, builder).await } /// Edits the guild's widget. @@ -986,10 +904,10 @@ impl PartialGuild { /// [Manage Guild]: Permissions::MANAGE_GUILD pub async fn edit_widget( &self, - cache_http: impl CacheHttp, + http: &Http, builder: EditGuildWidget<'_>, ) -> Result { - self.id.edit_widget(cache_http, builder).await + self.id.edit_widget(http, builder).await } /// Gets a partial amount of guild data by its Id. @@ -998,42 +916,11 @@ impl PartialGuild { /// /// Returns [`Error::Http`] if the current user is not /// in the guild. - #[inline] - pub async fn get( - cache_http: impl CacheHttp, - guild_id: impl Into, - ) -> Result { - guild_id.into().to_partial_guild(cache_http).await - } - - /// Returns which of two [`User`]s has a higher [`Member`] hierarchy. - /// - /// Hierarchy is essentially who has the [`Role`] with the highest [`position`]. - /// - /// Returns [`None`] if at least one of the given users' member instances is not present. - /// Returns [`None`] if the users have the same hierarchy, as neither are greater than the - /// other. - /// - /// If both user IDs are the same, [`None`] is returned. If one of the users is the guild - /// owner, their ID is returned. - /// - /// [`position`]: Role::position - #[cfg(feature = "cache")] - #[inline] - #[deprecated = "Use Cache::guild and Guild::greater_member_hierarchy"] - pub fn greater_member_hierarchy( - &self, - cache: impl AsRef, - lhs_id: impl Into, - rhs_id: impl Into, - ) -> Option { - let cache = cache.as_ref(); - let guild = cache.guild(self.id)?; - guild.greater_member_hierarchy(cache, lhs_id, rhs_id) + pub async fn get(cache_http: impl CacheHttp, guild_id: GuildId) -> Result { + guild_id.to_partial_guild(cache_http).await } /// Calculate a [`Member`]'s permissions in the guild. - #[inline] #[cfg(feature = "cache")] #[must_use] pub fn member_permissions(&self, member: &Member) -> Permissions { @@ -1047,6 +934,32 @@ impl PartialGuild { ) } + /// Gets the highest role a [`Member`] of this Guild has. + /// + /// Returns None if the member has no roles or the member from this guild. + #[must_use] + pub fn member_highest_role(&self, member: &Member) -> Option<&Role> { + Guild::_member_highest_role_in(&self.roles, member) + } + + /// See [`Guild::greater_member_hierarchy`] for more information. + /// + /// Note that unlike [`Guild::greater_member_hierarchy`], this method requires a [`Member`] as + /// member data is not available on a [`PartialGuild`]. + #[must_use] + pub fn greater_member_hierarchy(&self, lhs: &Member, rhs: &Member) -> Option { + let lhs_highest_role = self.member_highest_role(lhs); + let rhs_highest_role = self.member_highest_role(rhs); + + Guild::_greater_member_hierarchy_in( + lhs_highest_role, + rhs_highest_role, + self.owner_id, + lhs, + rhs, + ) + } + /// Calculate a [`PartialMember`]'s permissions in a given channel in a guild. /// /// # Panics @@ -1085,10 +998,9 @@ impl PartialGuild { /// Returns an [`Error::Http`] if the current user is lacking permission. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - #[inline] pub async fn reorder_channels( &self, - http: impl AsRef, + http: &Http, channels: impl IntoIterator, ) -> Result<()> { self.id.reorder_channels(http, channels).await @@ -1105,12 +1017,11 @@ impl PartialGuild { /// # Errors /// /// Returns an [`Error::Http`] if the API returns an error. - #[inline] pub async fn search_members( &self, - http: impl AsRef, + http: &Http, query: &str, - limit: Option, + limit: Option, ) -> Result> { self.id.search_members(http, query, limit).await } @@ -1123,10 +1034,8 @@ impl PartialGuild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to kick members. /// - /// Otherwise will return [`Error::Http`] if the current user does not have permission. + /// Returns [`Error::Http`] if the current user does not have permission. /// /// Can also return an [`Error::Json`] if there is an error deserializing the API response. /// @@ -1134,8 +1043,13 @@ impl PartialGuild { /// [Manage Guild]: Permissions::MANAGE_GUILD /// [`Error::Http`]: crate::error::Error::Http /// [`Error::Json`]: crate::error::Error::Json - pub async fn start_prune(&self, cache_http: impl CacheHttp, days: u8) -> Result { - self.id.start_prune(cache_http.http(), days).await + pub async fn start_prune( + &self, + http: &Http, + days: u8, + reason: Option<&str>, + ) -> Result { + self.id.start_prune(http, days, reason).await } /// Kicks a [`Member`] from the guild. @@ -1147,23 +1061,8 @@ impl PartialGuild { /// Returns [`Error::Http`] if the member cannot be kicked by the current user. /// /// [Kick Members]: Permissions::KICK_MEMBERS - #[inline] - pub async fn kick(&self, http: impl AsRef, user_id: impl Into) -> Result<()> { - self.id.kick(http, user_id).await - } - - /// # Errors - /// - /// In addition to the reasons [`Self::kick`] may return an error, can also return an error if - /// the reason is too long. - #[inline] - pub async fn kick_with_reason( - &self, - http: impl AsRef, - user_id: impl Into, - reason: &str, - ) -> Result<()> { - self.id.kick_with_reason(http, user_id, reason).await + pub async fn kick(&self, http: &Http, user_id: UserId, reason: Option<&str>) -> Result<()> { + self.id.kick(http, user_id, reason).await } /// Returns a formatted URL of the guild's icon, if the guild has an icon. @@ -1183,8 +1082,7 @@ impl PartialGuild { /// # Errors /// /// Returns [`Error::Http`] if the guild is unavailable. - #[inline] - pub async fn emojis(&self, http: impl AsRef) -> Result> { + pub async fn emojis(&self, http: &Http) -> Result> { self.id.emojis(http).await } @@ -1193,8 +1091,7 @@ impl PartialGuild { /// # Errors /// /// Returns [`Error::Http`] if an [`Emoji`] with the given Id does not exist for the guild. - #[inline] - pub async fn emoji(&self, http: impl AsRef, emoji_id: EmojiId) -> Result { + pub async fn emoji(&self, http: &Http, emoji_id: EmojiId) -> Result { self.id.emoji(http, emoji_id).await } @@ -1207,8 +1104,7 @@ impl PartialGuild { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn integrations(&self, http: impl AsRef) -> Result> { + pub async fn integrations(&self, http: &Http) -> Result> { self.id.integrations(http).await } @@ -1221,8 +1117,7 @@ impl PartialGuild { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - #[inline] - pub async fn invites(&self, http: impl AsRef) -> Result> { + pub async fn invites(&self, http: &Http) -> Result> { self.id.invites(http).await } @@ -1234,8 +1129,7 @@ impl PartialGuild { /// /// Returns an [`Error::Http`] if the current user is not in the guild or the access token /// lacks the necessary scope. - #[inline] - pub async fn current_user_member(&self, http: impl AsRef) -> Result { + pub async fn current_user_member(&self, http: &Http) -> Result { self.id.current_user_member(http).await } @@ -1245,8 +1139,7 @@ impl PartialGuild { /// /// Returns [`Error::Http`] if the current user is unable to leave the Guild, or currently is /// not in the guild. - #[inline] - pub async fn leave(&self, http: impl AsRef) -> Result<()> { + pub async fn leave(&self, http: &Http) -> Result<()> { self.id.leave(http).await } @@ -1256,12 +1149,7 @@ impl PartialGuild { /// /// Returns [`Error::Http`] if the member is not in the Guild, or if the Guild is otherwise /// unavailable. - #[inline] - pub async fn member( - &self, - cache_http: impl CacheHttp, - user_id: impl Into, - ) -> Result { + pub async fn member(&self, cache_http: impl CacheHttp, user_id: UserId) -> Result { self.id.member(cache_http, user_id).await } @@ -1275,15 +1163,14 @@ impl PartialGuild { /// # Errors /// /// Returns an [`Error::Http`] if the API returns an error, may also return - /// [`Error::NotInRange`] if the input is not within range. + /// [`ModelError::TooSmall`] or [`ModelError::TooLarge`] if the limit is not within range. /// /// [`User`]: crate::model::user::User - #[inline] pub async fn members( &self, - http: impl AsRef, - limit: Option, - after: impl Into>, + http: &Http, + limit: Option, + after: Option, ) -> Result> { self.id.members(http, limit, after).await } @@ -1298,18 +1185,16 @@ impl PartialGuild { /// currently in a voice channel for this Guild. /// /// [Move Members]: Permissions::MOVE_MEMBERS - #[inline] pub async fn move_member( &self, - cache_http: impl CacheHttp, - user_id: impl Into, - channel_id: impl Into, + http: &Http, + user_id: UserId, + channel_id: ChannelId, ) -> Result { - self.id.move_member(cache_http, user_id, channel_id).await + self.id.move_member(http, user_id, channel_id).await } /// Calculate a [`Member`]'s permissions in a given channel in the guild. - #[inline] #[must_use] pub fn user_permissions_in(&self, channel: &GuildChannel, member: &Member) -> Permissions { Guild::_user_permissions_in( @@ -1322,17 +1207,6 @@ impl PartialGuild { ) } - /// Calculate a [`Role`]'s permissions in a given channel in the guild. - /// - /// # Errors - /// - /// Returns [`Error::Model`] if the [`Role`] or [`Channel`] is not from this [`Guild`]. - #[inline] - #[deprecated = "this function ignores other roles the user may have as well as user-specific permissions; use user_permissions_in instead"] - pub fn role_permissions_in(&self, channel: &GuildChannel, role: &Role) -> Result { - Guild::_role_permissions_in(channel, role, self.id) - } - /// Gets the number of [`Member`]s that would be pruned with the given number of days. /// /// Requires the [Kick Members] permission. @@ -1345,53 +1219,20 @@ impl PartialGuild { /// /// [Kick Members]: Permissions::KICK_MEMBERS /// [`Guild::prune_count`]: crate::model::guild::Guild::prune_count - #[inline] - pub async fn prune_count(&self, http: impl AsRef, days: u8) -> Result { + pub async fn prune_count(&self, http: &Http, days: u8) -> Result { self.id.prune_count(http, days).await } /// Returns the Id of the shard associated with the guild. /// - /// When the cache is enabled this will automatically retrieve the total number of shards. - /// - /// **Note**: When the cache is enabled, this function unlocks the cache to retrieve the total - /// number of shards in use. If you already have the total, consider using [`utils::shard_id`]. - /// - /// [`utils::shard_id`]: crate::utils::shard_id - #[cfg(all(feature = "cache", feature = "utils"))] - #[inline] - #[must_use] - pub fn shard_id(&self, cache: impl AsRef) -> u32 { - self.id.shard_id(cache) - } - - /// Returns the Id of the shard associated with the guild. - /// - /// When the cache is enabled this will automatically retrieve the total number of shards. - /// - /// When the cache is not enabled, the total number of shards being used will need to be - /// passed. - /// - /// # Examples - /// - /// Retrieve the Id of the shard for a guild with Id `81384788765712384`, using 17 shards: - /// - /// ```rust,ignore - /// use serenity::utils; - /// - /// // assumes a `guild` has already been bound - /// - /// assert_eq!(guild.shard_id(17), 7); - /// ``` - #[cfg(all(feature = "utils", not(feature = "cache")))] - #[inline] + /// See the documentation for [`GuildId::shard_id`]. #[must_use] - pub fn shard_id(&self, shard_count: u32) -> u32 { - self.id.shard_id(shard_count) + #[cfg(feature = "utils")] + pub fn shard_id(&self, shard_total: std::num::NonZeroU16) -> u16 { + self.id.shard_id(shard_total) } /// Returns the formatted URL of the guild's splash image, if one exists. - #[inline] #[must_use] pub fn splash_url(&self) -> Option { self.splash.as_ref().map(|splash| cdn!("/splashes/{}/{}.webp?size=4096", self.id, splash)) @@ -1407,11 +1248,10 @@ impl PartialGuild { /// /// [Manage Guild]: Permissions::MANAGE_GUILD /// [`Guild::start_integration_sync`]: crate::model::guild::Guild::start_integration_sync - #[inline] pub async fn start_integration_sync( &self, - http: impl AsRef, - integration_id: impl Into, + http: &Http, + integration_id: IntegrationId, ) -> Result<()> { self.id.start_integration_sync(http, integration_id).await } @@ -1426,9 +1266,8 @@ impl PartialGuild { /// /// [Ban Members]: Permissions::BAN_MEMBERS /// [`Guild::unban`]: crate::model::guild::Guild::unban - #[inline] - pub async fn unban(&self, http: impl AsRef, user_id: impl Into) -> Result<()> { - self.id.unban(http, user_id).await + pub async fn unban(&self, http: &Http, user_id: UserId, reason: Option<&str>) -> Result<()> { + self.id.unban(http, user_id, reason).await } /// Retrieve's the guild's vanity URL. @@ -1441,8 +1280,7 @@ impl PartialGuild { /// /// [Manage Guild]: Permissions::MANAGE_GUILD /// [`Guild::vanity_url`]: crate::model::guild::Guild::vanity_url - #[inline] - pub async fn vanity_url(&self, http: impl AsRef) -> Result { + pub async fn vanity_url(&self, http: &Http) -> Result { self.id.vanity_url(http).await } @@ -1456,8 +1294,7 @@ impl PartialGuild { /// /// [Manage Webhooks]: Permissions::MANAGE_WEBHOOKS /// [`Guild::webhooks`]: crate::model::guild::Guild::webhooks - #[inline] - pub async fn webhooks(&self, http: impl AsRef) -> Result> { + pub async fn webhooks(&self, http: &Http) -> Result> { self.id.webhooks(http).await } @@ -1477,9 +1314,9 @@ impl PartialGuild { /// #[serenity::async_trait] /// #[cfg(all(feature = "cache", feature = "client"))] /// impl EventHandler for Handler { - /// async fn message(&self, context: Context, msg: Message) { + /// async fn message(&self, ctx: Context, msg: Message) { /// if let Some(guild_id) = msg.guild_id { - /// if let Some(guild) = guild_id.to_guild_cached(&context) { + /// if let Some(guild) = guild_id.to_guild_cached(&ctx.cache) { /// if let Some(role) = guild.role_by_name("role_name") { /// println!("Obtained role's reference: {:?}", role); /// } @@ -1488,38 +1325,34 @@ impl PartialGuild { /// } /// } /// ``` - #[inline] #[must_use] pub fn role_by_name(&self, role_name: &str) -> Option<&Role> { - self.roles.values().find(|role| role_name == role.name) + self.roles.iter().find(|role| role_name == &*role.name) } /// Returns a builder which can be awaited to obtain a message or stream of messages in this /// guild. #[cfg(feature = "collector")] - pub fn await_reply(&self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_reply(&self, shard_messenger: ShardMessenger) -> MessageCollector { MessageCollector::new(shard_messenger).guild_id(self.id) } /// Same as [`Self::await_reply`]. #[cfg(feature = "collector")] - pub fn await_replies(&self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_replies(&self, shard_messenger: ShardMessenger) -> MessageCollector { self.await_reply(shard_messenger) } /// Returns a builder which can be awaited to obtain a message or stream of reactions sent in /// this guild. #[cfg(feature = "collector")] - pub fn await_reaction(&self, shard_messenger: impl AsRef) -> ReactionCollector { + pub fn await_reaction(&self, shard_messenger: ShardMessenger) -> ReactionCollector { ReactionCollector::new(shard_messenger).guild_id(self.id) } /// Same as [`Self::await_reaction`]. #[cfg(feature = "collector")] - pub fn await_reactions( - &self, - shard_messenger: impl AsRef, - ) -> ReactionCollector { + pub fn await_reactions(&self, shard_messenger: ShardMessenger) -> ReactionCollector { self.await_reaction(shard_messenger) } @@ -1529,21 +1362,21 @@ impl PartialGuild { /// /// Returns [`Error::Http`] if there is an error in the deserialization, or if the bot issuing /// the request is not in the guild. - pub async fn get_active_threads(&self, http: impl AsRef) -> Result { + pub async fn get_active_threads(&self, http: &Http) -> Result { self.id.get_active_threads(http).await } } // Manual impl needed to insert guild_id into Role's -impl<'de> Deserialize<'de> for PartialGuild { +impl<'de> Deserialize<'de> for PartialGuildGeneratedOriginal { fn deserialize>(deserializer: D) -> StdResult { let mut guild = Self::deserialize(deserializer)?; // calls #[serde(remote)]-generated inherent method - guild.roles.values_mut().for_each(|r| r.guild_id = guild.id); + lending_for_each!(guild.roles.iter_mut(), |r| r.guild_id = guild.id); Ok(guild) } } -impl Serialize for PartialGuild { +impl Serialize for PartialGuildGeneratedOriginal { fn serialize(&self, serializer: S) -> StdResult { Self::serialize(self, serializer) // calls #[serde(remote)]-generated inherent method } @@ -1552,12 +1385,15 @@ impl Serialize for PartialGuild { impl From for PartialGuild { /// Converts this [`Guild`] instance into a [`PartialGuild`] fn from(guild: Guild) -> Self { - Self { + let (premium_progress_bar_enabled, widget_enabled) = + (guild.premium_progress_bar_enabled(), guild.widget_enabled()); + + let mut partial = Self { + __generated_flags: PartialGuildGeneratedFlags::empty(), application_id: guild.application_id, id: guild.id, afk_metadata: guild.afk_metadata, default_message_notifications: guild.default_message_notifications, - widget_enabled: guild.widget_enabled, widget_channel_id: guild.widget_channel_id, emojis: guild.emojis, features: guild.features, @@ -1590,7 +1426,9 @@ impl From for PartialGuild { explicit_content_filter: guild.explicit_content_filter, preferred_locale: guild.preferred_locale, max_stage_video_channel_users: guild.max_stage_video_channel_users, - premium_progress_bar_enabled: guild.premium_progress_bar_enabled, - } + }; + partial.set_premium_progress_bar_enabled(premium_progress_bar_enabled); + partial.set_widget_enabled(widget_enabled); + partial } } diff --git a/src/model/guild/premium_tier.rs b/src/model/guild/premium_tier.rs index cf749dcee44..367d2fae8d2 100644 --- a/src/model/guild/premium_tier.rs +++ b/src/model/guild/premium_tier.rs @@ -4,11 +4,9 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-object-premium-tier). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum PremiumTier { /// Guild has not unlocked any Server Boost perks - #[default] Tier0 = 0, /// Guild has unlocked Server Boost level 1 perks Tier1 = 1, diff --git a/src/model/guild/role.rs b/src/model/guild/role.rs index 28ae8ac3162..6670cef5d00 100644 --- a/src/model/guild/role.rs +++ b/src/model/guild/role.rs @@ -3,11 +3,8 @@ use std::fmt; #[cfg(feature = "model")] use crate::builder::EditRole; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::cache::Cache; #[cfg(feature = "model")] use crate::http::Http; -#[cfg(all(feature = "cache", feature = "model"))] use crate::internal::prelude::*; use crate::model::prelude::*; use crate::model::utils::is_false; @@ -19,8 +16,9 @@ use crate::model::utils::is_false; /// permissions. /// /// [Discord docs](https://discord.com/developers/docs/topics/permissions#role-object). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct Role { /// The Id of the role. Can be used to calculate the role's creation date. @@ -45,7 +43,7 @@ pub struct Role { #[serde(default)] pub mentionable: bool, /// The name of the role. - pub name: String, + pub name: FixedString, /// A set of permissions that the role has been assigned. /// /// See the [`permissions`] module for more information. @@ -56,7 +54,7 @@ pub struct Role { /// position is higher. /// /// The `@everyone` role is usually either `-1` or `0`. - pub position: u16, + pub position: i16, /// The tags this role has. It can be used to determine if this role is a special role in this /// guild such as guild subscriber role, or if the role is linked to an [`Integration`] or a /// bot. @@ -67,7 +65,7 @@ pub struct Role { /// Role icon image hash. pub icon: Option, /// Role unicoded image. - pub unicode_emoji: Option, + pub unicode_emoji: Option, } #[cfg(feature = "model")] @@ -81,9 +79,8 @@ impl Role { /// Returns [`Error::Http`] if the current user lacks permission to delete this role. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] - pub async fn delete(&mut self, http: impl AsRef) -> Result<()> { - http.as_ref().delete_role(self.guild_id, self.id, None).await + pub async fn delete(&mut self, http: &Http, reason: Option<&str>) -> Result<()> { + self.guild_id.delete_role(http, self.id, reason).await } /// Edits a [`Role`], optionally setting its new fields. @@ -99,14 +96,12 @@ impl Role { /// Returns [`Error::Http`] if the current user does not have permission to Manage Roles. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - #[inline] - pub async fn edit(&mut self, http: impl AsRef, builder: EditRole<'_>) -> Result<()> { - *self = self.guild_id.edit_role(http.as_ref(), self.id, builder).await?; + pub async fn edit(&mut self, http: &Http, builder: EditRole<'_>) -> Result<()> { + *self = self.guild_id.edit_role(http, self.id, builder).await?; Ok(()) } /// Check that the role has the given permission. - #[inline] #[must_use] pub fn has_permission(&self, permission: Permissions) -> bool { self.permissions.contains(permission) @@ -117,7 +112,6 @@ impl Role { /// The 'precise' argument is used to check if the role's permissions are precisely equivalent /// to the given permissions. If you need only check that the role has at least the given /// permissions, pass `false`. - #[inline] #[must_use] pub fn has_permissions(&self, permissions: Permissions, precise: bool) -> bool { if precise { @@ -136,21 +130,9 @@ impl fmt::Display for Role { } } -impl Eq for Role {} - -impl Ord for Role { - fn cmp(&self, other: &Role) -> Ordering { - if self.position == other.position { - self.id.cmp(&other.id) - } else { - self.position.cmp(&other.position) - } - } -} - -impl PartialEq for Role { - fn eq(&self, other: &Role) -> bool { - self.id == other.id +impl ExtractKey for Role { + fn extract_key(&self) -> &RoleId { + &self.id } } @@ -160,25 +142,13 @@ impl PartialOrd for Role { } } -#[cfg(feature = "model")] -impl RoleId { - /// Tries to find the [`Role`] by its Id in the cache. - #[cfg(feature = "cache")] - #[deprecated = "Use Guild::roles. This performs a loop over the entire cache!"] - pub fn to_role_cached(self, cache: impl AsRef) -> Option { - for guild_entry in cache.as_ref().guilds.iter() { - let guild = guild_entry.value(); - - if !guild.roles.contains_key(&self) { - continue; - } - - if let Some(role) = guild.roles.get(&self) { - return Some(role.clone()); - } +impl Ord for Role { + fn cmp(&self, other: &Role) -> Ordering { + if self.position == other.position { + self.id.cmp(&other.id) + } else { + self.position.cmp(&other.position) } - - None } } @@ -199,7 +169,8 @@ impl<'a> From<&'a Role> for RoleId { /// The tags of a [`Role`]. /// /// [Discord docs](https://discord.com/developers/docs/topics/permissions#role-object-role-tags-structure). -#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[bool_to_bitflags::bool_to_bitflags] +#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[non_exhaustive] pub struct RoleTags { @@ -249,7 +220,6 @@ mod bool_as_option_unit { Ok(true) } - /// Called by the `simd_json` crate fn visit_unit(self) -> Result { Ok(true) } @@ -258,19 +228,15 @@ mod bool_as_option_unit { #[cfg(test)] mod tests { + use serde_json::json; + use super::RoleTags; - use crate::json::{assert_json, json}; + use crate::model::utils::assert_json; #[test] fn premium_subscriber_role_serde() { - let value = RoleTags { - bot_id: None, - integration_id: None, - premium_subscriber: true, - subscription_listing_id: None, - available_for_purchase: false, - guild_connections: false, - }; + let mut value = RoleTags::default(); + value.set_premium_subscriber(true); assert_json( &value, @@ -280,14 +246,7 @@ mod tests { #[test] fn non_premium_subscriber_role_serde() { - let value = RoleTags { - bot_id: None, - integration_id: None, - premium_subscriber: false, - subscription_listing_id: None, - available_for_purchase: false, - guild_connections: false, - }; + let value = RoleTags::default(); assert_json( &value, diff --git a/src/model/guild/scheduled_event.rs b/src/model/guild/scheduled_event.rs index 19a5499c4f2..6aa978df765 100644 --- a/src/model/guild/scheduled_event.rs +++ b/src/model/guild/scheduled_event.rs @@ -1,3 +1,6 @@ +use nonmax::NonMaxU64; + +use crate::internal::prelude::*; use crate::model::prelude::*; /// Information about a guild scheduled event. @@ -18,9 +21,9 @@ pub struct ScheduledEvent { /// Only `None` for events created before October 25th, 2021. pub creator_id: Option, /// The name of the scheduled event. - pub name: String, + pub name: FixedString, /// The description of the scheduled event, if any. - pub description: Option, + pub description: Option, /// The event's starting time. #[serde(rename = "scheduled_start_time")] pub start_time: Timestamp, @@ -48,7 +51,7 @@ pub struct ScheduledEvent { /// /// Only populated if `with_user_count` is set to true provided when calling /// [`GuildId::scheduled_event`] or [`GuildId::scheduled_events`]. - pub user_count: Option, + pub user_count: Option, /// The hash of the event's cover image, if present. pub image: Option, } @@ -57,7 +60,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-status). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ScheduledEventStatus { Scheduled = 1, @@ -72,7 +74,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ScheduledEventType { StageInstance = 1, @@ -88,7 +89,7 @@ enum_number! { #[non_exhaustive] pub struct ScheduledEventMetadata { #[serde(default)] - pub location: Option, + pub location: Option, } /// [Discord docs](https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-user-object). @@ -108,7 +109,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-privacy-level). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum ScheduledEventPrivacyLevel { GuildOnly = 2, diff --git a/src/model/guild/welcome_screen.rs b/src/model/guild/welcome_screen.rs index f809065d2a0..29d98ffd286 100644 --- a/src/model/guild/welcome_screen.rs +++ b/src/model/guild/welcome_screen.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use crate::internal::prelude::*; use crate::model::id::{ChannelId, EmojiId}; /// Information relating to a guild's welcome screen. @@ -10,11 +11,11 @@ use crate::model::id::{ChannelId, EmojiId}; #[non_exhaustive] pub struct GuildWelcomeScreen { /// The server description shown in the welcome screen. - pub description: Option, + pub description: Option, /// The channels shown in the welcome screen. /// /// **Note**: There can only be only up to 5 channels. - pub welcome_channels: Vec, + pub welcome_channels: FixedArray, } /// A channel shown in the [`GuildWelcomeScreen`]. @@ -27,7 +28,7 @@ pub struct GuildWelcomeChannel { /// The channel Id. pub channel_id: ChannelId, /// The description shown for the channel. - pub description: String, + pub description: FixedString, /// The emoji shown, if there is one. pub emoji: Option, } @@ -38,9 +39,9 @@ impl<'de> Deserialize<'de> for GuildWelcomeChannel { #[derive(Deserialize)] struct Helper { channel_id: ChannelId, - description: String, + description: FixedString, emoji_id: Option, - emoji_name: Option, + emoji_name: Option, } let Helper { channel_id, @@ -95,7 +96,7 @@ impl Serialize for GuildWelcomeChannel { #[non_exhaustive] pub enum GuildWelcomeChannelEmoji { /// A custom emoji. - Custom { id: EmojiId, name: String }, + Custom { id: EmojiId, name: FixedString }, /// A unicode emoji. - Unicode(String), + Unicode(FixedString), } diff --git a/src/model/id.rs b/src/model/id.rs index f3bbf82b27b..d5dccf98107 100644 --- a/src/model/id.rs +++ b/src/model/id.rs @@ -1,7 +1,9 @@ //! A collection of newtypes defining type-strong IDs. use std::fmt; -use std::num::{NonZeroI64, NonZeroU64}; + +use nonmax::NonMaxU64; +use to_arraystring::ToArrayString; use super::Timestamp; @@ -9,8 +11,8 @@ macro_rules! newtype_display_impl { ($name:ident) => { impl fmt::Display for $name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let inner = self.0; - fmt::Display::fmt(&inner, f) + // See comment in Self::get for block. + fmt::Display::fmt(&{ self.0 }, f) } } }; @@ -19,37 +21,48 @@ macro_rules! newtype_display_impl { macro_rules! forward_fromstr_impl { ($name:ident) => { impl std::str::FromStr for $name { - type Err = ::Err; + type Err = ParseIdError; fn from_str(s: &str) -> Result { - Ok(Self(s.parse()?)) + s.parse().map(Self).map_err(ParseIdError) } } }; } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ParseIdError(nonmax::ParseIntError); + +impl std::error::Error for ParseIdError {} +impl std::fmt::Display for ParseIdError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + macro_rules! id_u64 { ($($name:ident;)*) => { $( impl $name { #[doc = concat!("Creates a new ", stringify!($name), " from a u64.")] /// # Panics - /// Panics if `id` is zero. - #[inline] + /// Panics if `id` is u64::MAX. #[must_use] #[track_caller] pub const fn new(id: u64) -> Self { - match NonZeroU64::new(id) { + match NonMaxU64::new(id) { Some(inner) => Self(inner), - None => panic!(concat!("Attempted to call ", stringify!($name), "::new with invalid (0) value")) + None => panic!(concat!("Attempted to call ", stringify!($name), "::new with invalid (u64::MAX) value")) } } /// Retrieves the inner `id` as a [`u64`]. - #[inline] #[must_use] pub const fn get(self) -> u64 { - self.0.get() + // By wrapping `self.0` in a block, it forces a Copy, as NonMax::get takes &self. + // If removed, the compiler will auto-ref to `&self.0`, which is a + // reference to a packed field and therefore errors. + {self.0}.get() } #[doc = concat!("Retrieves the time that the ", stringify!($name), " was created.")] @@ -59,15 +72,6 @@ macro_rules! id_u64 { } } - newtype_display_impl!($name); - forward_fromstr_impl!($name); - - impl Default for $name { - fn default() -> Self { - Self(NonZeroU64::MIN) - } - } - // This is a hack so functions can accept iterators that either: // 1. return the id itself (e.g: `MessageId`) // 2. return a reference to it (`&MessageId`). @@ -89,30 +93,12 @@ macro_rules! id_u64 { } } - impl From for $name { - fn from(id: NonZeroU64) -> $name { - $name(id) - } - } - impl PartialEq for $name { fn eq(&self, u: &u64) -> bool { self.get() == *u } } - impl From<$name> for NonZeroU64 { - fn from(id: $name) -> NonZeroU64 { - id.0 - } - } - - impl From<$name> for NonZeroI64 { - fn from(id: $name) -> NonZeroI64 { - unsafe {NonZeroI64::new_unchecked(id.get() as i64)} - } - } - impl From<$name> for u64 { fn from(id: $name) -> u64 { id.get() @@ -125,6 +111,16 @@ macro_rules! id_u64 { } } + newtype_display_impl!($name); + forward_fromstr_impl!($name); + + impl ToArrayString for $name { + type ArrayString = ::ArrayString; + fn to_arraystring(self) -> Self::ArrayString { + self.get().to_arraystring() + } + } + #[cfg(feature = "typesize")] impl typesize::TypeSize for $name {} )* @@ -133,134 +129,180 @@ macro_rules! id_u64 { /// An identifier for an Application. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct ApplicationId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct ApplicationId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a Channel #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct ChannelId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct ChannelId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for an Emoji #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct EmojiId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct EmojiId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for an unspecific entity. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct GenericId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct GenericId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a Guild #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct GuildId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct GuildId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for an Integration #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct IntegrationId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct IntegrationId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a Message #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct MessageId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct MessageId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a Role #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct RoleId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct RoleId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for an auto moderation rule #[repr(packed)] #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] -pub struct RuleId(#[serde(with = "snowflake")] NonZeroU64); +pub struct RuleId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a Scheduled Event #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct ScheduledEventId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct ScheduledEventId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a User #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct UserId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct UserId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a [`Webhook`][super::webhook::Webhook] #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct WebhookId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct WebhookId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for an audit log entry. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct AuditLogEntryId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct AuditLogEntryId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for an attachment. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct AttachmentId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct AttachmentId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a sticker. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct StickerId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct StickerId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a sticker pack. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct StickerPackId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct StickerPackId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a sticker pack banner. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct StickerPackBannerId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct StickerPackBannerId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a SKU. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct SkuId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct SkuId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for an interaction. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct InteractionId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct InteractionId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a slash command. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct CommandId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct CommandId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a slash command permission Id. Can contain /// a [`RoleId`] or [`UserId`]. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct CommandPermissionId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct CommandPermissionId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a slash command version Id. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct CommandVersionId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct CommandVersionId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a slash command target Id. Can contain /// a [`UserId`] or [`MessageId`]. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct TargetId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct TargetId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a stage channel instance. #[repr(packed)] -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub struct StageInstanceId(#[serde(with = "snowflake")] NonZeroU64); +#[derive( + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct StageInstanceId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for a forum tag. #[repr(packed)] #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] -pub struct ForumTagId(#[serde(with = "snowflake")] NonZeroU64); +pub struct ForumTagId(#[serde(with = "snowflake")] NonMaxU64); /// An identifier for an entitlement. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] -pub struct EntitlementId(#[serde(with = "snowflake")] pub NonZeroU64); +pub struct EntitlementId(#[serde(with = "snowflake")] pub NonMaxU64); id_u64! { AttachmentId; @@ -296,16 +338,16 @@ id_u64! { /// This identifier is special, it simply models internal IDs for type safety, /// and therefore cannot be [`Serialize`]d or [`Deserialize`]d. #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub struct ShardId(pub u32); +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct ShardId(pub u16); impl ShardId { - /// Retrieves the value as a [`u32`]. + /// Retrieves the value as a [`u16`]. /// /// This is not a [`u64`] as [`ShardId`]s are not a discord concept and are simply used for /// internal type safety. #[must_use] - pub fn get(self) -> u32 { + pub fn get(self) -> u16 { self.0 } } @@ -320,7 +362,7 @@ newtype_display_impl!(ShardId); #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] #[repr(packed)] -pub struct AnswerId(u8); +pub struct AnswerId(nonmax::NonMaxU8); impl AnswerId { /// Retrieves the value as a [`u64`]. @@ -328,7 +370,7 @@ impl AnswerId { /// Keep in mind that this is **not a snowflake** and the values are subject to change. #[must_use] pub fn get(self) -> u64 { - self.0.into() + { self.0 }.get().into() } } @@ -337,27 +379,27 @@ forward_fromstr_impl!(AnswerId); mod snowflake { use std::fmt; - use std::num::NonZeroU64; + use nonmax::NonMaxU64; use serde::de::{Error, Visitor}; use serde::{Deserializer, Serializer}; - pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { deserializer.deserialize_any(SnowflakeVisitor) } #[allow(clippy::trivially_copy_pass_by_ref)] - pub fn serialize(id: &NonZeroU64, serializer: S) -> Result { + pub fn serialize(id: &NonMaxU64, serializer: S) -> Result { serializer.collect_str(&id.get()) } struct SnowflakeVisitor; impl<'de> Visitor<'de> for SnowflakeVisitor { - type Value = NonZeroU64; + type Value = NonMaxU64; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("a non-zero string or integer snowflake") + formatter.write_str("a string or integer snowflake that is not u64::MAX") } // Called by formats like TOML. @@ -366,7 +408,7 @@ mod snowflake { } fn visit_u64(self, value: u64) -> Result { - NonZeroU64::new(value).ok_or_else(|| Error::custom("invalid value, expected non-zero")) + NonMaxU64::new(value).ok_or_else(|| Error::custom("invalid value, expected non-max")) } fn visit_str(self, value: &str) -> Result { @@ -377,7 +419,7 @@ mod snowflake { #[cfg(test)] mod tests { - use std::num::NonZeroU64; + use nonmax::NonMaxU64; use super::GuildId; @@ -392,14 +434,15 @@ mod tests { #[test] fn test_id_serde() { use serde::{Deserialize, Serialize}; + use serde_json::json; use super::snowflake; - use crate::json::{assert_json, json}; + use crate::model::utils::assert_json; #[derive(Debug, PartialEq, Deserialize, Serialize)] struct S { #[serde(with = "snowflake")] - id: NonZeroU64, + id: NonMaxU64, } #[derive(Debug, PartialEq, Deserialize, Serialize)] @@ -411,7 +454,7 @@ mod tests { assert_json(&id, json!("175928847299117063")); let s = S { - id: NonZeroU64::new(17_5928_8472_9911_7063).unwrap(), + id: NonMaxU64::new(17_5928_8472_9911_7063).unwrap(), }; assert_json(&s, json!({"id": "175928847299117063"})); diff --git a/src/model/invite.rs b/src/model/invite.rs index 1b6921007f6..ed4100c360e 100644 --- a/src/model/invite.rs +++ b/src/model/invite.rs @@ -1,13 +1,12 @@ //! Models for server and channel invites. +use nonmax::NonMaxU64; + use super::prelude::*; #[cfg(feature = "model")] use crate::builder::CreateInvite; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::cache::Cache; -#[cfg(feature = "model")] -use crate::http::{CacheHttp, Http}; #[cfg(feature = "model")] +use crate::http::Http; use crate::internal::prelude::*; /// Information about an invite code. @@ -19,14 +18,14 @@ use crate::internal::prelude::*; #[non_exhaustive] pub struct Invite { /// The approximate number of [`Member`]s in the related [`Guild`]. - pub approximate_member_count: Option, + pub approximate_member_count: Option, /// The approximate number of [`Member`]s with an active session in the related [`Guild`]. /// /// An active session is defined as an open, heartbeating WebSocket connection. /// These include [invisible][`OnlineStatus::Invisible`] members. - pub approximate_presence_count: Option, + pub approximate_presence_count: Option, /// The unique code for the invite. - pub code: String, + pub code: FixedString, /// A representation of the minimal amount of information needed about the [`GuildChannel`] /// being invited to. pub channel: InviteChannel, @@ -69,17 +68,15 @@ impl Invite { /// /// # Errors /// - /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user - /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE - #[inline] pub async fn create( - cache_http: impl CacheHttp, - channel_id: impl Into, + http: &Http, + channel_id: ChannelId, builder: CreateInvite<'_>, ) -> Result { - channel_id.into().create_invite(cache_http, builder).await + channel_id.create_invite(http, builder).await } /// Deletes the invite. @@ -88,27 +85,13 @@ impl Invite { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have the required [permission]. - /// - /// Otherwise may return [`Error::Http`] if permissions are lacking, or if the invite is + /// Returns [`Error::Http`] if the current user lacks permission or if the invite is /// invalid. /// /// [Manage Guild]: Permissions::MANAGE_GUILD /// [permission]: super::permissions - pub async fn delete(&self, cache_http: impl CacheHttp) -> Result { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - crate::utils::user_has_perms_cache( - cache, - self.channel.id, - Permissions::MANAGE_GUILD, - )?; - } - } - - cache_http.http().as_ref().delete_invite(&self.code, None).await + pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result { + http.delete_invite(&self.code, reason).await } /// Gets information about an invite. @@ -128,7 +111,7 @@ impl Invite { /// May return an [`Error::Http`] if the invite is invalid. Can also return an [`Error::Json`] /// if there is an error deserializing the API response. pub async fn get( - http: impl AsRef, + http: &Http, code: &str, member_counts: bool, expiration: bool, @@ -141,7 +124,7 @@ impl Invite { invite = crate::utils::parse_invite(invite); } - http.as_ref().get_invite(invite, member_counts, expiration, event_id).await + http.get_invite(invite, member_counts, expiration, event_id).await } /// Returns a URL to use for the invite. @@ -151,7 +134,7 @@ impl Invite { /// Retrieve the URL for an invite with the code `WxZumR`: /// /// ```rust - /// # use serenity::json::{json, from_value}; + /// # use serde_json::{json, from_value}; /// # use serenity::model::prelude::*; /// # /// # fn main() { @@ -199,7 +182,7 @@ impl Invite { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct InviteChannel { pub id: ChannelId, - pub name: String, + pub name: FixedString, #[serde(rename = "type")] pub kind: ChannelType, } @@ -211,58 +194,27 @@ pub struct InviteChannel { #[non_exhaustive] pub struct InviteGuild { pub id: GuildId, - pub name: String, + pub name: FixedString, pub splash: Option, pub banner: Option, - pub description: Option, + pub description: Option, pub icon: Option, - pub features: Vec, + pub features: FixedArray, pub verification_level: VerificationLevel, - pub vanity_url_code: Option, + pub vanity_url_code: Option, pub nsfw_level: NsfwLevel, - pub premium_subscription_count: Option, + pub premium_subscription_count: Option, } #[cfg(feature = "model")] impl InviteGuild { /// Returns the Id of the shard associated with the guild. /// - /// When the cache is enabled this will automatically retrieve the total number of shards. - /// - /// **Note**: When the cache is enabled, this function unlocks the cache to retrieve the total - /// number of shards in use. If you already have the total, consider using [`utils::shard_id`]. - /// - /// [`utils::shard_id`]: crate::utils::shard_id - #[cfg(all(feature = "cache", feature = "utils"))] - #[inline] - #[must_use] - pub fn shard_id(&self, cache: impl AsRef) -> u32 { - self.id.shard_id(&cache) - } - - /// Returns the Id of the shard associated with the guild. - /// - /// When the cache is enabled this will automatically retrieve the total number of shards. - /// - /// When the cache is not enabled, the total number of shards being used will need to be - /// passed. - /// - /// # Examples - /// - /// Retrieve the Id of the shard for a guild with Id `81384788765712384`, using 17 shards: - /// - /// ```rust,ignore - /// use serenity::utils; - /// - /// // assumes a `guild` has already been bound - /// - /// assert_eq!(guild.shard_id(17), 7); - /// ``` - #[cfg(all(feature = "utils", not(feature = "cache")))] - #[inline] + /// See the documentation for [`GuildId::shard_id`]. #[must_use] - pub fn shard_id(&self, shard_count: u32) -> u32 { - self.id.shard_id(shard_count) + #[cfg(feature = "utils")] + pub fn shard_id(&self, shard_total: std::num::NonZeroU16) -> u16 { + self.id.shard_id(shard_total) } } @@ -280,7 +232,7 @@ pub struct RichInvite { /// invited to. pub channel: InviteChannel, /// The unique code for the invite. - pub code: String, + pub code: FixedString, /// When the invite was created. pub created_at: Timestamp, /// A representation of the minimal amount of information needed about the [`Guild`] being @@ -314,24 +266,12 @@ impl RichInvite { /// /// # Errors /// - /// If the `cache` feature is enabled, then this returns a [`ModelError::InvalidPermissions`] - /// if the current user does not have the required [permission]. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD /// [permission]: super::permissions - pub async fn delete(&self, cache_http: impl CacheHttp) -> Result { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - crate::utils::user_has_perms_cache( - cache, - self.channel.id, - Permissions::MANAGE_GUILD, - )?; - } - } - - cache_http.http().as_ref().delete_invite(&self.code, None).await + pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result { + http.delete_invite(&self.code, reason).await } /// Returns a URL to use for the invite. @@ -341,7 +281,7 @@ impl RichInvite { /// Retrieve the URL for an invite with the code `WxZumR`: /// /// ```rust - /// # use serenity::json::{json, from_value}; + /// # use serde_json::{json, from_value}; /// # use serenity::model::prelude::*; /// # /// # fn main() { @@ -392,13 +332,13 @@ impl RichInvite { #[non_exhaustive] pub struct InviteStageInstance { /// The members speaking in the Stage - pub members: Vec, + pub members: FixedArray, /// The number of users in the Stage pub participant_count: u64, /// The number of users speaking in the Stage pub speaker_count: u64, /// The topic of the Stage instance (1-120 characters) - pub topic: String, + pub topic: FixedString, } enum_number! { @@ -407,7 +347,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/invite#invite-object-invite-target-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum InviteTargetType { Stream = 1, diff --git a/src/model/mention.rs b/src/model/mention.rs index 543f64b72aa..824aab9034a 100644 --- a/src/model/mention.rs +++ b/src/model/mention.rs @@ -4,6 +4,8 @@ use std::fmt; #[cfg(all(feature = "model", feature = "utils"))] use std::str::FromStr; +use to_arraystring::ToArrayString; + use super::prelude::*; #[cfg(all(feature = "model", feature = "utils"))] use crate::utils; @@ -41,7 +43,7 @@ pub trait Mentionable { /// member = member.mention(), /// rules = rules_channel.mention(), /// )); - /// to_channel.id.send_message(ctx, builder).await?; + /// to_channel.id.send_message(&ctx.http, builder).await?; /// Ok(()) /// } /// # } @@ -91,7 +93,6 @@ pub enum Mention { macro_rules! mention { ($i:ident: $($t:ty, $e:expr;)*) => {$( impl From<$t> for Mention { - #[inline(always)] fn from($i: $t) -> Self { $e } @@ -107,11 +108,26 @@ mention!(value: impl fmt::Display for Mention { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - Mention::Channel(id) => f.write_fmt(format_args!("<#{id}>")), - Mention::Role(id) => f.write_fmt(format_args!("<@&{id}>")), - Mention::User(id) => f.write_fmt(format_args!("<@{id}>")), - } + f.write_str(&self.to_arraystring()) + } +} + +impl ToArrayString for Mention { + type ArrayString = arrayvec::ArrayString<{ 20 + 4 }>; + + fn to_arraystring(self) -> Self::ArrayString { + let (prefix, id) = match self { + Self::Channel(id) => ("<#", id.get()), + Self::Role(id) => ("<@&", id.get()), + Self::User(id) => ("<@", id.get()), + }; + + let mut out = Self::ArrayString::new(); + out.push_str(prefix); + out.push_str(&id.to_arraystring()); + out.push('>'); + + out } } @@ -162,7 +178,6 @@ where macro_rules! mentionable { ($i:ident: $t:ty, $e:expr) => { impl Mentionable for $t { - #[inline(always)] fn mention(&self) -> Mention { let $i = self; $e.into() @@ -188,14 +203,11 @@ mod test { #[test] fn test_mention() { + let role = Role::default(); let channel = Channel::Guild(GuildChannel { id: ChannelId::new(4), ..Default::default() }); - let role = Role { - id: RoleId::new(2), - ..Default::default() - }; let user = User { id: UserId::new(6), ..Default::default() @@ -209,8 +221,8 @@ mod test { #[cfg(feature = "model")] assert_eq!(channel.mention().to_string(), "<#4>"); assert_eq!(member.mention().to_string(), "<@6>"); - assert_eq!(role.mention().to_string(), "<@&2>"); - assert_eq!(role.id.mention().to_string(), "<@&2>"); + assert_eq!(role.mention().to_string(), "<@&0>"); + assert_eq!(role.id.mention().to_string(), "<@&0>"); assert_eq!(user.mention().to_string(), "<@6>"); assert_eq!(user.id.mention().to_string(), "<@6>"); } diff --git a/src/model/misc.rs b/src/model/misc.rs index e5d6d8c2a78..ef2d66858b1 100644 --- a/src/model/misc.rs +++ b/src/model/misc.rs @@ -11,12 +11,13 @@ use std::str::FromStr; use arrayvec::ArrayString; use super::prelude::*; +use crate::internal::prelude::*; #[cfg(all(feature = "model", any(feature = "cache", feature = "utils")))] use crate::utils; /// Hides the implementation detail of ImageHash as an enum. #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, Hash, PartialEq, Eq)] enum ImageHashInner { Normal { hash: [u8; 16], is_animated: bool }, Clyde, @@ -36,7 +37,7 @@ enum ImageHashInner { /// ``` #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, Hash, PartialEq, Eq)] pub struct ImageHash(ImageHashInner); impl ImageHash { @@ -170,7 +171,7 @@ pub struct EmojiIdentifier { pub id: EmojiId, /// The name of the emoji. It must be at least 2 characters long and can only contain /// alphanumeric characters and underscores. - pub name: String, + pub name: FixedString, } #[cfg(all(feature = "model", feature = "utils"))] @@ -236,17 +237,17 @@ impl FromStr for EmojiIdentifier { #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct Incident { - pub created_at: String, - pub id: String, - pub impact: String, - pub incident_updates: Vec, - pub monitoring_at: Option, - pub name: String, - pub page_id: String, - pub resolved_at: Option, - pub shortlink: String, - pub status: String, - pub updated_at: String, + pub created_at: FixedString, + pub id: FixedString, + pub impact: FixedString, + pub incident_updates: FixedArray, + pub monitoring_at: Option, + pub name: FixedString, + pub page_id: FixedString, + pub resolved_at: Option, + pub shortlink: FixedString, + pub status: FixedString, + pub updated_at: FixedString, } /// An update to an incident from the Discord status page. @@ -257,13 +258,13 @@ pub struct Incident { #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct IncidentUpdate { - pub body: String, - pub created_at: String, - pub display_at: String, - pub id: String, - pub incident_id: String, - pub status: String, - pub updated_at: String, + pub body: FixedString, + pub created_at: FixedString, + pub display_at: FixedString, + pub id: FixedString, + pub incident_id: FixedString, + pub status: FixedString, + pub updated_at: FixedString, } /// A Discord status maintenance message. This can be either for active maintenances or for @@ -273,19 +274,19 @@ pub struct IncidentUpdate { #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct Maintenance { - pub created_at: String, - pub id: String, - pub impact: String, - pub incident_updates: Vec, - pub monitoring_at: Option, - pub name: String, - pub page_id: String, - pub resolved_at: Option, - pub scheduled_for: String, - pub scheduled_until: String, - pub shortlink: String, - pub status: String, - pub updated_at: String, + pub created_at: FixedString, + pub id: FixedString, + pub impact: FixedString, + pub incident_updates: FixedArray, + pub monitoring_at: Option, + pub name: FixedString, + pub page_id: FixedString, + pub resolved_at: Option, + pub scheduled_for: FixedString, + pub scheduled_until: FixedString, + pub shortlink: FixedString, + pub status: FixedString, + pub updated_at: FixedString, } #[cfg(test)] diff --git a/src/model/mod.rs b/src/model/mod.rs index 8870281759b..808b04212d3 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -19,6 +19,8 @@ #[macro_use] mod utils; +#[cfg(test)] +pub(crate) use utils::assert_json; pub mod application; pub mod channel; @@ -62,7 +64,6 @@ pub use self::timestamp::Timestamp; pub mod prelude { pub(crate) use std::collections::HashMap; - pub(crate) use serde::de::Visitor; pub(crate) use serde::{Deserialize, Deserializer}; pub use super::guild::automod::EventType as AutomodEventType; diff --git a/src/model/monetization.rs b/src/model/monetization.rs index 8b6cc2332d3..ff500abe446 100644 --- a/src/model/monetization.rs +++ b/src/model/monetization.rs @@ -25,7 +25,6 @@ enum_number! { /// /// [Discord docs](https://discord.com/developers/docs/monetization/skus#sku-object-sku-types). #[derive(Clone, Debug, Serialize, Deserialize)] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum SkuKind { /// Represents a recurring subscription. @@ -88,7 +87,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/monetization/entitlements#entitlement-object-entitlement-types). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Debug, Serialize, Deserialize)] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum EntitlementKind { /// Entitlement was purchased as an app subscription. diff --git a/src/model/permissions.rs b/src/model/permissions.rs index b4fa0248f8d..b596695484f 100644 --- a/src/model/permissions.rs +++ b/src/model/permissions.rs @@ -322,8 +322,6 @@ bitflags::bitflags! { /// Allows for editing and deleting emojis, stickers, and soundboard sounds created by all /// users. const MANAGE_GUILD_EXPRESSIONS = 1 << 30; - #[deprecated = "use `Permissions::MANAGE_GUILD_EXPRESSIONS` instead"] - const MANAGE_EMOJIS_AND_STICKERS = 1 << 30; /// Allows members to use application commands, including slash commands and context menu /// commands. const USE_APPLICATION_COMMANDS = 1 << 31; @@ -382,7 +380,6 @@ generate_get_permission_names! { create_public_threads: "Create Public Threads", deafen_members: "Deafen Members", embed_links: "Embed Links", - external_emojis: "Use External Emojis", kick_members: "Kick Members", manage_channels: "Manage Channels", manage_events: "Manage Events", @@ -571,15 +568,6 @@ impl Permissions { self.contains(Self::EMBED_LINKS) } - /// Shorthand for checking that the set of permissions contains the [Use External Emojis] - /// permission. - /// - /// [Use External Emojis]: Self::USE_EXTERNAL_EMOJIS - #[must_use] - pub const fn external_emojis(self) -> bool { - self.contains(Self::USE_EXTERNAL_EMOJIS) - } - /// Shorthand for checking that the set of permissions contains the [Kick Members] permission. /// /// [Kick Members]: Self::KICK_MEMBERS @@ -597,13 +585,6 @@ impl Permissions { self.contains(Self::MANAGE_CHANNELS) } - #[deprecated = "use `manage_guild_expressions` instead"] - #[must_use] - pub const fn manage_emojis_and_stickers(self) -> bool { - #[allow(deprecated)] - self.contains(Self::MANAGE_EMOJIS_AND_STICKERS) - } - /// Shorthand for checking that the set of permissions contains the [Manage Events] permission. /// /// [Manage Events]: Self::MANAGE_EVENTS @@ -853,8 +834,10 @@ impl fmt::Display for Permissions { #[cfg(test)] mod tests { + use serde_json::json; + use super::*; - use crate::json::{assert_json, json}; + use crate::model::utils::assert_json; #[test] fn permissions_serde() { diff --git a/src/model/sticker.rs b/src/model/sticker.rs index 5134f61cac7..48e3e5e5bc9 100644 --- a/src/model/sticker.rs +++ b/src/model/sticker.rs @@ -1,61 +1,21 @@ #[cfg(feature = "model")] use crate::builder::EditSticker; #[cfg(feature = "model")] -use crate::http::{CacheHttp, Http}; -#[cfg(feature = "model")] +use crate::http::Http; use crate::internal::prelude::*; use crate::model::prelude::*; use crate::model::utils::comma_separated_string; #[cfg(feature = "model")] impl StickerId { - /// Delete a guild sticker. - /// - /// **Note**: If the sticker was created by the current user, requires either the [Create Guild - /// Expressions] or the [Manage Guild Expressions] permission. Otherwise, the [Manage Guild - /// Expressions] permission is required. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - /// - /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS - /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[deprecated = "use `GuildId::delete_sticker` instead"] - pub async fn delete(self, http: impl AsRef, guild_id: impl Into) -> Result<()> { - guild_id.into().delete_sticker(http, self).await - } - /// Requests the sticker via the REST API to get a [`Sticker`] with all details. /// /// # Errors /// /// Returns [`Error::Http`] if a [`Sticker`] with that [`StickerId`] does not exist, or is /// otherwise unavailable. - pub async fn to_sticker(self, http: impl AsRef) -> Result { - http.as_ref().get_sticker(self).await - } - - /// Edits the sticker. - /// - /// **Note**: If the sticker was created by the current user, requires either the [Create Guild - /// Expressions] or the [Manage Guild Expressions] permission. Otherwise, the [Manage Guild - /// Expressions] permission is required. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - /// - /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS - /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[deprecated = "use `GuildId::edit_sticker` instead"] - pub async fn edit( - self, - cache_http: impl CacheHttp, - guild_id: impl Into, - builder: EditSticker<'_>, - ) -> Result { - guild_id.into().edit_sticker(cache_http, self, builder).await + pub async fn to_sticker(self, http: &Http) -> Result { + http.get_sticker(self).await } } @@ -69,7 +29,7 @@ pub struct StickerItem { /// The unique ID given to this sticker. pub id: StickerId, /// The name of the sticker. - pub name: String, + pub name: FixedString, /// The type of sticker format. pub format_type: StickerFormatType, } @@ -82,15 +42,13 @@ impl StickerItem { /// /// Returns [`Error::Http`] if a [`Sticker`] with that [`StickerId`] does /// not exist, or is otherwise unavailable. - #[inline] - pub async fn to_sticker(&self, http: impl AsRef) -> Result { + pub async fn to_sticker(&self, http: &Http) -> Result { self.id.to_sticker(http).await } /// Retrieves the URL to the sticker image. /// /// **Note**: This will only be `None` if the format_type is unknown. - #[inline] #[must_use] pub fn image_url(&self) -> Option { sticker_url(self.id, self.format_type) @@ -108,15 +66,15 @@ pub struct StickerPack { /// The unique ID given to this sticker sticker pack. pub id: StickerPackId, /// The stickers in the pack - pub stickers: Vec, + pub stickers: FixedArray, /// The name of the sticker pack - pub name: String, + pub name: FixedString, /// The unique ID given to the pack's SKU. pub sku_id: SkuId, /// ID of a sticker in the pack which is shown as the pack's icon. pub cover_sticker_id: Option, /// Description of the sticker pack. - pub description: String, + pub description: FixedString, /// The unique ID given to the sticker pack's banner image. pub banner_asset_id: StickerPackBannerId, } @@ -152,13 +110,13 @@ pub struct Sticker { /// The unique ID of the pack the sticker is from. pub pack_id: Option, /// The name of the sticker. - pub name: String, + pub name: FixedString, /// Description of the sticker - pub description: Option, + pub description: Option, /// For guild stickers, the Discord name of a unicode emoji representing the sticker's /// expression. For standard stickers, a list of related expressions. #[serde(with = "comma_separated_string")] - pub tags: Vec, + pub tags: FixedArray, /// The type of sticker. #[serde(rename = "type")] pub kind: StickerType, @@ -193,10 +151,9 @@ impl Sticker { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] - pub async fn delete(&self, http: impl AsRef) -> Result<()> { + pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result<()> { if let Some(guild_id) = self.guild_id { - guild_id.delete_sticker(http, self.id).await + guild_id.delete_sticker(http, self.id, reason).await } else { Err(Error::Model(ModelError::DeleteNitroSticker)) } @@ -233,14 +190,9 @@ impl Sticker { /// /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS - #[inline] - pub async fn edit( - &mut self, - cache_http: impl CacheHttp, - builder: EditSticker<'_>, - ) -> Result<()> { + pub async fn edit(&mut self, http: &Http, builder: EditSticker<'_>) -> Result<()> { if let Some(guild_id) = self.guild_id { - *self = guild_id.edit_sticker(cache_http, self.id, builder).await?; + *self = guild_id.edit_sticker(http, self.id, builder).await?; Ok(()) } else { Err(Error::Model(ModelError::DeleteNitroSticker)) @@ -250,20 +202,24 @@ impl Sticker { /// Retrieves the URL to the sticker image. /// /// **Note**: This will only be `None` if the format_type is unknown. - #[inline] #[must_use] pub fn image_url(&self) -> Option { sticker_url(self.id, self.format_type) } } +impl ExtractKey for Sticker { + fn extract_key(&self) -> &StickerId { + &self.id + } +} + enum_number! { /// Differentiates between sticker types. /// /// [Discord docs](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum StickerType { /// An official sticker in a pack, part of Nitro or in a removed purchasable pack. @@ -280,7 +236,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum StickerFormatType { /// A PNG format sticker. @@ -301,7 +256,7 @@ fn sticker_url(sticker_id: StickerId, sticker_format_type: StickerFormatType) -> StickerFormatType::Png | StickerFormatType::Apng => "png", StickerFormatType::Lottie => "json", StickerFormatType::Gif => "gif", - StickerFormatType::Unknown(_) => return None, + StickerFormatType(_) => return None, }; Some(cdn!("/stickers/{}.{}", sticker_id, ext)) diff --git a/src/model/user.rs b/src/model/user.rs index 7b9d6ba882e..3493e3b0bd8 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -10,20 +10,15 @@ use serde::{Deserialize, Serialize}; use super::prelude::*; #[cfg(feature = "model")] -use crate::builder::{Builder, CreateMessage, EditProfile}; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::cache::{Cache, UserRef}; +use crate::builder::{CreateMessage, EditProfile}; #[cfg(feature = "collector")] use crate::collector::{MessageCollector, ReactionCollector}; #[cfg(feature = "collector")] use crate::gateway::ShardMessenger; #[cfg(feature = "model")] -use crate::http::CacheHttp; -#[cfg(feature = "model")] +use crate::http::{CacheHttp, Http}; use crate::internal::prelude::*; #[cfg(feature = "model")] -use crate::json::json; -#[cfg(feature = "model")] use crate::model::utils::avatar_url; /// Used with `#[serde(with|deserialize_with|serialize_with)]` @@ -179,8 +174,8 @@ impl CurrentUser { /// /// Returns an [`Error::Http`] if an invalid value is set. May also return an [`Error::Json`] /// if there is an error in deserializing the API response. - pub async fn edit(&mut self, cache_http: impl CacheHttp, builder: EditProfile) -> Result<()> { - *self = builder.execute(cache_http, ()).await?; + pub async fn edit(&mut self, http: &Http, builder: EditProfile<'_>) -> Result<()> { + *self = builder.execute(http).await?; Ok(()) } } @@ -209,8 +204,8 @@ pub enum OnlineStatus { impl OnlineStatus { #[must_use] - pub fn name(&self) -> &str { - match *self { + pub fn name(self) -> &'static str { + match self { OnlineStatus::DoNotDisturb => "dnd", OnlineStatus::Idle => "idle", OnlineStatus::Invisible => "invisible", @@ -224,8 +219,9 @@ impl OnlineStatus { /// /// [Discord docs](https://discord.com/developers/docs/resources/user#user-object), existence of /// additional partial member field documented [here](https://discord.com/developers/docs/topics/gateway-events#message-create). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct User { /// The unique Id of the user. Can be used to calculate the account's creation date. @@ -234,7 +230,7 @@ pub struct User { /// change if the username+discriminator pair becomes non-unique. Unless the account has /// migrated to a next generation username, which does not have a discriminant. #[serde(rename = "username")] - pub name: String, + pub name: FixedString, /// The account's discriminator to differentiate the user from others with /// the same [`Self::name`]. The name+discriminator pair is always unique. /// If the discriminator is not present, then this is a next generation username @@ -243,7 +239,7 @@ pub struct User { pub discriminator: Option, /// The account's display name, if it is set. /// For bots this is the application name. - pub global_name: Option, + pub global_name: Option>, /// Optional avatar hash. pub avatar: Option, /// Indicator of whether the user is a bot. @@ -267,7 +263,7 @@ pub struct User { #[serde(rename = "accent_color")] pub accent_colour: Option, /// The user's chosen language option - pub locale: Option, + pub locale: Option, /// Whether the email on this account has been verified /// /// Requires [`Scope::Email`] @@ -275,7 +271,7 @@ pub struct User { /// The user's email /// /// Requires [`Scope::Email`] - pub email: Option, + pub email: Option, /// The flags on a user's account #[serde(default)] pub flags: UserPublicFlags, @@ -291,6 +287,12 @@ pub struct User { pub member: Option>, } +impl ExtractKey for User { + fn extract_key(&self) -> &UserId { + &self.id + } +} + enum_number! { /// Premium types denote the level of premium a user has. Visit the [Nitro](https://discord.com/nitro) /// page to learn more about the premium plans Discord currently offers. @@ -298,10 +300,8 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/user#user-object-premium-types). #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum PremiumType { - #[default] None = 0, NitroClassic = 1, Nitro = 2, @@ -348,35 +348,18 @@ bitflags! { /// Bot's running with HTTP interactions const BOT_HTTP_INTERACTIONS = 1 << 19; /// User's flag for suspected spam activity. - #[cfg(feature = "unstable_discord_api")] + #[cfg(feature = "unstable")] const SPAMMER = 1 << 20; /// User's flag as active developer const ACTIVE_DEVELOPER = 1 << 22; } } -use std::hash::{Hash, Hasher}; - -impl PartialEq for User { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Eq for User {} - -impl Hash for User { - fn hash(&self, hasher: &mut H) { - self.id.hash(hasher); - } -} - #[cfg(feature = "model")] impl User { /// Returns the formatted URL of the user's icon, if one exists. /// /// This will produce a WEBP image URL, or GIF if the user has a GIF avatar. - #[inline] #[must_use] pub fn avatar_url(&self) -> Option { avatar_url(None, self.id, self.avatar.as_ref()) @@ -388,7 +371,6 @@ impl User { /// /// **Note**: This will only be present if the user is fetched via Rest API, e.g. with /// [`crate::http::Http::get_user`]. - #[inline] #[must_use] pub fn banner_url(&self) -> Option { banner_url(self.id, self.banner.as_ref()) @@ -402,9 +384,8 @@ impl User { /// # Errors /// /// See [`UserId::create_dm_channel`] for what errors may be returned. - #[inline] pub async fn create_dm_channel(&self, cache_http: impl CacheHttp) -> Result { - if self.bot { + if self.bot() { return Err(Error::Model(ModelError::MessagingBot)); } @@ -412,7 +393,6 @@ impl User { } /// Retrieves the time that this user was created at. - #[inline] #[must_use] pub fn created_at(&self) -> Timestamp { self.id.created_at() @@ -421,7 +401,6 @@ impl User { /// Returns the formatted URL to the user's default avatar URL. /// /// This will produce a PNG URL. - #[inline] #[must_use] pub fn default_avatar_url(&self) -> String { default_avatar_url(self) @@ -437,19 +416,14 @@ impl User { /// # Errors /// /// See [`UserId::direct_message`] for errors. - pub async fn direct_message( - &self, - cache_http: impl CacheHttp, - builder: CreateMessage, - ) -> Result { - self.id.direct_message(cache_http, builder).await + pub async fn direct_message(&self, http: &Http, builder: CreateMessage<'_>) -> Result { + self.id.direct_message(http, builder).await } /// This is an alias of [`Self::direct_message`]. #[allow(clippy::missing_errors_doc)] - #[inline] - pub async fn dm(&self, cache_http: impl CacheHttp, builder: CreateMessage) -> Result { - self.direct_message(cache_http, builder).await + pub async fn dm(&self, http: &Http, builder: CreateMessage<'_>) -> Result { + self.direct_message(http, builder).await } /// Retrieves the URL to the user's avatar, falling back to the default avatar if needed. @@ -491,14 +465,13 @@ impl User { /// exist in the given [`Guild`], or if the given [`User`] is not in that [`Guild`]. /// /// May also return an [`Error::Json`] if there is an error in deserializing the API response. - #[inline] pub async fn has_role( &self, cache_http: impl CacheHttp, - guild_id: impl Into, - role: impl Into, + guild_id: GuildId, + role: RoleId, ) -> Result { - guild_id.into().member(cache_http, self).await.map(|m| m.roles.contains(&role.into())) + guild_id.member(cache_http, self.id).await.map(|m| m.roles.contains(&role)) } /// Refreshes the information about the user. @@ -508,7 +481,6 @@ impl User { /// # Errors /// /// See [`UserId::to_user`] for what errors may be returned. - #[inline] pub async fn refresh(&mut self, cache_http: impl CacheHttp) -> Result<()> { *self = self.id.to_user(cache_http).await?; @@ -518,7 +490,6 @@ impl User { /// Returns a static formatted URL of the user's icon, if one exists. /// /// This will always produce a WEBP image URL. - #[inline] #[must_use] pub fn static_avatar_url(&self) -> Option { static_avatar_url(self.id, self.avatar.as_ref()) @@ -548,7 +519,6 @@ impl User { /// } /// } /// ``` - #[inline] #[must_use] pub fn tag(&self) -> String { tag(&self.name, self.discriminator) @@ -557,14 +527,7 @@ impl User { /// Returns the user's nickname in the given `guild_id`. /// /// If none is used, it returns [`None`]. - #[inline] - pub async fn nick_in( - &self, - cache_http: impl CacheHttp, - guild_id: impl Into, - ) -> Option { - let guild_id = guild_id.into(); - + pub async fn nick_in(&self, cache_http: impl CacheHttp, guild_id: GuildId) -> Option { // This can't be removed because `GuildId::member` clones the entire `Member` struct if // it's present in the cache, which is expensive. #[cfg(feature = "cache")] @@ -572,42 +535,44 @@ impl User { if let Some(cache) = cache_http.cache() { if let Some(guild) = guild_id.to_guild_cached(cache) { if let Some(member) = guild.members.get(&self.id) { - return member.nick.clone(); + return member.nick.clone().map(Into::into); } } } } // At this point we're guaranteed to do an API call. - guild_id.member(cache_http, &self.id).await.ok().and_then(|member| member.nick) + guild_id + .member(cache_http, self.id) + .await + .ok() + .and_then(|member| member.nick) + .map(Into::into) } /// Returns a builder which can be awaited to obtain a message or stream of messages sent by /// this user. #[cfg(feature = "collector")] - pub fn await_reply(&self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_reply(&self, shard_messenger: ShardMessenger) -> MessageCollector { MessageCollector::new(shard_messenger).author_id(self.id) } /// Same as [`Self::await_reply`]. #[cfg(feature = "collector")] - pub fn await_replies(&self, shard_messenger: impl AsRef) -> MessageCollector { + pub fn await_replies(&self, shard_messenger: ShardMessenger) -> MessageCollector { self.await_reply(shard_messenger) } /// Returns a builder which can be awaited to obtain a reaction or stream of reactions sent by /// this user. #[cfg(feature = "collector")] - pub fn await_reaction(&self, shard_messenger: impl AsRef) -> ReactionCollector { + pub fn await_reaction(&self, shard_messenger: ShardMessenger) -> ReactionCollector { ReactionCollector::new(shard_messenger).author_id(self.id) } /// Same as [`Self::await_reaction`]. #[cfg(feature = "collector")] - pub fn await_reactions( - &self, - shard_messenger: impl AsRef, - ) -> ReactionCollector { + pub fn await_reactions(&self, shard_messenger: ShardMessenger) -> ReactionCollector { self.await_reaction(shard_messenger) } } @@ -635,6 +600,11 @@ impl UserId { /// /// [current user]: CurrentUser pub async fn create_dm_channel(self, cache_http: impl CacheHttp) -> Result { + #[derive(serde::Serialize)] + struct CreateDmChannel { + recipient_id: UserId, + } + #[cfg(feature = "temp_cache")] if let Some(cache) = cache_http.cache() { if let Some(private_channel) = cache.temp_private_channels.get(&self) { @@ -642,11 +612,11 @@ impl UserId { } } - let map = json!({ - "recipient_id": self, - }); + let body = CreateDmChannel { + recipient_id: self, + }; - let channel = cache_http.http().create_private_channel(&map).await?; + let channel = cache_http.http().create_private_channel(&body).await?; #[cfg(feature = "temp_cache")] if let Some(cache) = cache_http.cache() { @@ -679,9 +649,9 @@ impl UserId { /// if msg.content == "~help" { /// let builder = CreateMessage::new().content("Helpful info here."); /// - /// if let Err(why) = msg.author.id.direct_message(&ctx, builder).await { + /// if let Err(why) = msg.author.id.direct_message(&ctx.http, builder).await { /// println!("Err sending help: {why:?}"); - /// let _ = msg.reply(&ctx, "There was an error DMing you help.").await; + /// let _ = msg.reply(&ctx.http, "There was an error DMing you help.").await; /// }; /// } /// } @@ -698,32 +668,19 @@ impl UserId { pub async fn direct_message( self, cache_http: impl CacheHttp, - builder: CreateMessage, + builder: CreateMessage<'_>, ) -> Result { - self.create_dm_channel(&cache_http).await?.send_message(cache_http, builder).await + self.create_dm_channel(&cache_http).await?.send_message(cache_http.http(), builder).await } /// This is an alias of [`Self::direct_message`]. #[allow(clippy::missing_errors_doc)] - #[inline] - pub async fn dm(self, cache_http: impl CacheHttp, builder: CreateMessage) -> Result { - self.direct_message(cache_http, builder).await + pub async fn dm(self, http: &Http, builder: CreateMessage<'_>) -> Result { + self.direct_message(http, builder).await } - /// Attempts to find a [`User`] by its Id in the cache. - #[cfg(feature = "cache")] - #[inline] - pub fn to_user_cached(self, cache: &impl AsRef) -> Option> { - cache.as_ref().user(self) - } - - /// First attempts to find a [`User`] by its Id in the cache, upon failure requests it via the - /// REST API. - /// - /// **Note**: If the cache is not enabled, REST API will be used only. - /// - /// **Note**: If the cache is enabled, you might want to enable the `temp_cache` feature to - /// cache user data retrieved by this function for a short duration. + /// First attempts to find a [`User`] by its Id in the `temp_cache` if enabled, + /// upon failure requests it via the REST API. /// /// # Errors /// @@ -731,20 +688,19 @@ impl UserId { /// otherwise cannot be fetched. /// /// May also return an [`Error::Json`] if there is an error in deserializing the user. - #[inline] pub async fn to_user(self, cache_http: impl CacheHttp) -> Result { - #[cfg(feature = "cache")] + #[cfg(feature = "temp_cache")] { if let Some(cache) = cache_http.cache() { - if let Some(user) = cache.user(self) { - return Ok(user.clone()); + if let Some(user) = cache.temp_users.get(&self) { + return Ok(User::clone(&user)); } } } let user = cache_http.http().get_user(self).await?; - #[cfg(all(feature = "cache", feature = "temp_cache"))] + #[cfg(feature = "temp_cache")] { if let Some(cache) = cache_http.cache() { use crate::cache::MaybeOwnedArc; @@ -831,9 +787,10 @@ mod test { #[test] fn test_discriminator_serde() { use serde::{Deserialize, Serialize}; + use serde_json::json; use super::discriminator; - use crate::json::{assert_json, json}; + use crate::model::utils::assert_json; #[derive(Debug, PartialEq, Deserialize, Serialize)] struct User { @@ -857,6 +814,8 @@ mod test { use std::num::NonZeroU16; use std::str::FromStr; + use small_fixed_array::FixedString; + use crate::model::id::UserId; use crate::model::misc::ImageHash; use crate::model::user::User; @@ -867,7 +826,7 @@ mod test { id: UserId::new(210), avatar: Some(ImageHash::from_str("fb211703bcc04ee612c88d494df0272f").unwrap()), discriminator: NonZeroU16::new(1432), - name: "test".to_string(), + name: FixedString::from_static_trunc("test"), ..Default::default() }; diff --git a/src/model/utils.rs b/src/model/utils.rs index df900080bf0..02aa64e501e 100644 --- a/src/model/utils.rs +++ b/src/model/utils.rs @@ -1,14 +1,14 @@ use std::cell::Cell; use std::fmt; -use std::hash::Hash; -use std::marker::PhantomData; -use std::num::NonZeroU64; +use arrayvec::ArrayVec; use serde::de::Error as DeError; -use serde::ser::{Serialize, SerializeSeq, Serializer}; +use serde::ser::SerializeSeq; use serde_cow::CowStr; +use small_fixed_array::FixedString; use super::prelude::*; +use crate::internal::prelude::*; pub fn default_true() -> bool { true @@ -70,21 +70,6 @@ where remove_from_map_opt(map, key)?.ok_or_else(|| serde::de::Error::missing_field(key)) } -/// Workaround for Discord sending 0 value Ids as default values. -/// This has been fixed properly on next by swapping to a NonMax based impl. -pub fn deserialize_buggy_id<'de, D, Id>(deserializer: D) -> StdResult, D::Error> -where - D: Deserializer<'de>, - Id: From, -{ - if let Some(val) = Option::>::deserialize(deserializer)? { - let val = val.parse().map_err(serde::de::Error::custom)?; - Ok(NonZeroU64::new(val).map(Id::from)) - } else { - Ok(None) - } -} - pub(super) struct SerializeIter(Cell>); impl SerializeIter { @@ -122,11 +107,11 @@ impl StrOrInt<'_> { } } - pub fn into_enum(self, string: fn(String) -> T, int: fn(u64) -> T) -> T { + pub fn into_enum(self, string: fn(FixedString) -> T, int: fn(u64) -> T) -> T { match self { Self::Int(val) => int(val), - Self::String(val) => string(val), - Self::Str(val) => string(val.into()), + Self::String(val) => string(FixedString::from_string_trunc(val)), + Self::Str(val) => string(FixedString::from_str_trunc(val)), } } } @@ -169,157 +154,64 @@ impl<'de> serde::Deserialize<'de> for StrOrInt<'de> { } } -/// Used with `#[serde(with = "emojis")]` -pub mod emojis { - use std::collections::HashMap; - - use serde::Deserializer; - - use super::SequenceToMapVisitor; - use crate::model::guild::Emoji; - use crate::model::id::EmojiId; - - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - deserializer.deserialize_seq(SequenceToMapVisitor::new(|emoji: &Emoji| emoji.id)) - } - - pub use super::serialize_map_values as serialize; -} - -pub fn deserialize_guild_channels<'de, D: Deserializer<'de>>( - deserializer: D, -) -> StdResult, D::Error> { - struct TryDeserialize(StdResult); - impl<'de, T: Deserialize<'de>> Deserialize<'de> for TryDeserialize { - fn deserialize>(deserializer: D) -> StdResult { - Ok(Self(T::deserialize(deserializer).map_err(|e| e.to_string()))) - } - } - - let vec: Vec> = Deserialize::deserialize(deserializer)?; - let mut map = HashMap::new(); - - for channel in vec { - match channel.0 { - Ok(channel) => { - map.insert(channel.id, channel); - }, - Err(e) => tracing::warn!("skipping guild channel due to deserialization error: {}", e), - } - } - - Ok(map) -} - -/// Used with `#[serde(with = "members")] -pub mod members { - use std::collections::HashMap; - - use serde::Deserializer; - - use super::SequenceToMapVisitor; - use crate::model::guild::Member; - use crate::model::id::UserId; - - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - deserializer.deserialize_seq(SequenceToMapVisitor::new(|member: &Member| member.user.id)) - } - - pub use super::serialize_map_values as serialize; -} - -/// Used with `#[serde(with = "presences")]` -pub mod presences { - use std::collections::HashMap; - - use serde::Deserializer; - - use super::SequenceToMapVisitor; - use crate::model::gateway::Presence; - use crate::model::id::UserId; - - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - deserializer.deserialize_seq(SequenceToMapVisitor::new(|p: &Presence| p.user.id)) - } - - pub use super::serialize_map_values as serialize; +#[cfg(test)] +#[track_caller] +pub(crate) fn assert_json(data: &T, json: Value) +where + T: serde::Serialize + for<'de> Deserialize<'de> + PartialEq + std::fmt::Debug, +{ + // test serialization + let serialized = serde_json::to_value(data).unwrap(); + assert!( + serialized == json, + "data->JSON serialization failed\nexpected: {json:?}\n got: {serialized:?}" + ); + + // test deserialization + let deserialized = serde_json::from_value::(json).unwrap(); + assert!( + &deserialized == data, + "JSON->data deserialization failed\nexpected: {data:?}\n got: {deserialized:?}" + ); } pub fn deserialize_buttons<'de, D: Deserializer<'de>>( deserializer: D, -) -> StdResult, D::Error> { - Vec::deserialize(deserializer).map(|labels| { - labels - .into_iter() - .map(|l| ActivityButton { - label: l, - url: String::new(), - }) - .collect() +) -> StdResult, D::Error> { + ArrayVec::<_, 2>::deserialize(deserializer).map(|labels| { + FixedArray::from_vec_trunc( + labels + .into_iter() + .map(|l| ActivityButton { + label: l, + url: FixedString::default(), + }) + .collect(), + ) }) } -/// Used with `#[serde(with = "roles")]` -pub mod roles { - use std::collections::HashMap; - - use serde::Deserializer; - - use super::SequenceToMapVisitor; - use crate::model::guild::Role; - use crate::model::id::RoleId; - - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - deserializer.deserialize_seq(SequenceToMapVisitor::new(|role: &Role| role.id)) - } - - pub use super::serialize_map_values as serialize; -} - -/// Used with `#[serde(with = "stickers")]` -pub mod stickers { - use std::collections::HashMap; - - use serde::Deserializer; - - use super::SequenceToMapVisitor; - use crate::model::id::StickerId; - use crate::model::sticker::Sticker; - - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - deserializer.deserialize_seq(SequenceToMapVisitor::new(|sticker: &Sticker| sticker.id)) - } - - pub use super::serialize_map_values as serialize; -} - /// Used with `#[serde(with = "comma_separated_string")]` pub mod comma_separated_string { use serde::{Deserialize, Deserializer, Serializer}; use serde_cow::CowStr; + use crate::internal::prelude::*; + pub fn deserialize<'de, D: Deserializer<'de>>( deserializer: D, - ) -> Result, D::Error> { + ) -> Result, D::Error> { let str_sequence = CowStr::deserialize(deserializer)?.0; - let vec = str_sequence.split(", ").map(str::to_owned).collect(); + let vec = str_sequence.split(", ").map(FixedString::from_str_trunc).collect(); - Ok(vec) + Ok(FixedArray::from_vec_trunc(vec)) } - #[allow(clippy::ptr_arg)] - pub fn serialize(vec: &Vec, serializer: S) -> Result { - serializer.serialize_str(&vec.join(", ")) + pub fn serialize( + vec: &FixedArray, + serializer: S, + ) -> Result { + serializer.serialize_str(&join_to_string(", ", vec)) } } @@ -366,66 +258,7 @@ pub mod secret { secret: &Option>, serializer: Sr, ) -> Result { - secret.as_ref().map(|s| s.expose_secret()).serialize(serializer) - } -} - -pub fn deserialize_voice_states<'de, D: Deserializer<'de>>( - deserializer: D, -) -> StdResult, D::Error> { - deserializer.deserialize_seq(SequenceToMapVisitor::new(|state: &VoiceState| state.user_id)) -} - -pub fn serialize_map_values( - map: &HashMap, - serializer: S, -) -> StdResult { - let mut seq = serializer.serialize_seq(Some(map.len()))?; - - for value in map.values() { - seq.serialize_element(&value)?; - } - - seq.end() -} - -/// Deserializes a sequence and builds a `HashMap` with the key extraction function. -pub(in crate::model) struct SequenceToMapVisitor { - key: F, - marker: PhantomData, -} - -impl SequenceToMapVisitor { - pub fn new(key: F) -> Self { - Self { - key, - marker: PhantomData, - } - } -} - -impl<'de, F, K, V> Visitor<'de> for SequenceToMapVisitor -where - K: Eq + Hash, - V: Deserialize<'de>, - F: FnMut(&V) -> K, -{ - type Value = HashMap; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("sequence") - } - - fn visit_seq(mut self, mut seq: A) -> StdResult - where - A: serde::de::SeqAccess<'de>, - { - let mut map = seq.size_hint().map_or_else(HashMap::new, HashMap::with_capacity); - while let Some(elem) = seq.next_element()? { - map.insert((self.key)(&elem), elem); - } - - Ok(map) + secret.as_ref().map(ExposeSecret::expose_secret).serialize(serializer) } } diff --git a/src/model/voice.rs b/src/model/voice.rs index 9912c945f17..a25711928b4 100644 --- a/src/model/voice.rs +++ b/src/model/voice.rs @@ -3,6 +3,7 @@ use serde::de::{Deserialize, Deserializer}; use serde::Serialize; +use crate::internal::prelude::*; use crate::model::guild::Member; use crate::model::id::{ChannelId, GuildId, UserId}; use crate::model::Timestamp; @@ -10,7 +11,8 @@ use crate::model::Timestamp; /// Information about an available voice region. /// /// [Discord docs](https://discord.com/developers/docs/resources/voice#voice-region-object). -#[derive(Clone, Debug, Deserialize, Serialize)] +#[bool_to_bitflags::bool_to_bitflags] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct VoiceRegion { /// Whether it is a custom voice region, which is used for events. @@ -18,9 +20,9 @@ pub struct VoiceRegion { /// Whether it is a deprecated voice region, which you should avoid using. pub deprecated: bool, /// The internal Id of the voice region. - pub id: String, + pub id: FixedString, /// A recognizable name of the location of the voice region. - pub name: String, + pub name: FixedString, /// Whether the voice region is optimal for use by the current user. pub optimal: bool, } @@ -28,8 +30,9 @@ pub struct VoiceRegion { /// A user's state within a voice channel. /// /// [Discord docs](https://discord.com/developers/docs/resources/voice#voice-state-object). +#[bool_to_bitflags::bool_to_bitflags] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(remote = "Self")] #[non_exhaustive] pub struct VoiceState { @@ -42,7 +45,7 @@ pub struct VoiceState { pub self_mute: bool, pub self_stream: Option, pub self_video: bool, - pub session_id: String, + pub session_id: FixedString, pub suppress: bool, pub user_id: UserId, /// When unsuppressed, non-bot users will have this set to the current time. Bot users will be @@ -51,8 +54,14 @@ pub struct VoiceState { pub request_to_speak_timestamp: Option, } +impl extract_map::ExtractKey for VoiceState { + fn extract_key(&self) -> &UserId { + &self.user_id + } +} + // Manual impl needed to insert guild_id into Member -impl<'de> Deserialize<'de> for VoiceState { +impl<'de> Deserialize<'de> for VoiceStateGeneratedOriginal { fn deserialize>(deserializer: D) -> Result { // calls #[serde(remote)]-generated inherent method let mut state = Self::deserialize(deserializer)?; @@ -63,7 +72,7 @@ impl<'de> Deserialize<'de> for VoiceState { } } -impl Serialize for VoiceState { +impl Serialize for VoiceStateGeneratedOriginal { fn serialize(&self, serializer: S) -> Result { // calls #[serde(remote)]-generated inherent method Self::serialize(self, serializer) diff --git a/src/model/webhook.rs b/src/model/webhook.rs index b8bbcd005da..515f8c18194 100644 --- a/src/model/webhook.rs +++ b/src/model/webhook.rs @@ -6,12 +6,11 @@ use secrecy::SecretString; use super::utils::secret; #[cfg(feature = "model")] -use crate::builder::{Builder, EditWebhook, EditWebhookMessage, ExecuteWebhook}; +use crate::builder::{EditWebhook, EditWebhookMessage, ExecuteWebhook}; #[cfg(feature = "cache")] -use crate::cache::{Cache, GuildChannelRef, GuildRef}; +use crate::cache::{Cache, GuildRef}; #[cfg(feature = "model")] use crate::http::{CacheHttp, Http}; -#[cfg(feature = "model")] use crate::internal::prelude::*; use crate::model::prelude::*; @@ -21,7 +20,6 @@ enum_number! { /// [Discord docs](https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[serde(from = "u8", into = "u8")] #[non_exhaustive] pub enum WebhookType { /// An indicator that the webhook can post messages to channels with a token. @@ -36,14 +34,13 @@ enum_number! { } impl WebhookType { - #[inline] #[must_use] - pub fn name(&self) -> &str { + pub fn name(self) -> &'static str { match self { Self::Incoming => "incoming", Self::ChannelFollower => "channel follower", Self::Application => "application", - Self::Unknown(_) => "unknown", + _ => "unknown", } } } @@ -74,7 +71,7 @@ pub struct Webhook { /// The default name of the webhook. /// /// This can be temporarily overridden via [`ExecuteWebhook::username`]. - pub name: Option, + pub name: Option>, /// The default avatar. /// /// This can be temporarily overridden via [`ExecuteWebhook::avatar_url`]. @@ -95,6 +92,12 @@ pub struct Webhook { pub url: Option, } +impl ExtractKey for Webhook { + fn extract_key(&self) -> &WebhookId { + &self.id + } +} + /// The guild object returned by a [`Webhook`], of type [`WebhookType::ChannelFollower`]. #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Debug, Clone, Deserialize, Serialize)] @@ -103,7 +106,7 @@ pub struct WebhookGuild { /// The unique Id identifying the guild. pub id: GuildId, /// The name of the guild. - pub name: String, + pub name: FixedString, /// The hash of the icon used by the guild. /// /// In the client, this appears on the guild list on the left-hand side. @@ -114,9 +117,8 @@ pub struct WebhookGuild { impl WebhookGuild { /// Tries to find the [`Guild`] by its Id in the cache. #[cfg(feature = "cache")] - #[inline] - pub fn to_guild_cached(self, cache: &impl AsRef) -> Option> { - cache.as_ref().guild(self.id) + pub fn to_guild_cached<'a>(&self, cache: &'a Cache) -> Option> { + cache.guild(self.id) } /// Requests [`PartialGuild`] over REST API. @@ -127,7 +129,6 @@ impl WebhookGuild { /// # Errors /// /// Returns an [`Error::Http`] if the current user is not in the guild. - #[inline] pub async fn to_partial_guild(self, cache_http: impl CacheHttp) -> Result { #[cfg(feature = "cache")] { @@ -149,12 +150,8 @@ impl WebhookGuild { /// # Errors /// /// Returns an [`Error::Http`] if the current user is not in the guild. - #[inline] - pub async fn to_partial_guild_with_counts( - self, - http: impl AsRef, - ) -> Result { - http.as_ref().get_guild_with_counts(self.id).await + pub async fn to_partial_guild_with_counts(self, http: &Http) -> Result { + http.get_guild_with_counts(self.id).await } } @@ -165,20 +162,11 @@ pub struct WebhookChannel { /// The unique Id of the channel. pub id: ChannelId, /// The name of the channel. - pub name: String, + pub name: FixedString, } #[cfg(feature = "model")] impl WebhookChannel { - /// Attempts to find a [`GuildChannel`] by its Id in the cache. - #[cfg(feature = "cache")] - #[inline] - #[deprecated = "Use Cache::guild and Guild::channels"] - pub fn to_channel_cached(self, cache: &Cache) -> Option> { - #[allow(deprecated)] - cache.channel(self.id) - } - /// First attempts to retrieve the channel from the `temp_cache` if enabled, otherwise performs /// a HTTP request. /// @@ -188,7 +176,6 @@ impl WebhookChannel { /// # Errors /// /// Returns [`Error::Http`] if the channel retrieval request failed. - #[inline] pub async fn to_channel(self, cache_http: impl CacheHttp) -> Result { let channel = self.id.to_channel(cache_http).await?; channel.guild().ok_or(Error::Model(ModelError::InvalidChannelType)) @@ -225,8 +212,8 @@ impl Webhook { /// /// May also return an [`Error::Json`] if there is an error in deserialising Discord's /// response. - pub async fn from_id(http: impl AsRef, webhook_id: impl Into) -> Result { - http.as_ref().get_webhook(webhook_id.into()).await + pub async fn from_id(http: &Http, webhook_id: WebhookId) -> Result { + http.get_webhook(webhook_id).await } /// Retrieves a webhook given its Id and unique token. @@ -258,11 +245,11 @@ impl Webhook { /// May also return an [`Error::Json`] if there is an error in deserialising Discord's /// response. pub async fn from_id_with_token( - http: impl AsRef, - webhook_id: impl Into, + http: &Http, + webhook_id: WebhookId, token: &str, ) -> Result { - http.as_ref().get_webhook_with_token(webhook_id.into(), token).await + http.get_webhook_with_token(webhook_id, token).await } /// Retrieves a webhook given its url. @@ -292,8 +279,8 @@ impl Webhook { /// /// May also return an [`Error::Json`] if there is an error in deserialising Discord's /// response. - pub async fn from_url(http: impl AsRef, url: &str) -> Result { - http.as_ref().get_webhook_from_url(url).await + pub async fn from_url(http: &Http, url: &str) -> Result { + http.get_webhook_from_url(url).await } /// Deletes the webhook. @@ -305,14 +292,12 @@ impl Webhook { /// /// Returns [`Error::Http`] if the webhook does not exist, the token is invalid, or if the /// webhook could not otherwise be deleted. - #[inline] - pub async fn delete(&self, http: impl AsRef) -> Result<()> { - let http = http.as_ref(); + pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result<()> { match &self.token { Some(token) => { - http.delete_webhook_with_token(self.id, token.expose_secret(), None).await + http.delete_webhook_with_token(self.id, token.expose_secret(), reason).await }, - None => http.delete_webhook(self.id, None).await, + None => http.delete_webhook(self.id, reason).await, } } @@ -346,13 +331,9 @@ impl Webhook { /// May also return an [`Error::Http`] if the content is malformed, or if the token is invalid. /// /// Or may return an [`Error::Json`] if there is an error in deserialising Discord's response. - pub async fn edit( - &mut self, - cache_http: impl CacheHttp, - builder: EditWebhook<'_>, - ) -> Result<()> { + pub async fn edit(&mut self, http: &Http, builder: EditWebhook<'_>) -> Result<()> { let token = self.token.as_ref().map(ExposeSecret::expose_secret).map(String::as_str); - *self = builder.execute(cache_http, (self.id, token)).await?; + *self = builder.execute(http, self.id, token).await?; Ok(()) } @@ -414,15 +395,14 @@ impl Webhook { /// is invalid. /// /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response. - #[inline] pub async fn execute( &self, - cache_http: impl CacheHttp, + http: &Http, wait: bool, - builder: ExecuteWebhook, + builder: ExecuteWebhook<'_>, ) -> Result> { let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret(); - builder.execute(cache_http, (self.id, token, wait)).await + builder.execute(http, self.id, token, wait).await } /// Gets a previously sent message from the webhook. @@ -437,12 +417,12 @@ impl Webhook { /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response. pub async fn get_message( &self, - http: impl AsRef, + http: &Http, thread_id: Option, message_id: MessageId, ) -> Result { let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret(); - http.as_ref().get_webhook_message(self.id, thread_id, token, message_id).await + http.get_webhook_message(self.id, thread_id, token, message_id).await } /// Edits a webhook message with the fields set via the given builder. @@ -460,12 +440,12 @@ impl Webhook { /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response. pub async fn edit_message( &self, - cache_http: impl CacheHttp, + http: &Http, message_id: MessageId, - builder: EditWebhookMessage, + builder: EditWebhookMessage<'_>, ) -> Result { let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret(); - builder.execute(cache_http, (self.id, token, message_id)).await + builder.execute(http, self.id, token, message_id).await } /// Deletes a webhook message. @@ -478,12 +458,12 @@ impl Webhook { /// Id does not belong to the current webhook. pub async fn delete_message( &self, - http: impl AsRef, + http: &Http, thread_id: Option, message_id: MessageId, ) -> Result<()> { let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret(); - http.as_ref().delete_webhook_message(self.id, thread_id, token, message_id).await + http.delete_webhook_message(self.id, thread_id, token, message_id).await } /// Retrieves the latest information about the webhook, editing the webhook in-place. @@ -499,9 +479,9 @@ impl Webhook { /// error. Such as if the [`Webhook`] was deleted. /// /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response. - pub async fn refresh(&mut self, http: impl AsRef) -> Result<()> { + pub async fn refresh(&mut self, http: &Http) -> Result<()> { let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret(); - http.as_ref().get_webhook_with_token(self.id, token).await.map(|replacement| { + http.get_webhook_with_token(self.id, token).await.map(|replacement| { *self = replacement; }) } @@ -535,8 +515,7 @@ impl WebhookId { /// May also return an [`Error::Json`] if there is an error in deserialising the response. /// /// [Manage Webhooks]: super::permissions::Permissions::MANAGE_WEBHOOKS - #[inline] - pub async fn to_webhook(self, http: impl AsRef) -> Result { - http.as_ref().get_webhook(self).await + pub async fn to_webhook(self, http: &Http) -> Result { + http.get_webhook(self).await } } diff --git a/src/prelude.rs b/src/prelude.rs index 189b6c25b1b..e3221a439f9 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -14,8 +14,6 @@ //! [`serenity::Error`]: crate::Error pub use tokio::sync::{Mutex, RwLock}; -#[cfg(feature = "client")] -pub use typemap_rev::{TypeMap, TypeMapKey}; #[cfg(feature = "client")] pub use crate::client::Context; diff --git a/src/utils/argument_convert/channel.rs b/src/utils/argument_convert/channel.rs index f31cfacb9de..5fe9664dbca 100644 --- a/src/utils/argument_convert/channel.rs +++ b/src/utils/argument_convert/channel.rs @@ -54,7 +54,7 @@ async fn lookup_channel_global( #[cfg(feature = "cache")] if let Some(cache) = ctx.cache() { if let Some(guild) = cache.guild(guild_id) { - let channel = guild.channels.values().find(|c| c.name.eq_ignore_ascii_case(s)); + let channel = guild.channels.iter().find(|c| c.name.eq_ignore_ascii_case(s)); if let Some(channel) = channel { return Ok(Channel::Guild(channel.clone())); } @@ -79,7 +79,7 @@ async fn lookup_channel_global( /// /// The lookup strategy is as follows (in order): /// 1. Lookup by ID. -/// 2. [Lookup by mention](`crate::utils::parse_channel`). +/// 2. [Lookup by mention](`crate::utils::parse_channel_mention`). /// 3. Lookup by name. #[async_trait::async_trait] impl ArgumentConvert for Channel { diff --git a/src/utils/argument_convert/emoji.rs b/src/utils/argument_convert/emoji.rs index b42a067afc7..9a5d986ddb5 100644 --- a/src/utils/argument_convert/emoji.rs +++ b/src/utils/argument_convert/emoji.rs @@ -64,7 +64,7 @@ impl ArgumentConvert for Emoji { } if let Some(emoji) = - guild.emojis.values().find(|emoji| emoji.name.eq_ignore_ascii_case(s)).cloned() + guild.emojis.iter().find(|emoji| emoji.name.eq_ignore_ascii_case(s)).cloned() { return Ok(emoji); } diff --git a/src/utils/argument_convert/member.rs b/src/utils/argument_convert/member.rs index bcc6988e768..e3cea497ebd 100644 --- a/src/utils/argument_convert/member.rs +++ b/src/utils/argument_convert/member.rs @@ -35,7 +35,7 @@ impl fmt::Display for MemberParseError { /// /// The lookup strategy is as follows (in order): /// 1. Lookup by ID. -/// 2. [Lookup by mention](`crate::utils::parse_username`). +/// 2. [Lookup by mention](`crate::utils::parse_user_mention`). /// 3. [Lookup by name#discrim](`crate::utils::parse_user_tag`). /// 4. Lookup by name /// 5. Lookup by nickname @@ -63,8 +63,9 @@ impl ArgumentConvert for Member { // Following code is inspired by discord.py's MemberConvert::query_member_named // If string is a username+discriminator + let limit = nonmax::NonMaxU16::new(100); if let Some((name, discrim)) = crate::utils::parse_user_tag(s) { - if let Ok(member_results) = guild_id.search_members(ctx.http(), name, Some(100)).await { + if let Ok(member_results) = guild_id.search_members(ctx.http(), name, limit).await { if let Some(member) = member_results.into_iter().find(|m| { m.user.name.eq_ignore_ascii_case(name) && m.user.discriminator == discrim }) { @@ -74,7 +75,7 @@ impl ArgumentConvert for Member { } // If string is username or nickname - if let Ok(member_results) = guild_id.search_members(ctx.http(), s, Some(100)).await { + if let Ok(member_results) = guild_id.search_members(ctx.http(), s, limit).await { if let Some(member) = member_results.into_iter().find(|m| { m.user.name.eq_ignore_ascii_case(s) || m.nick.as_ref().is_some_and(|nick| nick.eq_ignore_ascii_case(s)) diff --git a/src/utils/argument_convert/role.rs b/src/utils/argument_convert/role.rs index e446ef5d68b..c18319d9a9a 100644 --- a/src/utils/argument_convert/role.rs +++ b/src/utils/argument_convert/role.rs @@ -45,7 +45,7 @@ impl fmt::Display for RoleParseError { /// /// The lookup strategy is as follows (in order): /// 1. Lookup by ID -/// 2. [Lookup by mention](`crate::utils::parse_role`). +/// 2. [Lookup by mention](`crate::utils::parse_role_mention`). /// 3. Lookup by name (case-insensitive) #[async_trait::async_trait] impl ArgumentConvert for Role { @@ -84,7 +84,7 @@ impl ArgumentConvert for Role { } #[cfg(feature = "cache")] - if let Some(role) = roles.values().find(|role| role.name.eq_ignore_ascii_case(s)) { + if let Some(role) = roles.iter().find(|role| role.name.eq_ignore_ascii_case(s)) { return Ok(role.clone()); } #[cfg(not(feature = "cache"))] diff --git a/src/utils/argument_convert/user.rs b/src/utils/argument_convert/user.rs index b99ab60e779..c49db78a360 100644 --- a/src/utils/argument_convert/user.rs +++ b/src/utils/argument_convert/user.rs @@ -23,43 +23,13 @@ impl fmt::Display for UserParseError { } } -#[cfg(feature = "cache")] -fn lookup_by_global_cache(ctx: impl CacheHttp, s: &str) -> Option { - let users = &ctx.cache()?.users; - - let lookup_by_id = || users.get(&s.parse().ok()?).map(|u| u.clone()); - - let lookup_by_mention = || users.get(&crate::utils::parse_user_mention(s)?).map(|u| u.clone()); - - let lookup_by_name_and_discrim = || { - let (name, discrim) = crate::utils::parse_user_tag(s)?; - users.iter().find_map(|m| { - let user = m.value(); - (user.discriminator == discrim && user.name.eq_ignore_ascii_case(name)) - .then(|| user.clone()) - }) - }; - - let lookup_by_name = || { - users.iter().find_map(|m| { - let user = m.value(); - (user.name == s).then(|| user.clone()) - }) - }; - - lookup_by_id() - .or_else(lookup_by_mention) - .or_else(lookup_by_name_and_discrim) - .or_else(lookup_by_name) -} - /// Look up a user by a string case-insensitively. /// /// Requires the cache feature to be enabled. If a user is not in cache, they will not be found! /// /// The lookup strategy is as follows (in order): /// 1. Lookup by ID. -/// 2. [Lookup by mention](`crate::utils::parse_username`). +/// 2. [Lookup by mention](`crate::utils::parse_user_mention`). /// 3. [Lookup by name#discrim](`crate::utils::parse_user_tag`). /// 4. Lookup by name #[async_trait::async_trait] @@ -72,13 +42,7 @@ impl ArgumentConvert for User { channel_id: Option, s: &str, ) -> Result { - // Try to look up in global user cache via a variety of methods - #[cfg(feature = "cache")] - if let Some(user) = lookup_by_global_cache(&ctx, s) { - return Ok(user); - } - - // If not successful, convert as a Member which uses HTTP endpoints instead of cache + // Convert as a Member which uses HTTP endpoints instead of cache if let Ok(member) = Member::convert(&ctx, guild_id, channel_id, s).await { return Ok(member.user); } diff --git a/src/utils/content_safe.rs b/src/utils/content_safe.rs index c32f6d00340..607af0290ff 100644 --- a/src/utils/content_safe.rs +++ b/src/utils/content_safe.rs @@ -1,92 +1,46 @@ use std::borrow::Cow; -use crate::cache::Cache; -use crate::model::id::GuildId; +use crate::model::guild::Guild; use crate::model::mention::Mention; use crate::model::user::User; /// Struct that allows to alter [`content_safe`]'s behaviour. -#[derive(Clone, Debug)] +#[bool_to_bitflags::bool_to_bitflags( + getter_prefix = "get_", + setter_prefix = "", + private_getters, + document_setters, + owning_setters +)] +#[derive(Copy, Clone, Debug)] pub struct ContentSafeOptions { - clean_role: bool, - clean_user: bool, - clean_channel: bool, - clean_here: bool, - clean_everyone: bool, - show_discriminator: bool, - guild_reference: Option, -} - -impl ContentSafeOptions { - #[must_use] - pub fn new() -> Self { - ContentSafeOptions::default() - } - /// [`content_safe`] will replace role mentions (`<@&{id}>`) with its name prefixed with `@` /// (`@rolename`) or with `@deleted-role` if the identifier is invalid. - #[must_use] - pub fn clean_role(mut self, b: bool) -> Self { - self.clean_role = b; - - self - } - + pub clean_role: bool, /// If set to true, [`content_safe`] will replace user mentions (`<@!{id}>` or `<@{id}>`) with /// the user's name prefixed with `@` (`@username`) or with `@invalid-user` if the identifier /// is invalid. - #[must_use] - pub fn clean_user(mut self, b: bool) -> Self { - self.clean_user = b; - - self - } - + pub clean_user: bool, /// If set to true, [`content_safe`] will replace channel mentions (`<#{id}>`) with the /// channel's name prefixed with `#` (`#channelname`) or with `#deleted-channel` if the /// identifier is invalid. - #[must_use] - pub fn clean_channel(mut self, b: bool) -> Self { - self.clean_channel = b; - - self - } - + pub clean_channel: bool, + /// If set, [`content_safe`] will replace `@here` with a non-pinging alternative. + pub clean_here: bool, + /// If set, [`content_safe`] will replace `@everyone` with a non-pinging alternative. + pub clean_everyone: bool, /// If set to true, if [`content_safe`] replaces a user mention it will add their four digit /// discriminator with a preceding `#`, turning `@username` to `@username#discriminator`. /// /// This option is ignored if the username is a next-gen username, and /// therefore does not have a discriminator. - #[must_use] - pub fn show_discriminator(mut self, b: bool) -> Self { - self.show_discriminator = b; - - self - } - - /// If set, [`content_safe`] will replace a user mention with the user's display name in passed - /// `guild`. - #[must_use] - pub fn display_as_member_from>(mut self, guild: G) -> Self { - self.guild_reference = Some(guild.into()); - - self - } - - /// If set, [`content_safe`] will replace `@here` with a non-pinging alternative. - #[must_use] - pub fn clean_here(mut self, b: bool) -> Self { - self.clean_here = b; - - self - } + pub show_discriminator: bool, +} - /// If set, [`content_safe`] will replace `@everyone` with a non-pinging alternative. +impl ContentSafeOptions { #[must_use] - pub fn clean_everyone(mut self, b: bool) -> Self { - self.clean_everyone = b; - - self + pub fn new() -> Self { + ContentSafeOptions::default() } } @@ -94,19 +48,13 @@ impl Default for ContentSafeOptions { /// Instantiates with all options set to `true`. fn default() -> Self { ContentSafeOptions { - clean_role: true, - clean_user: true, - clean_channel: true, - clean_here: true, - clean_everyone: true, - show_discriminator: true, - guild_reference: None, + __generated_flags: ContentSafeOptionsGeneratedFlags::all(), } } } /// Transforms role, channel, user, `@everyone` and `@here` mentions into raw text by using the -/// [`Cache`] and the users passed in with `users`. +/// Guild and the users passed in with `users`. /// /// [`ContentSafeOptions`] decides what kind of mentions should be filtered and how the raw-text /// will be displayed. @@ -116,15 +64,14 @@ impl Default for ContentSafeOptions { /// Sanitise an `@everyone` mention. /// /// ```rust -/// # use serenity::client::Cache; -/// # -/// # let cache = Cache::default(); +/// # let cache = serenity::client::Cache::default(); +/// # let guild = serenity::model::guild::Guild::default(); /// use serenity::utils::{content_safe, ContentSafeOptions}; /// /// let with_mention = "@everyone"; -/// let without_mention = content_safe(&cache, &with_mention, &ContentSafeOptions::default(), &[]); +/// let without_mention = content_safe(&guild, &with_mention, ContentSafeOptions::default(), &[]); /// -/// assert_eq!("@\u{200B}everyone".to_string(), without_mention); +/// assert_eq!("@\u{200B}everyone", without_mention); /// ``` /// /// Filtering out mentions from a message. @@ -135,35 +82,30 @@ impl Default for ContentSafeOptions { /// use serenity::utils::{content_safe, ContentSafeOptions}; /// /// fn filter_message(cache: &Cache, message: &Message) -> String { -/// content_safe(cache, &message.content, &ContentSafeOptions::default(), &message.mentions) +/// if let Some(guild) = message.guild(cache) { +/// content_safe(&guild, &message.content, ContentSafeOptions::default(), &message.mentions) +/// } else { +/// // We don't need to clean messages in DMs +/// message.content.to_string() +/// } /// } /// ``` -pub fn content_safe( - cache: impl AsRef, - s: impl AsRef, - options: &ContentSafeOptions, - users: &[User], -) -> String { - let mut content = clean_mentions(&cache, s, options, users); +#[must_use] +pub fn content_safe(guild: &Guild, s: &str, options: ContentSafeOptions, users: &[User]) -> String { + let mut content = clean_mentions(guild, s, options, users); - if options.clean_here { + if options.get_clean_here() { content = content.replace("@here", "@\u{200B}here"); } - if options.clean_everyone { + if options.get_clean_everyone() { content = content.replace("@everyone", "@\u{200B}everyone"); } content } -fn clean_mentions( - cache: impl AsRef, - s: impl AsRef, - options: &ContentSafeOptions, - users: &[User], -) -> String { - let s = s.as_ref(); +fn clean_mentions(guild: &Guild, s: &str, options: ContentSafeOptions, users: &[User]) -> String { let mut content = String::with_capacity(s.len()); let mut brackets = s.match_indices(|c| c == '<' || c == '>').peekable(); let mut progress = 0; @@ -180,12 +122,12 @@ fn clean_mentions( let mut chars = mention_str.chars(); chars.next(); let should_parse = match chars.next() { - Some('#') => options.clean_channel, + Some('#') => options.get_clean_channel(), Some('@') => { if let Some('&') = chars.next() { - options.clean_role + options.get_clean_role() } else { - options.clean_user + options.get_clean_user() } }, _ => false, @@ -197,7 +139,7 @@ fn clean_mentions( // NOTE: numeric strings that are too large to fit into u64 will not parse // correctly and will be left unchanged. if let Ok(mention) = mention_str.parse() { - content.push_str(&clean_mention(&cache, mention, options, users)); + content.push_str(&clean_mention(guild, mention, options, users)); cleaned = true; } } @@ -214,54 +156,39 @@ fn clean_mentions( } fn clean_mention( - cache: impl AsRef, + guild: &Guild, mention: Mention, - options: &ContentSafeOptions, + options: ContentSafeOptions, users: &[User], ) -> Cow<'static, str> { - let cache = cache.as_ref(); match mention { Mention::Channel(id) => { - #[allow(deprecated)] // This is reworked on next already. - if let Some(channel) = id.to_channel_cached(cache) { + if let Some(channel) = guild.channels.get(&id) { format!("#{}", channel.name).into() } else { "#deleted-channel".into() } }, - Mention::Role(id) => options - .guild_reference - .and_then(|id| cache.guild(id)) - .and_then(|g| g.roles.get(&id).map(|role| format!("@{}", role.name).into())) - .unwrap_or(Cow::Borrowed("@deleted-role")), + Mention::Role(id) => guild + .roles + .get(&id) + .map_or(Cow::Borrowed("@deleted-role"), |role| format!("@{}", role.name).into()), Mention::User(id) => { - if let Some(guild_id) = options.guild_reference { - if let Some(guild) = cache.guild(guild_id) { - if let Some(member) = guild.members.get(&id) { - return if options.show_discriminator { - format!("@{}", member.distinct()) - } else { - format!("@{}", member.display_name()) - } - .into(); - } + if let Some(member) = guild.members.get(&id) { + if options.get_show_discriminator() { + format!("@{}", member.distinct()).into() + } else { + format!("@{}", member.display_name()).into() } - } - - let get_username = |user: &User| { - if options.show_discriminator { - format!("@{}", user.tag()) + } else if let Some(user) = users.iter().find(|u| u.id == id) { + if options.get_show_discriminator() { + format!("@{}", user.tag()).into() } else { - format!("@{}", user.name) + format!("@{}", user.name).into() } - .into() - }; - - cache - .user(id) - .map(|u| get_username(&u)) - .or_else(|| users.iter().find(|u| u.id == id).map(get_username)) - .unwrap_or(Cow::Borrowed("@invalid-user")) + } else { + "@invalid-user".into() + } }, } } @@ -269,57 +196,49 @@ fn clean_mention( #[allow(clippy::non_ascii_literal)] #[cfg(test)] mod tests { - use std::sync::Arc; + use small_fixed_array::FixedString; use super::*; use crate::model::channel::*; use crate::model::guild::*; - use crate::model::id::{ChannelId, RoleId, UserId}; + use crate::model::id::{ChannelId, GuildId, RoleId, UserId}; #[test] fn test_content_safe() { let user = User { id: UserId::new(100000000000000000), - name: "Crab".to_string(), - ..Default::default() - }; - - let outside_cache_user = User { - id: UserId::new(100000000000000001), - name: "Boat".to_string(), + name: FixedString::from_static_trunc("Crab"), ..Default::default() }; - let mut guild = Guild { + let no_member_guild = Guild { id: GuildId::new(381880193251409931), ..Default::default() }; + let mut guild = no_member_guild.clone(); + let member = Member { - nick: Some("Ferris".to_string()), + nick: Some(FixedString::from_static_trunc("Ferris")), + user: user.clone(), ..Default::default() }; let role = Role { id: RoleId::new(333333333333333333), - name: "ferris-club-member".to_string(), + name: FixedString::from_static_trunc("ferris-club-member"), ..Default::default() }; let channel = GuildChannel { id: ChannelId::new(111880193700067777), - name: "general".to_string(), + name: FixedString::from_static_trunc("general"), ..Default::default() }; - let cache = Arc::new(Cache::default()); - - guild.channels.insert(channel.id, channel.clone()); - guild.members.insert(user.id, member.clone()); - guild.roles.insert(role.id, role); - cache.users.insert(user.id, user.clone()); - cache.guilds.insert(guild.id, guild.clone()); - cache.channels.insert(channel.id, guild.id); + guild.channels.insert(channel.clone()); + guild.members.insert(member.clone()); + guild.roles.insert(role); let with_user_mentions = "<@!100000000000000000> <@!000000000000000000> <@123> <@!123> \ <@!123123123123123123123> <@123> <@123123123123123123> <@!invalid> \ @@ -327,7 +246,7 @@ mod tests { <@!i)/==(<<>z/9080)> <@!1231invalid> <@invalid123> \ <@123invalid> <@> <@ "; - let without_user_mentions = "@Crab <@!000000000000000000> @invalid-user @invalid-user \ + let without_user_mentions = "@Ferris @invalid-user @invalid-user @invalid-user \ <@!123123123123123123123> @invalid-user @invalid-user <@!invalid> \ <@invalid> <@日本語 한국어$§)[/__#\\(/&2032$§#> \ <@!i)/==(<<>z/9080)> <@!1231invalid> <@invalid123> \ @@ -335,49 +254,32 @@ mod tests { // User mentions let options = ContentSafeOptions::default(); - assert_eq!(without_user_mentions, content_safe(&cache, with_user_mentions, &options, &[])); + assert_eq!(without_user_mentions, content_safe(&guild, with_user_mentions, options, &[])); - let options = ContentSafeOptions::default(); assert_eq!( - format!("@{}", user.name), - content_safe(&cache, "<@!100000000000000000>", &options, &[]) + "@invalid-user", + content_safe(&no_member_guild, "<@100000000000000001>", options, &[]) ); - let options = ContentSafeOptions::default(); + let mut options = ContentSafeOptions::default(); + options = options.show_discriminator(false); assert_eq!( format!("@{}", user.name), - content_safe(&cache, "<@100000000000000000>", &options, &[]) + content_safe(&no_member_guild, "<@!100000000000000000>", options, &[user.clone()]) ); - let options = ContentSafeOptions::default(); - assert_eq!("@invalid-user", content_safe(&cache, "<@100000000000000001>", &options, &[])); - - let options = ContentSafeOptions::default(); assert_eq!( - format!("@{}", outside_cache_user.name), - content_safe(&cache, "<@100000000000000001>", &options, &[outside_cache_user]) + "@invalid-user", + content_safe(&no_member_guild, "<@!100000000000000000>", options, &[]) ); - let options = options.show_discriminator(false); assert_eq!( - format!("@{}", user.name), - content_safe(&cache, "<@!100000000000000000>", &options, &[]) + format!("@{}", member.nick.as_ref().unwrap()), + content_safe(&guild, "<@100000000000000000>", options, &[]) ); - let options = options.show_discriminator(false); - assert_eq!( - format!("@{}", user.name), - content_safe(&cache, "<@100000000000000000>", &options, &[]) - ); - - let options = options.display_as_member_from(guild.id); - assert_eq!( - format!("@{}", member.nick.unwrap()), - content_safe(&cache, "<@!100000000000000000>", &options, &[]) - ); - - let options = options.clean_user(false); - assert_eq!(with_user_mentions, content_safe(&cache, with_user_mentions, &options, &[])); + options = options.clean_user(false); + assert_eq!(with_user_mentions, content_safe(&guild, with_user_mentions, options, &[])); // Channel mentions let with_channel_mentions = "<#> <#deleted-channel> #deleted-channel <#1> \ @@ -390,13 +292,13 @@ mod tests { assert_eq!( without_channel_mentions, - content_safe(&cache, with_channel_mentions, &options, &[]) + content_safe(&guild, with_channel_mentions, options, &[]) ); - let options = options.clean_channel(false); + options = options.clean_channel(false); assert_eq!( with_channel_mentions, - content_safe(&cache, with_channel_mentions, &options, &[]) + content_safe(&guild, with_channel_mentions, options, &[]) ); // Role mentions @@ -408,25 +310,24 @@ mod tests { @ferris-club-member @deleted-role \ <@&111111111111111111111111111111> <@&@deleted-role"; - assert_eq!(without_role_mentions, content_safe(&cache, with_role_mentions, &options, &[])); + assert_eq!(without_role_mentions, content_safe(&guild, with_role_mentions, options, &[])); - let options = options.clean_role(false); - assert_eq!(with_role_mentions, content_safe(&cache, with_role_mentions, &options, &[])); + options = options.clean_role(false); + assert_eq!(with_role_mentions, content_safe(&guild, with_role_mentions, options, &[])); // Everyone mentions let with_everyone_mention = "@everyone"; - let without_everyone_mention = "@\u{200B}everyone"; assert_eq!( without_everyone_mention, - content_safe(&cache, with_everyone_mention, &options, &[]) + content_safe(&guild, with_everyone_mention, options, &[]) ); - let options = options.clean_everyone(false); + options = options.clean_everyone(false); assert_eq!( with_everyone_mention, - content_safe(&cache, with_everyone_mention, &options, &[]) + content_safe(&guild, with_everyone_mention, options, &[]) ); // Here mentions @@ -434,9 +335,9 @@ mod tests { let without_here_mention = "@\u{200B}here"; - assert_eq!(without_here_mention, content_safe(&cache, with_here_mention, &options, &[])); + assert_eq!(without_here_mention, content_safe(&guild, with_here_mention, options, &[])); - let options = options.clean_here(false); - assert_eq!(with_here_mention, content_safe(&cache, with_here_mention, &options, &[])); + options = options.clean_here(false); + assert_eq!(with_here_mention, content_safe(&guild, with_here_mention, options, &[])); } } diff --git a/src/utils/custom_message.rs b/src/utils/custom_message.rs index 7250c789d43..3c8d9ebf5f4 100644 --- a/src/utils/custom_message.rs +++ b/src/utils/custom_message.rs @@ -13,7 +13,6 @@ pub struct CustomMessage { impl CustomMessage { /// Constructs a new instance of this builder, alongside a message with dummy data. Use the /// methods to replace the individual bits of this message with valid data. - #[inline] #[must_use] pub fn new() -> Self { Self::default() @@ -22,7 +21,6 @@ impl CustomMessage { /// Assign the dummy message a proper ID for identification. /// /// If not used, the default value is `MessageId::new(1)`. - #[inline] #[must_use] pub fn id(&mut self, id: MessageId) -> &mut Self { self.msg.id = id; @@ -33,9 +31,8 @@ impl CustomMessage { /// Assign the dummy message files attached to it. /// /// If not used, the default value is an empty vector (`Vec::default()`). - #[inline] - pub fn attachments(&mut self, attachments: impl IntoIterator) -> &mut Self { - self.msg.attachments = attachments.into_iter().collect(); + pub fn attachments(&mut self, attachments: Vec) -> &mut Self { + self.msg.attachments = attachments.trunc_into(); self } @@ -43,7 +40,6 @@ impl CustomMessage { /// Assign the dummy message its author. /// /// If not used, the default value is a dummy [`User`]. - #[inline] pub fn author(&mut self, user: User) -> &mut Self { self.msg.author = user; @@ -53,7 +49,6 @@ impl CustomMessage { /// Assign the dummy message its origin channel's ID. /// /// If not used, the default value is `ChannelId::new(1)`. - #[inline] pub fn channel_id(&mut self, channel_id: ChannelId) -> &mut Self { self.msg.channel_id = channel_id; @@ -63,9 +58,8 @@ impl CustomMessage { /// Assign the dummy message its contents. /// /// If not used, the default value is an empty string (`String::default()`). - #[inline] pub fn content(&mut self, s: impl Into) -> &mut Self { - self.msg.content = s.into(); + self.msg.content = s.into().trunc_into(); self } @@ -73,7 +67,6 @@ impl CustomMessage { /// Assign the dummy message the timestamp it was edited. /// /// If not used, the default value is [`None`] (not all messages are edited). - #[inline] pub fn edited_timestamp>(&mut self, timestamp: T) -> &mut Self { self.msg.edited_timestamp = Some(timestamp.into()); @@ -83,9 +76,8 @@ impl CustomMessage { /// Assign the dummy message embeds. /// /// If not used, the default value is an empty vector (`Vec::default()`). - #[inline] - pub fn embeds(&mut self, embeds: impl IntoIterator) -> &mut Self { - self.msg.embeds = embeds.into_iter().collect(); + pub fn embeds(&mut self, embeds: Vec) -> &mut Self { + self.msg.embeds = embeds.trunc_into(); self } @@ -93,7 +85,6 @@ impl CustomMessage { /// Assign the dummy message its origin guild's ID. /// /// If not used, the default value is [`None`] (not all messages are sent in guilds). - #[inline] pub fn guild_id(&mut self, guild_id: GuildId) -> &mut Self { self.msg.guild_id = Some(guild_id); @@ -103,7 +94,6 @@ impl CustomMessage { /// Assign the dummy message its type. /// /// If not used, the default value is [`MessageType::Regular`]. - #[inline] pub fn kind(&mut self, kind: MessageType) -> &mut Self { self.msg.kind = kind; @@ -115,7 +105,6 @@ impl CustomMessage { /// If not used, the default value is [`None`] (not all messages are sent in guilds). /// /// [author]: Self::author - #[inline] pub fn member(&mut self, member: PartialMember) -> &mut Self { self.msg.member = Some(Box::new(member)); @@ -125,9 +114,8 @@ impl CustomMessage { /// Assign the dummy message a flag whether it mentions everyone (`@everyone`). /// /// If not used, the default value is `false`. - #[inline] pub fn mention_everyone(&mut self, mentions: bool) -> &mut Self { - self.msg.mention_everyone = mentions; + self.msg.set_mention_everyone(mentions); self } @@ -135,9 +123,8 @@ impl CustomMessage { /// Assign the dummy message a list of roles it mentions. /// /// If not used, the default value is an empty vector (`Vec::default()`). - #[inline] - pub fn mention_roles(&mut self, roles: impl IntoIterator) -> &mut Self { - self.msg.mention_roles = roles.into_iter().collect(); + pub fn mention_roles(&mut self, roles: Vec) -> &mut Self { + self.msg.mention_roles = roles.trunc_into(); self } @@ -145,9 +132,8 @@ impl CustomMessage { /// Assign the dummy message a list of mentions. /// /// If not used, the default value is an empty vector (`Vec::default()`). - #[inline] - pub fn mentions(&mut self, mentions: impl IntoIterator) -> &mut Self { - self.msg.mentions = mentions.into_iter().collect(); + pub fn mentions(&mut self, mentions: Vec) -> &mut Self { + self.msg.mentions = mentions.trunc_into(); self } @@ -155,9 +141,8 @@ impl CustomMessage { /// Assign the dummy message a flag whether it's been pinned. /// /// If not used, the default value is `false`. - #[inline] pub fn pinned(&mut self, pinned: bool) -> &mut Self { - self.msg.pinned = pinned; + self.msg.set_pinned(pinned); self } @@ -165,9 +150,8 @@ impl CustomMessage { /// Assign the dummy message a list of emojis it was reacted with. /// /// If not used, the default value is an empty vector (`Vec::default()`). - #[inline] - pub fn reactions(&mut self, reactions: impl IntoIterator) -> &mut Self { - self.msg.reactions = reactions.into_iter().collect(); + pub fn reactions(&mut self, reactions: Vec) -> &mut Self { + self.msg.reactions = reactions.trunc_into(); self } @@ -175,7 +159,6 @@ impl CustomMessage { /// Assign the dummy message the timestamp it was created at. /// /// If not used, the default value is the current local time. - #[inline] pub fn timestamp>(&mut self, timestamp: T) -> &mut Self { self.msg.timestamp = timestamp.into(); @@ -185,9 +168,8 @@ impl CustomMessage { /// Assign the dummy message a flag whether it'll be read by a Text-To-Speech program. /// /// If not used, the default value is `false`. - #[inline] pub fn tts(&mut self, tts: bool) -> &mut Self { - self.msg.tts = tts; + self.msg.set_tts(tts); self } @@ -195,7 +177,6 @@ impl CustomMessage { /// Assign the dummy message the webhook author's ID. /// /// If not used, the default value is [`None`] (not all messages are sent by webhooks). - #[inline] pub fn webhook_id(&mut self, id: WebhookId) -> &mut Self { self.msg.webhook_id = Some(id); @@ -203,7 +184,6 @@ impl CustomMessage { } /// Consume this builder and return the constructed message. - #[inline] #[must_use] pub fn build(self) -> Message { self.msg diff --git a/src/utils/message_builder.rs b/src/utils/message_builder.rs index 73ac20f8535..797c40dbd2d 100644 --- a/src/utils/message_builder.rs +++ b/src/utils/message_builder.rs @@ -33,6 +33,7 @@ use crate::model::mention::Mentionable; /// # } /// ``` #[derive(Clone, Debug, Default)] +#[must_use] pub struct MessageBuilder(pub String); impl MessageBuilder { @@ -50,7 +51,6 @@ impl MessageBuilder { /// // alternatively: /// let message = MessageBuilder::default(); /// ``` - #[must_use] pub fn new() -> MessageBuilder { MessageBuilder::default() } @@ -78,13 +78,12 @@ impl MessageBuilder { /// ```rust /// use serenity::utils::MessageBuilder; /// - /// let mut content = MessageBuilder::new(); - /// content.push("test"); - /// + /// let mut content = MessageBuilder::new().push("test"); /// assert_eq!(content.build(), "test"); /// ``` - pub fn build(&mut self) -> String { - self.clone().0 + #[must_use] + pub fn build(self) -> String { + self.0 } /// Mentions the [`GuildChannel`] in the built message. @@ -113,12 +112,7 @@ impl MessageBuilder { /// [`Channel`]: crate::model::channel::Channel /// [`GuildChannel`]: crate::model::channel::GuildChannel /// [Display implementation]: ChannelId#impl-Display - #[inline] - pub fn channel>(&mut self, channel: C) -> &mut Self { - self._channel(channel.into()) - } - - fn _channel(&mut self, channel: ChannelId) -> &mut Self { + pub fn channel(mut self, channel: ChannelId) -> Self { self._push(&channel.mention()); self } @@ -132,7 +126,7 @@ impl MessageBuilder { /// Mention an emoji in a message's content: /// /// ```rust - /// # use serenity::json::{json, from_value}; + /// # use serde_json::{json, from_value}; /// # use serenity::model::guild::Emoji; /// # use serenity::model::id::EmojiId; /// # use serenity::utils::MessageBuilder; @@ -148,13 +142,13 @@ impl MessageBuilder { /// ``` /// /// [Display implementation]: crate::model::guild::Emoji#impl-Display - pub fn emoji(&mut self, emoji: &Emoji) -> &mut Self { + pub fn emoji(mut self, emoji: &Emoji) -> Self { self._push(&emoji); self } /// Mentions something that implements the [`Mentionable`] trait. - pub fn mention(&mut self, item: &M) -> &mut Self { + pub fn mention(mut self, item: &M) -> Self { self._push(&item.mention()); self } @@ -169,27 +163,18 @@ impl MessageBuilder { /// ```rust /// use serenity::utils::MessageBuilder; /// - /// let mut message = MessageBuilder::new(); - /// message.push("test"); + /// let message = MessageBuilder::new().push("test"); /// - /// assert_eq!( - /// { - /// message.push("ing"); - /// message.build() - /// }, - /// "testing" - /// ); + /// assert_eq!(message.push("ing").build(), "testing"); /// ``` - #[inline] - pub fn push(&mut self, content: impl Into) -> &mut Self { - self._push(&content.into()) + pub fn push<'a>(mut self, content: impl Into>) -> Self { + self._push(&content.into()); + + self } - #[inline] - fn _push(&mut self, content: &C) -> &mut Self { + fn _push(&mut self, content: &dyn std::fmt::Display) { write!(self.0, "{content}").unwrap(); - - self } /// Pushes a codeblock to the content, with optional syntax highlighting. @@ -230,11 +215,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "```\nhello\n```"); /// ``` - pub fn push_codeblock( - &mut self, - content: impl Into, + pub fn push_codeblock<'a>( + mut self, + content: impl Into>, language: Option<&str>, - ) -> &mut Self { + ) -> Self { self.0.push_str("```"); if let Some(language) = language { @@ -272,7 +257,7 @@ impl MessageBuilder { /// /// assert_eq!(content, expected); /// ``` - pub fn push_mono(&mut self, content: impl Into) -> &mut Self { + pub fn push_mono<'a>(mut self, content: impl Into>) -> Self { self.0.push('`'); self._push(&content.into()); self.0.push('`'); @@ -301,7 +286,7 @@ impl MessageBuilder { /// /// assert_eq!(content, expected); /// ``` - pub fn push_italic(&mut self, content: impl Into) -> &mut Self { + pub fn push_italic<'a>(mut self, content: impl Into>) -> Self { self.0.push('_'); self._push(&content.into()); self.0.push('_'); @@ -310,7 +295,7 @@ impl MessageBuilder { } /// Pushes an inline bold text to the content. - pub fn push_bold(&mut self, content: impl Into) -> &mut Self { + pub fn push_bold<'a>(mut self, content: impl Into>) -> Self { self.0.push_str("**"); self._push(&content.into()); self.0.push_str("**"); @@ -319,7 +304,7 @@ impl MessageBuilder { } /// Pushes an underlined inline text to the content. - pub fn push_underline(&mut self, content: impl Into) -> &mut Self { + pub fn push_underline<'a>(mut self, content: impl Into>) -> Self { self.0.push_str("__"); self._push(&content.into()); self.0.push_str("__"); @@ -328,7 +313,7 @@ impl MessageBuilder { } /// Pushes a strikethrough inline text to the content. - pub fn push_strike(&mut self, content: impl Into) -> &mut Self { + pub fn push_strike<'a>(mut self, content: impl Into>) -> Self { self.0.push_str("~~"); self._push(&content.into()); self.0.push_str("~~"); @@ -337,7 +322,7 @@ impl MessageBuilder { } /// Pushes a spoiler'd inline text to the content. - pub fn push_spoiler(&mut self, content: impl Into) -> &mut Self { + pub fn push_spoiler<'a>(mut self, content: impl Into>) -> Self { self.0.push_str("||"); self._push(&content.into()); self.0.push_str("||"); @@ -346,7 +331,7 @@ impl MessageBuilder { } /// Pushes a quoted inline text to the content - pub fn push_quote(&mut self, content: impl Into) -> &mut Self { + pub fn push_quote<'a>(mut self, content: impl Into>) -> Self { self.0.push_str("> "); self._push(&content.into()); @@ -366,8 +351,8 @@ impl MessageBuilder { /// /// assert_eq!(content, "hello\nworld"); /// ``` - pub fn push_line(&mut self, content: impl Into) -> &mut Self { - self.push(content); + pub fn push_line<'a>(mut self, content: impl Into>) -> Self { + self._push(&content.into()); self.0.push('\n'); self @@ -386,11 +371,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "`hello`\nworld"); /// ``` - pub fn push_mono_line(&mut self, content: impl Into) -> &mut Self { - self.push_mono(content); - self.0.push('\n'); + pub fn push_mono_line<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_mono(content); + this.0.push('\n'); - self + this } /// Pushes an inlined italicized text with an added newline to the content. @@ -406,11 +391,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "_hello_\nworld"); /// ``` - pub fn push_italic_line(&mut self, content: impl Into) -> &mut Self { - self.push_italic(content); - self.0.push('\n'); + pub fn push_italic_line<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_italic(content); + this.0.push('\n'); - self + this } /// Pushes an inline bold text with an added newline to the content. @@ -426,11 +411,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "**hello**\nworld"); /// ``` - pub fn push_bold_line(&mut self, content: impl Into) -> &mut Self { - self.push_bold(content); - self.0.push('\n'); + pub fn push_bold_line<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_bold(content); + this.0.push('\n'); - self + this } /// Pushes an underlined inline text with an added newline to the content. @@ -446,11 +431,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "__hello__\nworld"); /// ``` - pub fn push_underline_line(&mut self, content: impl Into) -> &mut Self { - self.push_underline(content); - self.0.push('\n'); + pub fn push_underline_line<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_underline(content); + this.0.push('\n'); - self + this } /// Pushes a strikethrough inline text with a newline added to the content. @@ -466,11 +451,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "~~hello~~\nworld"); /// ``` - pub fn push_strike_line(&mut self, content: impl Into) -> &mut Self { - self.push_strike(content); - self.0.push('\n'); + pub fn push_strike_line<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_strike(content); + this.0.push('\n'); - self + this } /// Pushes a spoiler'd inline text with a newline added to the content. @@ -486,11 +471,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "||hello||\nworld"); /// ``` - pub fn push_spoiler_line(&mut self, content: impl Into) -> &mut Self { - self.push_spoiler(content); - self.0.push('\n'); + pub fn push_spoiler_line<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_spoiler(content); + this.0.push('\n'); - self + this } /// Pushes a quoted inline text to the content @@ -506,33 +491,39 @@ impl MessageBuilder { /// /// assert_eq!(content, "> hello\nworld"); /// ``` - pub fn push_quote_line(&mut self, content: impl Into) -> &mut Self { - self.push_quote(content); - self.0.push('\n'); + pub fn push_quote_line<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_quote(content); + this.0.push('\n'); - self + this + } + + fn _push_safe(&mut self, content: Content<'_>, norm: impl Fn(&str) -> String) { + let safe_content = norm(content.inner); + let safe_content = Content { + inner: &safe_content, + ..content + }; + + self._push(&safe_content); } /// Pushes text to your message, but normalizing content - that means ensuring that there's no /// unwanted formatting, mention spam etc. - pub fn push_safe(&mut self, content: impl Into) -> &mut Self { - { - let mut c = content.into(); - c.inner = - normalize(&c.inner).replace('*', "\\*").replace('`', "\\`").replace('_', "\\_"); - - self._push(&c); - } + pub fn push_safe<'a>(mut self, content: impl Into>) -> Self { + self._push_safe(content.into(), |c| { + normalize(c).replace('*', "\\*").replace('`', "\\`").replace('_', "\\_") + }); self } /// Pushes a code-block to your message normalizing content. - pub fn push_codeblock_safe( - &mut self, - content: impl Into, + pub fn push_codeblock_safe<'a>( + mut self, + content: impl Into>, language: Option<&str>, - ) -> &mut Self { + ) -> Self { self.0.push_str("```"); if let Some(language) = language { @@ -540,102 +531,70 @@ impl MessageBuilder { } self.0.push('\n'); - { - let mut c = content.into(); - c.inner = normalize(&c.inner).replace("```", " "); - self._push(&c); - } + self._push_safe(content.into(), |c| normalize(c).replace("```", " ")); self.0.push_str("\n```"); self } /// Pushes an inline monospaced text to the content normalizing content. - pub fn push_mono_safe(&mut self, content: impl Into) -> &mut Self { + pub fn push_mono_safe<'a>(mut self, content: impl Into>) -> Self { self.0.push('`'); - { - let mut c = content.into(); - c.inner = normalize(&c.inner).replace('`', "'"); - self._push(&c); - } + self._push_safe(content.into(), |c| normalize(c).replace('`', "'")); self.0.push('`'); self } /// Pushes an inline italicized text to the content normalizing content. - pub fn push_italic_safe(&mut self, content: impl Into) -> &mut Self { + pub fn push_italic_safe<'a>(mut self, content: impl Into>) -> Self { self.0.push('_'); - { - let mut c = content.into(); - c.inner = normalize(&c.inner).replace('_', " "); - self._push(&c); - } + self._push_safe(content.into(), |c| normalize(c).replace('_', " ")); self.0.push('_'); self } /// Pushes an inline bold text to the content normalizing content. - pub fn push_bold_safe(&mut self, content: impl Into) -> &mut Self { + pub fn push_bold_safe<'a>(mut self, content: impl Into>) -> Self { self.0.push_str("**"); - { - let mut c = content.into(); - c.inner = normalize(&c.inner).replace("**", " "); - self._push(&c); - } + self._push_safe(content.into(), |c| normalize(c).replace("**", " ")); self.0.push_str("**"); self } /// Pushes an underlined inline text to the content normalizing content. - pub fn push_underline_safe(&mut self, content: impl Into) -> &mut Self { + pub fn push_underline_safe<'a>(mut self, content: impl Into>) -> Self { self.0.push_str("__"); - { - let mut c = content.into(); - c.inner = normalize(&c.inner).replace("__", " "); - self._push(&c); - } + self._push_safe(content.into(), |c| normalize(c).replace("__", " ")); self.0.push_str("__"); self } /// Pushes a strikethrough inline text to the content normalizing content. - pub fn push_strike_safe(&mut self, content: impl Into) -> &mut Self { + pub fn push_strike_safe<'a>(mut self, content: impl Into>) -> Self { self.0.push_str("~~"); - { - let mut c = content.into(); - c.inner = normalize(&c.inner).replace("~~", " "); - self._push(&c); - } + self._push_safe(content.into(), |c| normalize(c).replace("~~", " ")); self.0.push_str("~~"); self } /// Pushes a spoiler'd inline text to the content normalizing content. - pub fn push_spoiler_safe(&mut self, content: impl Into) -> &mut Self { + pub fn push_spoiler_safe<'a>(mut self, content: impl Into>) -> Self { self.0.push_str("||"); - { - let mut c = content.into(); - c.inner = normalize(&c.inner).replace("||", " "); - self._push(&c); - } + self._push_safe(content.into(), |c| normalize(c).replace("||", " ")); self.0.push_str("||"); self } /// Pushes a quoted inline text to the content normalizing content. - pub fn push_quote_safe(&mut self, content: impl Into) -> &mut Self { + pub fn push_quote_safe<'a>(mut self, content: impl Into>) -> Self { self.0.push_str("> "); - { - let mut c = content.into(); - c.inner = normalize(&c.inner).replace("> ", " "); - self._push(&c); - } + self._push_safe(content.into(), |c| normalize(c).replace("> ", " ")); self } @@ -654,11 +613,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "Hello @\u{200B}everyone\nHow are you?"); /// ``` - pub fn push_line_safe(&mut self, content: impl Into) -> &mut Self { - self.push_safe(content); - self.0.push('\n'); + pub fn push_line_safe<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_safe(content); + this.0.push('\n'); - self + this } /// Pushes an inline monospaced text with added newline to the content normalizing content. @@ -675,11 +634,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "`'hello @\u{200B}everyone'`\nworld"); /// ``` - pub fn push_mono_line_safe(&mut self, content: impl Into) -> &mut Self { - self.push_mono_safe(content); - self.0.push('\n'); + pub fn push_mono_line_safe<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_mono_safe(content); + this.0.push('\n'); - self + this } /// Pushes an inline italicized text with added newline to the content normalizing content. @@ -696,11 +655,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "_@\u{200B}everyone_\nIsn't a mention."); /// ``` - pub fn push_italic_line_safe(&mut self, content: impl Into) -> &mut Self { - self.push_italic_safe(content); - self.0.push('\n'); + pub fn push_italic_line_safe<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_italic_safe(content); + this.0.push('\n'); - self + this } /// Pushes an inline bold text with added newline to the content normalizing content. @@ -717,11 +676,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "**@\u{200B}everyone**\nIsn't a mention."); /// ``` - pub fn push_bold_line_safe(&mut self, content: impl Into) -> &mut Self { - self.push_bold_safe(content); - self.0.push('\n'); + pub fn push_bold_line_safe<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_bold_safe(content); + this.0.push('\n'); - self + this } /// Pushes an underlined inline text with added newline to the content normalizing content. @@ -740,11 +699,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "__@\u{200B}everyone__\nIsn't a mention."); /// ``` - pub fn push_underline_line_safe(&mut self, content: impl Into) -> &mut Self { - self.push_underline_safe(content); - self.0.push('\n'); + pub fn push_underline_line_safe<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_underline_safe(content); + this.0.push('\n'); - self + this } /// Pushes a strikethrough inline text with added newline to the content normalizing content. @@ -761,11 +720,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "~~@\u{200B}everyone~~\nIsn't a mention."); /// ``` - pub fn push_strike_line_safe(&mut self, content: impl Into) -> &mut Self { - self.push_strike_safe(content); - self.0.push('\n'); + pub fn push_strike_line_safe<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_strike_safe(content); + this.0.push('\n'); - self + this } /// Pushes a spoiler'd inline text with added newline to the content normalizing content. @@ -782,11 +741,11 @@ impl MessageBuilder { /// /// assert_eq!(content, "||@\u{200B}everyone||\nIsn't a mention."); /// ``` - pub fn push_spoiler_line_safe(&mut self, content: impl Into) -> &mut Self { - self.push_spoiler_safe(content); - self.0.push('\n'); + pub fn push_spoiler_line_safe<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_spoiler_safe(content); + this.0.push('\n'); - self + this } /// Pushes a quoted inline text with added newline to the content normalizing content. @@ -803,15 +762,15 @@ impl MessageBuilder { /// /// assert_eq!(content, "> @\u{200B}everyone\nIsn't a mention."); /// ``` - pub fn push_quote_line_safe(&mut self, content: impl Into) -> &mut Self { - self.push_quote_safe(content); - self.0.push('\n'); + pub fn push_quote_line_safe<'a>(self, content: impl Into>) -> Self { + let mut this = self.push_quote_safe(content); + this.0.push('\n'); - self + this } /// Starts a multi-line quote, every push after this one will be quoted - pub fn quote_rest(&mut self) -> &mut Self { + pub fn quote_rest(mut self) -> Self { self.0.push_str("\n>>> "); self @@ -819,31 +778,25 @@ impl MessageBuilder { /// Mentions the [`Role`] in the built message. /// - /// This accepts anything that converts _into_ a [`RoleId`]. Refer to [`RoleId`]'s - /// documentation for more information. - /// /// Refer to [`RoleId`]'s [Display implementation] for more information on how this is /// formatted. /// /// [`Role`]: crate::model::guild::Role /// [Display implementation]: RoleId#impl-Display - pub fn role>(&mut self, role: R) -> &mut Self { - self._push(&role.into().mention()); + pub fn role(mut self, role: RoleId) -> Self { + self._push(&role.mention()); self } /// Mentions the [`User`] in the built message. /// - /// This accepts anything that converts _into_ a [`UserId`]. Refer to [`UserId`]'s - /// documentation for more information. - /// /// Refer to [`UserId`]'s [Display implementation] for more information on how this is /// formatted. /// /// [`User`]: crate::model::user::User /// [Display implementation]: UserId#impl-Display - pub fn user>(&mut self, user: U) -> &mut Self { - self._push(&user.into().mention()); + pub fn user(mut self, user: UserId) -> Self { + self._push(&user.mention()); self } } @@ -896,14 +849,16 @@ pub trait EmbedMessageBuilding { /// ```rust /// use serenity::utils::{EmbedMessageBuilding, MessageBuilder}; /// - /// let mut msg = MessageBuilder::new(); - /// msg.push("Rust's website: "); - /// msg.push_named_link("Homepage", "https://rust-lang.org"); - /// let content = msg.build(); + /// let content = MessageBuilder::new() + /// .push("Rust's website: ") + /// .push_named_link("Homepage", "https://rust-lang.org") + /// .build(); /// /// assert_eq!(content, "Rust's website: [Homepage](https://rust-lang.org)"); /// ``` - fn push_named_link(&mut self, name: impl Into, url: impl Into) -> &mut Self; + #[must_use] + fn push_named_link<'a>(self, name: impl Into>, url: impl Into>) + -> Self; /// Pushes a named link intended for use in an embed, but with a normalized name to avoid /// escaping issues. @@ -915,44 +870,41 @@ pub trait EmbedMessageBuilding { /// ```rust /// use serenity::utils::{EmbedMessageBuilding, MessageBuilder}; /// - /// let mut msg = MessageBuilder::new(); - /// msg.push("A weird website name: "); - /// msg.push_named_link_safe("Try to ] break links (](", "https://rust-lang.org"); - /// let content = msg.build(); + /// let content = MessageBuilder::new() + /// .push("A weird website name: ") + /// .push_named_link_safe("Try to ] break links (](", "https://rust-lang.org") + /// .build(); /// /// assert_eq!(content, "A weird website name: [Try to break links ( (](https://rust-lang.org)"); /// ``` - fn push_named_link_safe( - &mut self, - name: impl Into, - url: impl Into, - ) -> &mut Self; + #[must_use] + fn push_named_link_safe<'a>( + self, + name: impl Into>, + url: impl Into>, + ) -> Self; } impl EmbedMessageBuilding for MessageBuilder { - fn push_named_link(&mut self, name: impl Into, url: impl Into) -> &mut Self { + fn push_named_link<'a>( + mut self, + name: impl Into>, + url: impl Into>, + ) -> Self { write!(self.0, "[{}]({})", name.into(), url.into()).unwrap(); self } - fn push_named_link_safe( - &mut self, - name: impl Into, - url: impl Into, - ) -> &mut Self { + fn push_named_link_safe<'a>( + mut self, + name: impl Into>, + url: impl Into>, + ) -> Self { self.0.push('['); - { - let mut c = name.into(); - c.inner = normalize(&c.inner).replace(']', " "); - self._push(&c); - } + self._push_safe(name.into(), |c| normalize(c).replace(']', " ")); self.0.push_str("]("); - { - let mut c = url.into(); - c.inner = normalize(&c.inner).replace(')', " "); - self._push(&c); - } + self._push_safe(url.into(), |c| normalize(c).replace(')', " ")); self.0.push(')'); self @@ -971,9 +923,10 @@ impl EmbedMessageBuilding for MessageBuilder { /// ```rust,no_run /// use serenity::utils::Content; /// use serenity::utils::ContentModifier::{Bold, Italic}; -/// let content: Content = Bold + Italic + "text"; +/// let content: Content = "text" + Bold + Italic; /// ``` #[non_exhaustive] +#[derive(Clone, Copy)] pub enum ContentModifier { Italic, Bold, @@ -983,142 +936,126 @@ pub enum ContentModifier { Spoiler, } +#[bool_to_bitflags::bool_to_bitflags(owning_setters)] /// Describes formatting on string content #[derive(Clone, Debug, Default)] -pub struct Content { +pub struct Content<'a> { pub italic: bool, pub bold: bool, pub strikethrough: bool, - pub inner: String, + pub inner: &'a str, pub code: bool, pub underline: bool, pub spoiler: bool, } -impl> Add for Content { - type Output = Content; +impl<'a> Add for &'a str { + type Output = Content<'a>; - fn add(mut self, rhs: T) -> Content { - self.inner += rhs.as_ref(); + fn add(self, rhs: ContentModifier) -> Self::Output { + let content = Content { + inner: self, + ..Default::default() + }; - self + content.apply(rhs) } } -impl> Add for ContentModifier { - type Output = Content; +impl<'a> Add<&'a str> for ContentModifier { + type Output = Content<'a>; - fn add(self, rhs: T) -> Content { - let mut nc = self.to_content(); - nc.inner += rhs.as_ref(); - nc + fn add(self, rhs: &'a str) -> Self::Output { + rhs + self } } -impl Add for Content { - type Output = Content; - - fn add(mut self, rhs: ContentModifier) -> Content { - self.apply(&rhs); +impl<'a> Add for Content<'a> { + type Output = Content<'a>; - self + fn add(self, rhs: ContentModifier) -> Content<'a> { + self.apply(rhs) } } impl Add for ContentModifier { - type Output = Content; + type Output = Content<'static>; - fn add(self, rhs: ContentModifier) -> Content { - let mut nc = self.to_content(); - nc.apply(&rhs); - - nc + fn add(self, rhs: ContentModifier) -> Self::Output { + let content = self.to_content(); + content.apply(rhs) } } impl ContentModifier { - fn to_content(&self) -> Content { - let mut nc = Content::default(); - nc.apply(self); - - nc + fn to_content(self) -> Content<'static> { + Content::default().apply(self) } } -impl Content { - pub fn apply(&mut self, modifier: &ContentModifier) { - match *modifier { - ContentModifier::Italic => { - self.italic = true; - }, - ContentModifier::Bold => { - self.bold = true; - }, - ContentModifier::Strikethrough => { - self.strikethrough = true; - }, - ContentModifier::Code => { - self.code = true; - }, - ContentModifier::Underline => { - self.underline = true; - }, - ContentModifier::Spoiler => { - self.spoiler = true; - }, +impl Content<'_> { + #[must_use] + pub fn apply(self, modifier: ContentModifier) -> Self { + match modifier { + ContentModifier::Italic => self.set_italic(true), + ContentModifier::Bold => self.set_bold(true), + ContentModifier::Strikethrough => self.set_strikethrough(true), + ContentModifier::Code => self.set_code(true), + ContentModifier::Underline => self.set_underline(true), + ContentModifier::Spoiler => self.set_spoiler(true), } } } -impl std::fmt::Display for Content { +impl std::fmt::Display for Content<'_> { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.spoiler { + if self.spoiler() { fmt.write_str("||")?; } - if self.bold { + if self.bold() { fmt.write_str("**")?; } - if self.italic { + if self.italic() { fmt.write_char('*')?; } - if self.strikethrough { + if self.strikethrough() { fmt.write_str("~~")?; } - if self.underline { + if self.underline() { fmt.write_str("__")?; } - if self.code { + if self.code() { fmt.write_char('`')?; } - fmt.write_str(&self.inner)?; + fmt.write_str(self.inner)?; - if self.code { + if self.code() { fmt.write_char('`')?; } - if self.underline { + if self.underline() { fmt.write_str("__")?; } - if self.strikethrough { + if self.strikethrough() { fmt.write_str("~~")?; } - if self.italic { + if self.italic() { fmt.write_char('*')?; } - if self.bold { + if self.bold() { fmt.write_str("**")?; } - if self.spoiler { + if self.spoiler() { fmt.write_str("||")?; } @@ -1126,10 +1063,10 @@ impl std::fmt::Display for Content { } } -impl> From for Content { - fn from(t: T) -> Content { +impl<'a> From<&'a str> for Content<'a> { + fn from(inner: &'a str) -> Content<'a> { Content { - inner: t.into(), + inner, ..Default::default() } } @@ -1195,18 +1132,18 @@ mod test { #[test] fn mentions() { - let content_emoji = MessageBuilder::new() - .emoji(&Emoji { - animated: false, - available: true, - id: EmojiId::new(32), - name: "Rohrkatze".to_string(), - managed: false, - require_colons: true, - roles: vec![], - user: None, - }) - .build(); + let mut emoji = Emoji { + id: EmojiId::new(32), + name: FixedString::from_static_trunc("Rohrkatze"), + roles: FixedArray::empty(), + user: None, + __generated_flags: EmojiGeneratedFlags::empty(), + }; + + emoji.set_available(true); + emoji.set_require_colons(true); + + let content_emoji = MessageBuilder::new().emoji(&emoji).build(); let content_mentions = MessageBuilder::new() .channel(ChannelId::new(1)) .mention(&UserId::new(2)) @@ -1219,11 +1156,11 @@ mod test { #[test] fn content() { - let content = Bold + Italic + Code + "Fun!"; + let content = "Fun!" + Bold + Italic + Code; assert_eq!(content.to_string(), "***`Fun!`***"); - let content = Spoiler + Bold + "Divert your eyes elsewhere"; + let content = "Divert your eyes elsewhere" + Spoiler + Bold; assert_eq!(content.to_string(), "||**Divert your eyes elsewhere**||"); } @@ -1236,21 +1173,21 @@ mod test { #[test] fn message_content() { - let message_content = MessageBuilder::new().push(Bold + Italic + Code + "Fun!").build(); + let message_content = MessageBuilder::new().push("Fun!" + Bold + Italic + Code).build(); assert_eq!(message_content, "***`Fun!`***"); } #[test] fn message_content_safe() { - let message_content = MessageBuilder::new().push_safe(Bold + Italic + "test**test").build(); + let message_content = MessageBuilder::new().push_safe("test**test" + Bold + Italic).build(); assert_eq!(message_content, "***test\\*\\*test***"); } #[test] fn push() { - assert_eq!(MessageBuilder::new().push('a').0, "a"); + assert_eq!(MessageBuilder::new().push("a").0, "a"); assert!(MessageBuilder::new().push("").0.is_empty()); } @@ -1360,7 +1297,7 @@ mod test { push_bold => [ "a" => "**a**", "" => "****", - '*' => "*****", + "*" => "*****", "**" => "******" ], push_bold_line => [ diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 324376bfe87..6e62a15dc25 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -22,16 +22,13 @@ pub use content_safe::*; pub use formatted_timestamp::*; #[cfg(feature = "collector")] pub use quick_modal::*; +use tracing::warn; use url::Url; pub use self::custom_message::CustomMessage; pub use self::message_builder::{Content, ContentModifier, EmbedMessageBuilding, MessageBuilder}; #[doc(inline)] pub use self::token::validate as validate_token; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::cache::Cache; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::http::CacheHttp; use crate::model::prelude::*; /// Retrieves the "code" part of an invite out of a URL. @@ -113,22 +110,22 @@ pub fn parse_user_tag(s: &str) -> Option<(&str, Option)> { /// /// ```rust /// use serenity::model::id::UserId; -/// use serenity::utils::parse_username; +/// use serenity::utils::parse_user_mention; /// /// // regular username mention -/// assert_eq!(parse_username("<@114941315417899012>"), Some(UserId::new(114941315417899012))); +/// assert_eq!(parse_user_mention("<@114941315417899012>"), Some(UserId::new(114941315417899012))); /// /// // nickname mention -/// assert_eq!(parse_username("<@!114941315417899012>"), Some(UserId::new(114941315417899012))); +/// assert_eq!(parse_user_mention("<@!114941315417899012>"), Some(UserId::new(114941315417899012))); /// ``` /// /// Asserting that an invalid username or nickname mention returns [`None`]: /// /// ```rust -/// use serenity::utils::parse_username; +/// use serenity::utils::parse_user_mention; /// -/// assert!(parse_username("<@1149413154aa17899012").is_none()); -/// assert!(parse_username("<@!11494131541789a90b1c2").is_none()); +/// assert!(parse_user_mention("<@1149413154aa17899012").is_none()); +/// assert!(parse_user_mention("<@!11494131541789a90b1c2").is_none()); /// ``` /// /// [`User`]: crate::model::user::User @@ -148,11 +145,6 @@ pub fn parse_user_mention(mention: &str) -> Option { } } -#[deprecated = "use `utils::parse_user_mention` instead"] -pub fn parse_username(mention: impl AsRef) -> Option { - parse_user_mention(mention.as_ref()) -} - /// Retrieves an Id from a role mention. /// /// If the mention is invalid, then [`None`] is returned. @@ -163,17 +155,17 @@ pub fn parse_username(mention: impl AsRef) -> Option { /// /// ```rust /// use serenity::model::id::RoleId; -/// use serenity::utils::parse_role; +/// use serenity::utils::parse_role_mention; /// -/// assert_eq!(parse_role("<@&136107769680887808>"), Some(RoleId::new(136107769680887808))); +/// assert_eq!(parse_role_mention("<@&136107769680887808>"), Some(RoleId::new(136107769680887808))); /// ``` /// /// Asserting that an invalid role mention returns [`None`]: /// /// ```rust -/// use serenity::utils::parse_role; +/// use serenity::utils::parse_role_mention; /// -/// assert!(parse_role("<@&136107769680887808").is_none()); +/// assert!(parse_role_mention("<@&136107769680887808").is_none()); /// ``` /// /// [`Role`]: crate::model::guild::Role @@ -191,11 +183,6 @@ pub fn parse_role_mention(mention: &str) -> Option { } } -#[deprecated = "use `utils::parse_role_mention` instead"] -pub fn parse_role(mention: impl AsRef) -> Option { - parse_role_mention(mention.as_ref()) -} - /// Retrieves an Id from a channel mention. /// /// If the channel mention is invalid, then [`None`] is returned. @@ -206,18 +193,21 @@ pub fn parse_role(mention: impl AsRef) -> Option { /// /// ```rust /// use serenity::model::id::ChannelId; -/// use serenity::utils::parse_channel; +/// use serenity::utils::parse_channel_mention; /// -/// assert_eq!(parse_channel("<#81384788765712384>"), Some(ChannelId::new(81384788765712384))); +/// assert_eq!( +/// parse_channel_mention("<#81384788765712384>"), +/// Some(ChannelId::new(81384788765712384)) +/// ); /// ``` /// /// Asserting that an invalid channel mention returns [`None`]: /// /// ```rust -/// use serenity::utils::parse_channel; +/// use serenity::utils::parse_channel_mention; /// -/// assert!(parse_channel("<#!81384788765712384>").is_none()); -/// assert!(parse_channel("<#81384788765712384").is_none()); +/// assert!(parse_channel_mention("<#!81384788765712384>").is_none()); +/// assert!(parse_channel_mention("<#81384788765712384").is_none()); /// ``` /// /// [`Channel`]: crate::model::channel::Channel @@ -235,11 +225,6 @@ pub fn parse_channel_mention(mention: &str) -> Option { } } -#[deprecated = "use `utils::parse_channel_mention` instead"] -pub fn parse_channel(mention: impl AsRef) -> Option { - parse_channel_mention(mention.as_ref()) -} - /// Retrieves the animated state, name and Id from an emoji mention, in the form of an /// [`EmojiIdentifier`]. /// @@ -257,7 +242,7 @@ pub fn parse_channel(mention: impl AsRef) -> Option { /// let emoji = parse_emoji("<:smugAnimeFace:302516740095606785>").unwrap(); /// assert_eq!(emoji.animated, false); /// assert_eq!(emoji.id, EmojiId::new(302516740095606785)); -/// assert_eq!(emoji.name, "smugAnimeFace".to_string()); +/// assert_eq!(&*emoji.name, "smugAnimeFace"); /// ``` /// /// Asserting that an invalid emoji usage returns [`None`]: @@ -269,11 +254,9 @@ pub fn parse_channel(mention: impl AsRef) -> Option { /// ``` /// /// [`Emoji`]: crate::model::guild::Emoji -pub fn parse_emoji(mention: impl AsRef) -> Option { - let mention = mention.as_ref(); - +#[must_use] +pub fn parse_emoji(mention: &str) -> Option { let len = mention.len(); - if !(6..=56).contains(&len) { return None; } @@ -302,9 +285,9 @@ pub fn parse_emoji(mention: impl AsRef) -> Option { } id.parse().ok().map(|id| EmojiIdentifier { + name: name.trunc_into(), animated, id, - name, }) } else { None @@ -336,8 +319,8 @@ pub fn parse_emoji(mention: impl AsRef) -> Option { /// /// assert_eq!(parse_quotes(command), expected); /// ``` -pub fn parse_quotes(s: impl AsRef) -> Vec { - let s = s.as_ref(); +#[must_use] +pub fn parse_quotes(s: &str) -> Vec { let mut args = vec![]; let mut in_string = false; let mut escaping = false; @@ -420,68 +403,6 @@ pub fn parse_webhook(url: &Url) -> Option<(WebhookId, &str)> { Some((webhook_id.parse().ok()?, token)) } -#[cfg(all(feature = "cache", feature = "model"))] -pub(crate) fn user_has_guild_perms( - cache_http: impl CacheHttp, - guild_id: GuildId, - permissions: Permissions, -) -> Result<()> { - if let Some(cache) = cache_http.cache() { - if let Some(guild) = cache.guild(guild_id) { - guild.require_perms(cache, permissions)?; - } - } - Ok(()) -} - -/// Tries to find a user's permissions using the cache. Unlike [`user_has_perms`], this function -/// will return `true` even when the permissions are not in the cache. -#[cfg(all(feature = "cache", feature = "model"))] -#[inline] -pub(crate) fn user_has_perms_cache( - cache: impl AsRef, - channel_id: ChannelId, - required_permissions: Permissions, -) -> Result<()> { - match user_perms(cache, channel_id) { - Ok(perms) => { - if perms.contains(required_permissions) { - Ok(()) - } else { - Err(Error::Model(ModelError::InvalidPermissions { - required: required_permissions, - present: perms, - })) - } - }, - Err(Error::Model(err)) if err.is_cache_err() => Ok(()), - Err(other) => Err(other), - } -} - -#[cfg(all(feature = "cache", feature = "model"))] -pub(crate) fn user_perms(cache: impl AsRef, channel_id: ChannelId) -> Result { - let cache = cache.as_ref(); - - let Some(guild_id) = cache.channels.get(&channel_id).map(|c| *c) else { - return Err(Error::Model(ModelError::ChannelNotFound)); - }; - - let Some(guild) = cache.guild(guild_id) else { - return Err(Error::Model(ModelError::GuildNotFound)); - }; - - let Some(channel) = guild.channels.get(&channel_id) else { - return Err(Error::Model(ModelError::ChannelNotFound)); - }; - - let Some(member) = guild.members.get(&cache.current_user().id) else { - return Err(Error::Model(ModelError::MemberNotFound)); - }; - - Ok(guild.user_permissions_in(channel, member)) -} - /// Calculates the Id of the shard responsible for a guild, given its Id and total number of shards /// used. /// @@ -493,12 +414,21 @@ pub(crate) fn user_perms(cache: impl AsRef, channel_id: ChannelId) -> Res /// use serenity::model::id::GuildId; /// use serenity::utils; /// -/// assert_eq!(utils::shard_id(GuildId::new(81384788765712384), 17), 7); +/// let guild_id = GuildId::new(81384788765712384); +/// let shard_total = std::num::NonZeroU16::new(17).unwrap(); +/// +/// assert_eq!(utils::shard_id(guild_id, shard_total), 7); /// ``` -#[inline] #[must_use] -pub fn shard_id(guild_id: GuildId, shard_count: u32) -> u32 { - ((guild_id.get() >> 22) % (shard_count as u64)) as u32 +pub fn shard_id(guild_id: GuildId, shard_count: NonZeroU16) -> u16 { + ((guild_id.get() >> 22) % u64::from(shard_count.get())) as u16 +} + +pub(crate) fn check_shard_total(total_shards: u16) -> NonZeroU16 { + NonZeroU16::new(total_shards).unwrap_or_else(|| { + warn!("Invalid shard total provided ({total_shards}), defaulting to 1"); + NonZeroU16::MIN + }) } #[cfg(test)] @@ -535,7 +465,7 @@ mod test { #[test] fn test_emoji_parser() { let emoji = parse_emoji("<:name:12345>").unwrap(); - assert_eq!(emoji.name, "name"); + assert_eq!(&*emoji.name, "name"); assert_eq!(emoji.id, 12_345); } diff --git a/src/utils/quick_modal.rs b/src/utils/quick_modal.rs index 1aa2befd4bd..08df17c40b1 100644 --- a/src/utils/quick_modal.rs +++ b/src/utils/quick_modal.rs @@ -1,18 +1,17 @@ -use crate::builder::{ - Builder as _, - CreateActionRow, - CreateInputText, - CreateInteractionResponse, - CreateModal, -}; +use std::borrow::Cow; + +use to_arraystring::ToArrayString; + +use crate::builder::{CreateActionRow, CreateInputText, CreateInteractionResponse, CreateModal}; use crate::client::Context; use crate::collector::ModalInteractionCollector; +use crate::internal::prelude::*; use crate::model::prelude::*; #[cfg(feature = "collector")] pub struct QuickModalResponse { pub interaction: ModalInteraction, - pub inputs: Vec, + pub inputs: FixedArray>, } /// Convenience builder to create a modal, wait for the user to submit and parse the response. @@ -33,15 +32,15 @@ pub struct QuickModalResponse { /// ``` #[cfg(feature = "collector")] #[must_use] -pub struct CreateQuickModal { - title: String, +pub struct CreateQuickModal<'a> { + title: Cow<'a, str>, timeout: Option, - input_texts: Vec, + input_texts: Vec>, } #[cfg(feature = "collector")] -impl CreateQuickModal { - pub fn new(title: impl Into) -> Self { +impl<'a> CreateQuickModal<'a> { + pub fn new(title: impl Into>) -> Self { Self { title: title.into(), timeout: None, @@ -62,7 +61,7 @@ impl CreateQuickModal { /// /// As the `custom_id` field of [`CreateInputText`], just supply an empty string. All custom /// IDs are overwritten by [`CreateQuickModal`] when sending the modal. - pub fn field(mut self, input_text: CreateInputText) -> Self { + pub fn field(mut self, input_text: CreateInputText<'a>) -> Self { self.input_texts.push(input_text); self } @@ -70,14 +69,14 @@ impl CreateQuickModal { /// Convenience method to add a single-line input text field. /// /// Wraps [`Self::field`]. - pub fn short_field(self, label: impl Into) -> Self { + pub fn short_field(self, label: impl Into>) -> Self { self.field(CreateInputText::new(InputTextStyle::Short, label, "")) } /// Convenience method to add a multi-line input text field. /// /// Wraps [`Self::field`]. - pub fn paragraph_field(self, label: impl Into) -> Self { + pub fn paragraph_field(self, label: impl Into>) -> Self { self.field(CreateInputText::new(InputTextStyle::Paragraph, label, "")) } @@ -90,22 +89,22 @@ impl CreateQuickModal { interaction_id: InteractionId, token: &str, ) -> Result, crate::Error> { - let modal_custom_id = interaction_id.get().to_string(); + let modal_custom_id = interaction_id.to_arraystring(); let builder = CreateInteractionResponse::Modal( - CreateModal::new(&modal_custom_id, self.title).components( + CreateModal::new(modal_custom_id.as_str(), self.title).components( self.input_texts .into_iter() .enumerate() .map(|(i, input_text)| { CreateActionRow::InputText(input_text.custom_id(i.to_string())) }) - .collect(), + .collect::>(), ), ); - builder.execute(ctx, (interaction_id, token)).await?; + builder.execute(&ctx.http, interaction_id, token).await?; - let collector = - ModalInteractionCollector::new(&ctx.shard).custom_ids(vec![modal_custom_id]); + let collector = ModalInteractionCollector::new(ctx.shard.clone()) + .custom_ids(vec![FixedString::from_str_trunc(&modal_custom_id)]); let collector = match self.timeout { Some(timeout) => collector.timeout(timeout), @@ -141,7 +140,7 @@ impl CreateQuickModal { .collect(); Ok(Some(QuickModalResponse { - inputs, + inputs: FixedArray::from_vec_trunc(inputs), interaction: modal_interaction, })) } diff --git a/src/utils/token.rs b/src/utils/token.rs index 4a8d8e27c96..6059ebe9296 100644 --- a/src/utils/token.rs +++ b/src/utils/token.rs @@ -27,9 +27,9 @@ use std::{fmt, str}; /// /// Returns a [`InvalidToken`] when one of the above checks fail. The type of failure is not /// specified. -pub fn validate(token: impl AsRef) -> Result<(), InvalidToken> { +pub fn validate(token: &str) -> Result<(), InvalidToken> { // Tokens can be preceded by "Bot " (that's how the Discord API expects them) - let mut parts = token.as_ref().trim_start_matches("Bot ").split('.'); + let mut parts = token.trim_start_matches("Bot ").split('.'); let is_valid = parts.next().is_some_and(|p| !p.is_empty()) && parts.next().is_some_and(|p| !p.is_empty()) diff --git a/tests/test_reaction.rs b/tests/test_reaction.rs index 135deb172f6..1bb82d7ebee 100644 --- a/tests/test_reaction.rs +++ b/tests/test_reaction.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use serenity::model::channel::ReactionType; use serenity::model::id::EmojiId; +use small_fixed_array::FixedString; #[test] fn str_to_reaction_type() { @@ -10,7 +11,7 @@ fn str_to_reaction_type() { let reaction2 = ReactionType::Custom { animated: false, id: EmojiId::new(600404340292059257), - name: Some("customemoji".to_string()), + name: Some(FixedString::from_static_trunc("customemoji")), }; assert_eq!(reaction, reaction2); } @@ -22,7 +23,7 @@ fn str_to_reaction_type_animated() { let reaction2 = ReactionType::Custom { animated: true, id: EmojiId::new(600409340292059257), - name: Some("customemoji2".to_string()), + name: Some(FixedString::from_static_trunc("customemoji2")), }; assert_eq!(reaction, reaction2); } @@ -34,7 +35,7 @@ fn string_to_reaction_type() { let reaction2 = ReactionType::Custom { animated: false, id: EmojiId::new(600404340292059257), - name: Some("customemoji".to_string()), + name: Some(FixedString::from_static_trunc("customemoji")), }; assert_eq!(reaction, reaction2); } @@ -105,7 +106,7 @@ fn json_to_reaction_type() { let value = serde_json::from_str(s).unwrap(); assert!(matches!(value, ReactionType::Unicode(_))); if let ReactionType::Unicode(value) = value { - assert_eq!(value, "foo"); + assert_eq!(&*value, "foo"); } let s = r#"{"name": null}"#;