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 569bde9f0de..8a04f48308f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,8 @@ name: CI on: [push, pull_request] env: - rust_min: 1.74.0 - rust_nightly: nightly-2024-03-09 + rust_min: 1.82.0 + rust_nightly: nightly-2024-11-01 jobs: test: @@ -26,7 +26,9 @@ jobs: - no cache - no gateway - unstable Discord API features - - simd-json + - zlib compression + - zstd compression + - zlib and zstd compression include: - name: beta @@ -38,22 +40,26 @@ 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: framework rustls_backend - name: no gateway - features: model http rustls_backend + features: model 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 + - name: zlib compression + features: default transport_compression_zlib + - name: zstd compression + features: default transport_compression_zstd + - name: zlib and zstd compression + features: default transport_compression_zlib transport_compression_zstd steps: - name: Checkout sources @@ -131,9 +137,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 +213,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 full,unstable env: RUSTDOCFLAGS: -D rustdoc::broken_intra_doc_links @@ -241,32 +243,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 9fe0fa46735..820f4441cb4 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 709f9d74ed1..88dde863974 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ readme = "README.md" repository = "https://github.com/serenity-rs/serenity.git" version = "0.12.4" edition = "2021" -rust-version = "1.74" +rust-version = "1.82.0" include = ["src/**/*", "LICENSE.md", "README.md", "CHANGELOG.md", "build.rs"] [workspace] @@ -22,41 +22,43 @@ bitflags = "2.4.2" serde_json = "1.0.108" async-trait = "0.1.74" tracing = { version = "0.1.40", features = ["log"] } -serde = { version = "1.0.192", features = ["derive"] } +serde = { version = "1.0.192", features = ["derive", "rc"] } 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.36", 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.2.0" +extract_map = { version = "0.1.0", features = ["serde", "iter_mut"] } +aformat = "0.1.3" +bytes = "1.5.0" # 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 } -tokio-tungstenite = { version = "0.21.0", optional = true } -typemap_rev = { version = "0.3.0", optional = true } -bytes = { version = "1.5.0", optional = true } +zstd-safe = { version = "7.2.1", optional = true } +reqwest = { version = "0.12.2", default-features = false, features = ["multipart", "stream", "json"], optional = true } +tokio-tungstenite = { version = "0.24.0", features = ["url"], 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 } +dashmap = { version = "6.1.0", features = ["serde"], 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", "parking_lot", "nonmax", "extract_map_01"] } # serde feature only allows for serialisation, # Serenity workspace crates -command_attr = { version = "0.5.3", 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] @@ -66,49 +68,44 @@ default_native_tls = ["default_no_backend", "native_tls_backend"] # Serenity requires a backend, this picks all default features without a backend. default_no_backend = [ - "builder", "cache", "chrono", - "client", "framework", - "gateway", - "model", - "http", - "standard_framework", - "utils", + "transport_compression_zlib", + "transport_compression_zstd", ] # 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"] +collector = ["gateway"] # Enables the Framework trait which is an abstraction for old-style text commands. -framework = ["client", "model", "utils"] +framework = ["gateway"] # Enables gateway support, which allows bots to listen for Discord events. -gateway = ["flate2"] +gateway = ["model", "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 zlib-stream transport compression of incoming gateway events. +transport_compression_zlib = ["flate2", "gateway"] +# Enables zstd-stream transport compression of incoming gateway events. +transport_compression_zstd = ["zstd-safe", "gateway"] # 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"] +voice = ["gateway"] # Enables unstable tokio features to give explicit names to internally spawned tokio tasks tokio_task_builder = ["tokio/tracing"] interactions_endpoint = ["ed25519-dalek"] @@ -117,33 +114,33 @@ 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", "dashmap/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 rustls_backend = [ "reqwest/rustls-tls", "tokio-tungstenite/rustls-tls-webpki-roots", - "bytes", ] # - Native TLS Backends native_tls_backend = [ "reqwest/native-tls", "tokio-tungstenite/native-tls", - "bytes", ] [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..2378daa0313 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ docs. A basic ping-pong bot looks like: -```rust,ignore +```rust use std::env; use serenity::async_trait; @@ -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 { @@ -98,7 +100,7 @@ tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] } ## MSRV Policy -Serenity's minimum supported Rust version (MSRV) is Rust 1.74. +Serenity's minimum supported Rust version (MSRV) is Rust 1.82. We opt to keep MSRV stable on the `current` branch. This means it will remain unchanged between minor releases. Occasionally, dependencies may violate SemVer @@ -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", ] @@ -245,5 +244,5 @@ a Rust-native cloud development platform that allows deploying Serenity bots for [repo:andesite]: https://github.com/natanbc/andesite [repo:lavaplayer]: https://github.com/sedmelluq/lavaplayer [logo]: https://raw.githubusercontent.com/serenity-rs/serenity/current/logo.png -[rust-version-badge]: https://img.shields.io/badge/rust-1.74.0+-93450a.svg?style=flat-square -[rust-version-link]: https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html +[rust-version-badge]: https://img.shields.io/badge/rust-1.82.0+-93450a.svg?style=flat-square +[rust-version-link]: https://blog.rust-lang.org/2024/10/17/Rust-1.82.0.html 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/build.rs b/build.rs index 725dd621ce6..0b97e08c250 100644 --- a/build.rs +++ b/build.rs @@ -1,10 +1,7 @@ -#[cfg(all( - any(feature = "http", feature = "gateway"), - not(any(feature = "rustls_backend", feature = "native_tls_backend")) -))] +#[cfg(all(feature = "http", not(any(feature = "rustls_backend", feature = "native_tls_backend"))))] compile_error!( - "You have the `http` or `gateway` feature enabled, either the `rustls_backend` or \ - `native_tls_backend` feature must be selected to let Serenity use `http` or `gateway`.\n\ + "You have the `http` feature enabled; either the `rustls_backend` or `native_tls_backend` \ + feature must be enabled to let Serenity make requests over the network.\n\ - `rustls_backend` uses Rustls, a pure Rust TLS-implemenation.\n\ - `native_tls_backend` uses SChannel on Windows, Secure Transport on macOS, and OpenSSL on \ other platforms.\n\ diff --git a/clippy.toml b/clippy.toml index 02718485a9e..cda8d17eed4 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -cognitive-complexity-threshold = 20 +avoid-breaking-exported-api = false diff --git a/command_attr/Cargo.toml b/command_attr/Cargo.toml deleted file mode 100644 index 0104d3a3326..00000000000 --- a/command_attr/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "command_attr" -version = "0.5.3" -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 8664968e3e1..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 fmt::Display for DisplaySlice<'_, 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 f5bc2a5761d..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/Cargo.toml b/examples/e01_basic_ping_bot/Cargo.toml index 7f9cb61751b..211146c8ae6 100644 --- a/examples/e01_basic_ping_bot/Cargo.toml +++ b/examples/e01_basic_ping_bot/Cargo.toml @@ -5,5 +5,5 @@ authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +serenity = { path = "../../", default-features = false, features = ["gateway", "model", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } diff --git a/examples/e01_basic_ping_bot/src/main.rs b/examples/e01_basic_ping_bot/src/main.rs index 166ccae3574..5954373c7d9 100644 --- a/examples/e01_basic_ping_bot/src/main.rs +++ b/examples/e01_basic_ping_bot/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::model::channel::Message; use serenity::model::gateway::Ready; @@ -11,8 +9,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 @@ -37,7 +35,8 @@ impl EventHandler for Handler { #[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 token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // Set gateway intents, which decides what events the bot will be notified about let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES @@ -46,7 +45,7 @@ async fn main() { // Create a new instance of the Client, logging in as a bot. This will automatically prepend // your bot token with "Bot ", which is a requirement by Discord for bot users. let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); // Finally, start a single shard, and start listening to events. // diff --git a/examples/e02_transparent_guild_sharding/Cargo.toml b/examples/e02_transparent_guild_sharding/Cargo.toml index d1fab7917da..eec0c2aff9d 100644 --- a/examples/e02_transparent_guild_sharding/Cargo.toml +++ b/examples/e02_transparent_guild_sharding/Cargo.toml @@ -5,5 +5,5 @@ authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +serenity = { path = "../../", default-features = false, features = ["gateway", "model", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } diff --git a/examples/e02_transparent_guild_sharding/src/main.rs b/examples/e02_transparent_guild_sharding/src/main.rs index 288323e8b29..96215d41817 100644 --- a/examples/e02_transparent_guild_sharding/src/main.rs +++ b/examples/e02_transparent_guild_sharding/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::model::channel::Message; use serenity::model::gateway::Ready; @@ -42,12 +40,13 @@ impl EventHandler for Handler { #[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 token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); // The total number of shards to use. The "current shard number" of a shard - that is, the // shard it is assigned to - is indexed at 0, while the total shard count is indexed at 1. diff --git a/examples/e03_struct_utilities/Cargo.toml b/examples/e03_struct_utilities/Cargo.toml index bcb28fbf983..80548cee27f 100644 --- a/examples/e03_struct_utilities/Cargo.toml +++ b/examples/e03_struct_utilities/Cargo.toml @@ -5,5 +5,5 @@ authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +serenity = { path = "../../", default-features = false, features = ["gateway", "model", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } diff --git a/examples/e03_struct_utilities/src/main.rs b/examples/e03_struct_utilities/src/main.rs index f313cc3d96f..cdbcea2f90f 100644 --- a/examples/e03_struct_utilities/src/main.rs +++ b/examples/e03_struct_utilities/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::builder::CreateMessage; use serenity::model::channel::Message; @@ -19,7 +17,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.id.dm(&context.http, builder).await; if let Err(why) = dm { println!("Error when direct messaging user: {why:?}"); @@ -35,12 +33,13 @@ impl EventHandler for Handler { #[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 token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating 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/e04_message_builder/Cargo.toml b/examples/e04_message_builder/Cargo.toml index 0e36abf0b3d..144b1d0f155 100644 --- a/examples/e04_message_builder/Cargo.toml +++ b/examples/e04_message_builder/Cargo.toml @@ -5,5 +5,5 @@ authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +serenity = { path = "../../", default-features = false, features = ["gateway", "model", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } diff --git a/examples/e04_message_builder/src/main.rs b/examples/e04_message_builder/src/main.rs index e5fd8ce3b0c..03ff0feb6b0 100644 --- a/examples/e04_message_builder/src/main.rs +++ b/examples/e04_message_builder/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::model::channel::Message; use serenity::model::gateway::Ready; @@ -12,7 +10,7 @@ struct Handler; impl EventHandler for Handler { async fn message(&self, context: Context, msg: Message) { if msg.content == "!ping" { - let channel = match msg.channel_id.to_channel(&context).await { + let channel = match msg.channel(&context).await { Ok(channel) => channel, Err(why) => { println!("Error getting channel: {why:?}"); @@ -26,7 +24,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") @@ -46,12 +44,13 @@ impl EventHandler for Handler { #[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 token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating 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/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 d38631a82b7..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/e09_create_message_builder/Cargo.toml b/examples/e05_sample_bot_structure/Cargo.toml similarity index 65% rename from examples/e09_create_message_builder/Cargo.toml rename to examples/e05_sample_bot_structure/Cargo.toml index b130703c887..40c5ec191fe 100644 --- a/examples/e09_create_message_builder/Cargo.toml +++ b/examples/e05_sample_bot_structure/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "e09_create_message_builder" +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", "chrono"] } +serenity = { path = "../../", default-features = false, features = ["collector", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 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 88% rename from examples/e14_slash_commands/src/commands/modal.rs rename to examples/e05_sample_bot_structure/src/commands/modal.rs index 1f3e7f918a1..d46a3d2fcc6 100644 --- a/examples/e14_slash_commands/src/commands/modal.rs +++ b/examples/e05_sample_bot_structure/src/commands/modal.rs @@ -1,7 +1,7 @@ use serenity::builder::*; +use serenity::collector::{CreateQuickModal, QuickModal}; use serenity::model::prelude::*; use serenity::prelude::*; -use serenity::utils::CreateQuickModal; pub async fn run(ctx: &Context, interaction: &CommandInteraction) -> Result<(), serenity::Error> { let modal = CreateQuickModal::new("About you") @@ -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 95% rename from examples/e14_slash_commands/src/main.rs rename to examples/e05_sample_bot_structure/src/main.rs index 70e5cea2a29..83c6ff8ebb7 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(), @@ -72,7 +72,8 @@ impl EventHandler for Handler { #[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 token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // Build our client. let mut client = Client::builder(token, GatewayIntents::empty()) diff --git a/examples/e07_env_logging/Cargo.toml b/examples/e06_env_logging/Cargo.toml similarity index 79% rename from examples/e07_env_logging/Cargo.toml rename to examples/e06_env_logging/Cargo.toml index c81e6b6d031..b06835d181b 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" @@ -10,5 +10,5 @@ tracing-subscriber = "0.3" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } [dependencies.serenity] -features = ["client", "rustls_backend"] +features = ["gateway", "rustls_backend"] path = "../../" 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 51% rename from examples/e07_env_logging/src/main.rs rename to examples/e06_env_logging/src/main.rs index f49348526c8..2c70eae2f43 100644 --- a/examples/e07_env_logging/src/main.rs +++ b/examples/e06_env_logging/src/main.rs @@ -1,11 +1,4 @@ -#![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 +27,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() { @@ -64,32 +40,17 @@ async fn main() { tracing_subscriber::fmt::init(); // 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 token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); 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 73% rename from examples/e08_shard_manager/Cargo.toml rename to examples/e07_shard_manager/Cargo.toml index b11f2acf544..211277318ac 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" @@ -9,5 +9,5 @@ tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "time"] } [dependencies.serenity] default-features = false -features = ["client", "gateway", "rustls_backend", "model"] +features = ["gateway", "model", "rustls_backend"] path = "../../" 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 93% rename from examples/e08_shard_manager/src/main.rs rename to examples/e07_shard_manager/src/main.rs index 53ddcedfd35..9a3c8719706 100644 --- a/examples/e08_shard_manager/src/main.rs +++ b/examples/e07_shard_manager/src/main.rs @@ -19,7 +19,6 @@ //! //! Note that it may take a minute or more for a latency to be recorded or to update, depending on //! how often Discord tells the client to send a heartbeat. -use std::env; use std::time::Duration; use serenity::async_trait; @@ -44,13 +43,14 @@ impl EventHandler for Handler { #[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 token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); // Here we clone a lock to the Shard Manager, and then move it into a new thread. The thread // will unlock the manager and print shards' status on a loop. diff --git a/examples/e14_slash_commands/Cargo.toml b/examples/e08_create_message_builder/Cargo.toml similarity index 71% rename from examples/e14_slash_commands/Cargo.toml rename to examples/e08_create_message_builder/Cargo.toml index 4e73adeb7f2..8e1cc2b35a0 100644 --- a/examples/e14_slash_commands/Cargo.toml +++ b/examples/e08_create_message_builder/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "e14_slash_commands" +name = "e08_create_message_builder" version = "0.1.0" authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "collector"] } +serenity = { path = "../../", default-features = false, features = ["gateway", "model", "chrono", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 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 90% rename from examples/e09_create_message_builder/src/main.rs rename to examples/e08_create_message_builder/src/main.rs index bb7561f1ca6..3432a42a5b9 100644 --- a/examples/e09_create_message_builder/src/main.rs +++ b/examples/e08_create_message_builder/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::builder::{CreateAttachment, CreateEmbed, CreateEmbedFooter, CreateMessage}; use serenity::model::channel::Message; @@ -22,7 +20,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), ]) @@ -51,12 +49,13 @@ impl EventHandler for Handler { #[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 token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating 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/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..ab5603e689c 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 = ["collector", "framework", "rustls_backend"] 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..e2c45dec104 --- /dev/null +++ b/examples/e09_collectors/src/main.rs @@ -0,0 +1,152 @@ +//! 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::time::Duration; + +use serenity::async_trait; +use serenity::collector::{CollectMessages, CollectReactions, 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.id.collect_messages(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 + .id + .collect_reactions(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 = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid 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 61cfdce9bb8..00000000000 --- a/examples/e10_collectors/src/main.rs +++ /dev/null @@ -1,199 +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 73% rename from examples/e11_gateway_intents/Cargo.toml rename to examples/e10_gateway_intents/Cargo.toml index 34ec998c5ca..e92adcc6830 100644 --- a/examples/e11_gateway_intents/Cargo.toml +++ b/examples/e10_gateway_intents/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "e11_gateway_intents" +name = "e10_gateway_intents" version = "0.1.0" authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +serenity = { path = "../../", default-features = false, features = ["gateway", "model", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 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 85% rename from examples/e11_gateway_intents/src/main.rs rename to examples/e10_gateway_intents/src/main.rs index db846bc25ea..7e3e0c8bcd2 100644 --- a/examples/e11_gateway_intents/src/main.rs +++ b/examples/e10_gateway_intents/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::model::channel::Message; use serenity::model::gateway::{Presence, Ready}; @@ -16,7 +14,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"); } @@ -28,7 +31,8 @@ impl EventHandler for Handler { #[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 token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // Intents are a bitflag, bitwise operations can be used to dictate which intents to use let intents = 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..7de92c9e879 --- /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::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 = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid 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 78% rename from examples/e13_parallel_loops/Cargo.toml rename to examples/e12_parallel_loops/Cargo.toml index a682559aa62..7ad44ca6472 100644 --- a/examples/e13_parallel_loops/Cargo.toml +++ b/examples/e12_parallel_loops/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "e13_parallel_loops" +name = "e12_parallel_loops" version = "0.1.0" authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] } +serenity = { path = "../../", default-features = false, features = ["gateway", "model", "cache", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } sys-info = "0.9" chrono = { version = "0.4", default-features = false, features = ["clock"] } 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 87% rename from examples/e13_parallel_loops/src/main.rs rename to examples/e12_parallel_loops/src/main.rs index 14fc79c2737..73a17b67ecf 100644 --- a/examples/e13_parallel_loops/src/main.rs +++ b/examples/e12_parallel_loops/src/main.rs @@ -1,6 +1,4 @@ -use std::env; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; use std::time::Duration; use chrono::offset::Utc; @@ -35,10 +33,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 +40,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 +52,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 +84,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:?}"); }; @@ -106,13 +99,14 @@ fn set_activity_to_current_time(ctx: &Context) { #[tokio::main] async fn main() { - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::GUILDS | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(&token, intents) + let mut client = Client::builder(token, intents) .event_handler(Handler { is_loop_running: AtomicBool::new(false), }) 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 78% rename from examples/e16_sqlite_database/Cargo.toml rename to examples/e13_sqlite_database/Cargo.toml index eeda2a295b1..9cf3e53320f 100644 --- a/examples/e16_sqlite_database/Cargo.toml +++ b/examples/e13_sqlite_database/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "e16_sqlite_database" +name = "e13_sqlite_database" version = "0.1.0" authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +serenity = { path = "../../", default-features = false, features = ["gateway", "model", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] } 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 90% rename from examples/e16_sqlite_database/src/main.rs rename to examples/e13_sqlite_database/src/main.rs index 11e4b500f87..d41cc479dc9 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(); } } } @@ -72,7 +72,8 @@ impl EventHandler for Bot { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // Initiate a connection to the database file, creating the file if required. let database = sqlx::sqlite::SqlitePoolOptions::new() @@ -96,6 +97,6 @@ async fn main() { | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(bot).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(bot).await.expect("Err creating client"); client.start().await.unwrap(); } diff --git a/examples/e17_message_components/Cargo.toml b/examples/e14_message_components/Cargo.toml similarity index 73% rename from examples/e17_message_components/Cargo.toml rename to examples/e14_message_components/Cargo.toml index f34f727f55c..f4882f23e52 100644 --- a/examples/e17_message_components/Cargo.toml +++ b/examples/e14_message_components/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "e17_message_components" +name = "e14_message_components" version = "0.1.0" authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "collector"] } +serenity = { path = "../../", default-features = false, features = ["gateway", "model", "collector", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } dotenv = { version = "0.15.0" } 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 87% rename from examples/e17_message_components/src/main.rs rename to examples/e14_message_components/src/main.rs index c70203354a9..bbc103330db 100644 --- a/examples/e17_message_components/src/main.rs +++ b/examples/e14_message_components/src/main.rs @@ -1,4 +1,4 @@ -use std::env; +use std::borrow::Cow; use std::time::Duration; use dotenv::dotenv; @@ -11,7 +11,9 @@ use serenity::builder::{ CreateSelectMenu, CreateSelectMenuKind, CreateSelectMenuOption, + EditInteractionResponse, }; +use serenity::collector::CollectComponentInteractions; use serenity::futures::StreamExt; use serenity::model::prelude::*; use serenity::prelude::*; @@ -36,16 +38,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 +60,14 @@ 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) + .id + .collect_component_interactions(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,9 +84,9 @@ impl EventHandler for Handler { // Acknowledge the interaction and edit the message interaction .create_response( - &ctx, + &ctx.http, CreateInteractionResponse::UpdateMessage( - CreateInteractionResponseMessage::default() + EditInteractionResponse::default() .content(format!("You chose: **{animal}**\nNow choose a sound!")) .button(sound_button("meow", "🐈".parse().unwrap())) .button(sound_button("woof", "🐕".parse().unwrap())) @@ -106,14 +109,16 @@ impl EventHandler for Handler { // Wait for multiple interactions let mut interaction_stream = - m.await_component_interaction(&ctx.shard).timeout(Duration::from_secs(60 * 3)).stream(); + m.id.collect_component_interactions(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 +133,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() } } @@ -136,7 +141,8 @@ impl EventHandler for Handler { async fn main() { dotenv().ok(); // 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 token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // Build our client. let intents = GatewayIntents::GUILD_MESSAGES 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 81% rename from examples/e18_webhook/Cargo.toml rename to examples/e15_webhook/Cargo.toml index ad995b8f4fe..b3e9f9c8346 100644 --- a/examples/e18_webhook/Cargo.toml +++ b/examples/e15_webhook/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "e18_webhook" +name = "e15_webhook" version = "0.1.0" authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["rustls_backend", "model"] } +serenity = { path = "../../", default-features = false, features = ["model", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 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 94% rename from examples/e18_webhook/src/main.rs rename to examples/e15_webhook/src/main.rs index 946c18ba665..3ea35063e97 100644 --- a/examples/e18_webhook/src/main.rs +++ b/examples/e15_webhook/src/main.rs @@ -5,7 +5,7 @@ use serenity::model::webhook::Webhook; #[tokio::main] async fn main() { // You don't need a token when you are only dealing with webhooks. - let http = Http::new(""); + let http = Http::without_token(); let webhook = Webhook::from_url(&http, "https://discord.com/api/webhooks/133742013374206969/hello-there-oPNtRN5UY5DVmBe7m1N0HE-replace-me-Dw9LRkgq3zI7LoW3Rb-k-q") .await .expect("Replace the webhook with your own"); 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/Cargo.toml b/examples/testing/Cargo.toml index dbea94e6ba6..de539355513 100644 --- a/examples/testing/Cargo.toml +++ b/examples/testing/Cargo.toml @@ -5,6 +5,6 @@ authors = ["my name "] edition = "2018" [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache", "collector"] } +serenity = { path = "../../", default-features = false, features = ["gateway", "model", "cache", "collector", "rustls_backend"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } env_logger = "0.10.0" diff --git a/examples/testing/src/main.rs b/examples/testing/src/main.rs index 76b059526aa..eb0011c8c8b 100644 --- a/examples/testing/src/main.rs +++ b/examples/testing/src/main.rs @@ -1,4 +1,7 @@ +use std::borrow::Cow; + use serenity::builder::*; +use serenity::collector::CollectComponentInteractions; use serenity::model::prelude::*; use serenity::prelude::*; @@ -12,39 +15,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 +55,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 +88,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 +103,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 +115,18 @@ 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) + .id + .collect_component_interactions(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 +134,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 +147,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), @@ -157,12 +163,13 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { ) .await?; } else if msg.content == "assigntags" { - let forum_id = channel_id.to_channel(ctx).await?.guild().unwrap().parent_id.unwrap(); - let forum = forum_id.to_channel(ctx).await?.guild().unwrap(); + let forum_id = msg.guild_channel(&ctx).await?.parent_id.unwrap(); + let forum = forum_id.to_guild_channel(&ctx, msg.guild_id).await?; 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 +177,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,47 +193,46 @@ 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.id.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 mut channel = { + let channel_id = channel.trim().parse::().unwrap(); + channel_id.to_guild_channel(&ctx, msg.guild_id).await.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" { - channel_id.say(ctx, format!("{:?}", msg.author_permissions(ctx))).await?; + channel_id.say(&ctx.http, format!("{:?}", msg.author_permissions(&ctx.cache))).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(()) } @@ -238,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?; @@ -265,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)), ) @@ -273,7 +280,7 @@ async fn interaction( } else if interaction.data.name == "unifiedattachments1" { interaction .create_response( - ctx, + &ctx.http, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new().content("works"), ), @@ -281,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") @@ -329,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", @@ -380,10 +389,9 @@ 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"), + CreateAutocompleteResponse::new().add_choice("suggestion"), ), ) .await @@ -408,7 +416,8 @@ async fn main() -> Result<(), serenity::Error> { } env_logger::init(); - let token = std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT; 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..b4527230d6a 100644 --- a/examples/testing/src/model_type_sizes.rs +++ b/examples/testing/src/model_type_sizes.rs @@ -125,13 +125,12 @@ 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::()), ("ModalInteraction", std::mem::size_of::()), ("ModalInteractionData", std::mem::size_of::()), - ("Options", std::mem::size_of::()), + ("AuditLogEntryOptions", std::mem::size_of::()), ("PartialChannel", std::mem::size_of::()), ("PartialCurrentApplicationInfo", std::mem::size_of::()), ("PartialGuild", std::mem::size_of::()), @@ -157,7 +156,7 @@ pub fn print_ranking() { ("Role", std::mem::size_of::()), ("RoleId", std::mem::size_of::()), ("RoleTags", std::mem::size_of::()), - ("Rule", std::mem::size_of::()), + ("AutoModRule", std::mem::size_of::()), ("RuleId", std::mem::size_of::()), ("ScheduledEvent", std::mem::size_of::()), ("ScheduledEventId", 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..90920e58d89 100644 --- a/src/builder/bot_auth_parameters.rs +++ b/src/builder/bot_auth_parameters.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use arrayvec::ArrayVec; use url::Url; @@ -10,15 +12,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 +29,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 +80,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 +90,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 +102,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 de7f346fe7a..092bf9778c5 100644 --- a/src/builder/create_attachment.rs +++ b/src/builder/create_attachment.rs @@ -1,41 +1,38 @@ +use std::borrow::Cow; use std::path::Path; +use bytes::Bytes; +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")] +#[cfg(doc)] use crate::error::Error; use crate::error::Result; #[cfg(feature = "http")] use crate::http::Http; +use crate::model::channel::Message; use crate::model::id::AttachmentId; /// A builder for creating a new attachment from a file path, file data, or URL. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#attachment-object-attachment-structure). -#[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: Bytes, } -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, } } @@ -44,19 +41,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. @@ -64,7 +65,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?; @@ -75,18 +76,15 @@ 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?; - let data = response.bytes().await?.to_vec(); - - let filename = url - .path_segments() - .and_then(Iterator::last) - .ok_or_else(|| Error::Url(url.to_string()))?; + 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?; Ok(CreateAttachment::bytes(data, filename)) } @@ -106,21 +104,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), } @@ -146,7 +143,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?; @@ -157,7 +154,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?; @@ -168,7 +165,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?; @@ -179,14 +176,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 @@ -235,7 +231,7 @@ impl EditAttachments { /// /// Opposite of [`Self::keep`]. pub fn remove(mut self, id: AttachmentId) -> Self { - #[allow(clippy::match_like_matches_macro)] // `matches!` is less clear here + #[expect(clippy::match_like_matches_macro)] // `matches!` is less clear here self.new_and_existing_attachments.retain(|a| match a { NewOrExisting::Existing(a) if a.id == id => false, _ => true, @@ -244,8 +240,8 @@ 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 { + #[expect(clippy::should_implement_trait)] // Clippy thinks add == std::ops::Add::add + pub fn add(mut self, attachment: CreateAttachment<'a>) -> Self { self.new_and_existing_attachments.push(NewOrExisting::New(attachment)); self } @@ -253,30 +249,53 @@ 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; - + #[cfg(feature = "http")] + 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 Serialize for EditAttachments<'_> { + 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 12fee5eca05..a1775ee0fc9 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 } @@ -167,12 +169,12 @@ impl<'a> CreateChannel<'a> { /// Inheriting permissions from an existing channel: /// /// ```rust,no_run - /// # use serenity::{http::Http, model::guild::Guild}; + /// # use serenity::{http::Http, model::id::GuildId}; /// # use std::sync::Arc; /// # /// # async fn run() -> Result<(), Box> { /// # let http: Http = unimplemented!(); - /// # let mut guild: Guild = unimplemented!(); + /// # let mut guild_id: GuildId = unimplemented!(); /// use serenity::builder::CreateChannel; /// use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType}; /// use serenity::model::id::UserId; @@ -186,12 +188,12 @@ impl<'a> CreateChannel<'a> { /// }]; /// /// let builder = CreateChannel::new("my_new_cool_channel").permissions(permissions); - /// guild.create_channel(&http, builder).await?; + /// guild_id.create_channel(&http, builder).await?; /// # 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 Builder for CreateChannel<'_> { - type Context<'ctx> = GuildId; - type Built = GuildChannel; /// Creates a new [`Channel`] in the guild. /// @@ -266,18 +261,11 @@ impl Builder for CreateChannel<'_> { /// /// # 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 c32795f8d39..281eada6845 100644 --- a/src/builder/create_command.rs +++ b/src/builder/create_command.rs @@ -1,8 +1,9 @@ +use std::borrow::Cow; +use std::collections::HashMap; + +use crate::builder::EditCommand; #[cfg(feature = "http")] -use super::Builder; -#[cfg(feature = "http")] -use crate::http::CacheHttp; -use crate::internal::prelude::*; +use crate::http::Http; use crate::model::prelude::*; /// A builder for creating a new [`CommandOption`]. @@ -14,17 +15,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 +65,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 +95,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 +108,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 +124,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 +136,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 +144,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 +155,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 +170,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 +185,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 +200,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 +211,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 +233,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 +249,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 +262,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 +303,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 +312,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 } @@ -290,57 +320,32 @@ impl CreateCommandOption { /// A builder for creating a new [`Command`]. /// -/// [`Self::name`] and [`Self::description`] are required fields. -/// /// [`Command`]: crate::model::application::Command /// /// Discord docs: -/// - [global command](https://discord.com/developers/docs/interactions/application-commands#create-global-application-command-json-params) -/// - [guild command](https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command-json-params) +/// - [global command](https://discord.com/developers/docs/interactions/application-commands#create-global-application-command) +/// - [guild command](https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command) #[derive(Clone, Debug, Serialize)] #[must_use] -pub struct CreateCommand { - name: String, - name_localizations: HashMap, - #[serde(skip_serializing_if = "Option::is_none")] - description: Option, - description_localizations: HashMap, - options: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - default_member_permissions: Option, - #[serde(skip_serializing_if = "Option::is_none")] - dm_permission: Option, +pub struct CreateCommand<'a> { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "type")] kind: Option, #[serde(skip_serializing_if = "Option::is_none")] - integration_types: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - contexts: Option>, - nsfw: bool, - #[serde(skip_serializing_if = "Option::is_none")] handler: Option, + + #[serde(flatten)] + fields: EditCommand<'a>, } -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, - - name: name.into(), - name_localizations: HashMap::new(), - description: None, - description_localizations: HashMap::new(), - default_member_permissions: None, - dm_permission: None, - - integration_types: None, - contexts: None, - - options: Vec::new(), - nsfw: false, handler: None, + + fields: EditCommand::new().name(name), } } @@ -350,22 +355,25 @@ 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 { - self.name = name.into(); + pub fn name(mut self, name: impl Into>) -> Self { + self.fields = self.fields.name(name); self } /// Specifies a localized name of the application command. /// /// ```rust - /// # serenity::builder::CreateCommand::new("") - /// .name("birthday") + /// # serenity::builder::CreateCommand::new("birthday") /// .name_localized("zh-CN", "生日") /// .name_localized("el", "γενέθλια") /// # ; /// ``` - pub fn name_localized(mut self, locale: impl Into, name: impl Into) -> Self { - self.name_localizations.insert(locale.into(), name.into()); + pub fn name_localized( + mut self, + locale: impl Into>, + name: impl Into>, + ) -> Self { + self.fields = self.fields.name_localized(locale, name); self } @@ -377,85 +385,85 @@ 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.fields = self.fields.default_member_permissions(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.fields = self.fields.dm_permission(enabled); self } /// 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 { - self.description = Some(description.into()); + pub fn description(mut self, description: impl Into>) -> Self { + self.fields = self.fields.description(description); self } /// Specifies a localized description of the application command. /// /// ```rust - /// # serenity::builder::CreateCommand::new("") + /// # serenity::builder::CreateCommand::new("birthday") /// .description("Wish a friend a happy birthday") /// .description_localized("zh-CN", "祝你朋友生日快乐") /// # ; /// ``` 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.fields = self.fields.description_localized(locale, description); self } /// 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.fields = self.fields.add_option(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.fields = self.fields.set_options(options); self } /// 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.fields = self.fields.add_integration_type(integration_type); self } /// 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.fields = self.fields.integration_types(integration_types); self } /// 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.fields = self.fields.add_context(context); self } /// Sets the interaction contexts that this application command can be used in. pub fn contexts(mut self, contexts: Vec) -> Self { - self.contexts = Some(contexts); + self.fields = self.fields.contexts(contexts); self } /// Whether this command is marked NSFW (age-restricted) pub fn nsfw(mut self, nsfw: bool) -> Self { - self.nsfw = nsfw; + self.fields = self.fields.nsfw(nsfw); self } @@ -467,21 +475,12 @@ impl CreateCommand { self.handler = Some(handler); 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. + /// Create a [`Command`], overwriting an existing one with the same name if it exists. /// /// Providing a [`GuildId`] will create a command in the corresponding [`Guild`]. Otherwise, a /// global command will be created. /// - /// Providing a [`CommandId`] will edit the corresponding command. - /// /// # Errors /// /// Returns [`Error::Http`] if invalid data is given. See [Discord's docs] for more details. @@ -489,19 +488,19 @@ 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( - self, - cache_http: impl CacheHttp, - ctx: Self::Context<'_>, - ) -> Result { - let http = cache_http.http(); - match ctx { - (Some(guild_id), Some(cmd_id)) => { - http.edit_guild_command(guild_id, cmd_id, &self).await - }, - (Some(guild_id), None) => http.create_guild_command(guild_id, &self).await, - (None, Some(cmd_id)) => http.edit_global_command(cmd_id, &self).await, - (None, None) => http.create_global_command(&self).await, + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, guild_id: Option) -> Result { + match guild_id { + Some(guild_id) => http.create_guild_command(guild_id, &self).await, + None => http.create_global_command(&self).await, } } } + +#[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 4d9858e2a0f..a2fc8842a92 100644 --- a/src/builder/create_components.rs +++ b/src/builder/create_components.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use serde::Serialize; use crate::model::prelude::*; @@ -5,16 +7,30 @@ 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(Cow<'a, [CreateButton<'a>]>), + SelectMenu(CreateSelectMenu<'a>), /// Only valid in modals! - InputText(CreateInputText), + InputText(CreateInputText<'a>), } -impl serde::Serialize for CreateActionRow { +impl<'a> CreateActionRow<'a> { + pub fn buttons(buttons: impl Into]>>) -> Self { + Self::Buttons(buttons.into()) + } + + pub fn select_menu(select_menu: impl Into>) -> Self { + Self::SelectMenu(select_menu.into()) + } + + pub fn input_text(input_text: impl Into>) -> Self { + Self::InputText(input_text.into()) + } +} + +impl serde::Serialize for CreateActionRow<'_> { fn serialize(&self, serializer: S) -> Result { use serde::ser::SerializeMap as _; @@ -32,68 +48,84 @@ 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")] + custom_id: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + sku_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + label: 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, + sku_id: None, label: None, emoji: None, disabled: false, - }) + } } /// Creates a new premium button associated with the given SKU. /// /// Clicking this button _will not_ trigger an interaction event in your bot. pub fn new_premium(sku_id: impl Into) -> Self { - Self(Button { + Self { + style: ButtonStyle::Unknown(6), kind: ComponentType::Button, - data: ButtonKind::Premium { - sku_id: sku_id.into(), - }, - label: None, + url: None, + custom_id: None, emoji: None, + label: None, + sku_id: Some(sku_id.into()), 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, + url: None, + custom_id: Some(custom_id.into()), + sku_id: 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 and premium 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 } @@ -101,37 +133,57 @@ impl CreateButton { /// /// Has no effect on link buttons and premium 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 } } -impl From