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..41939a9c01c 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.85.0 + rust_nightly: nightly-2025-02-19 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 dc4828d7dd5..242448381fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,55 +23,53 @@ homepage = "https://github.com/serenity-rs/serenity" repository = "https://github.com/serenity-rs/serenity.git" keywords = ["discord", "api"] license = "ISC" -edition = "2021" -rust-version = "1.74" +edition = "2024" +rust-version = "1.85" [dependencies] -#!! Downgrade to versions still supporting v1.74.0 -#!! Make sure to remove on `next` -litemap = "=0.7.4" -zerofrom = "=0.1.5" - # Required dependencies bitflags = "2.4.2" -serde_json = "1.0.108" +serde_json = { version = "1.0.108", features = ["raw_value"] } 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"] } time = { version = "0.3.36", 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.3.0", features = ["serde"] } +aformat = "0.1.3" +bytes = "1.5.0" +ref-cast = "1.0.23" # 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 } +foldhash = { version = "0.1.4", 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.26.1", 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", features = ["serde"] } 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.13", optional = true, features = ["url", "time", "serde_json", "secrecy", "parking_lot", "nonmax"] } # 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] @@ -81,49 +79,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 = ["foldhash", "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", "dashmap"] # 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"] @@ -132,33 +125,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", "extract_map/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..a4c0967cdd7 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,12 @@ Serenity supports bot login via the use of [`Client::builder`]. You may also check your tokens prior to login via the use of [`validate_token`]. -Once logged in, you may add handlers to your client to dispatch [`Event`]s, -by implementing the handlers in a trait, such as [`EventHandler::message`]. -This will cause your handler to be called when a [`Event::MessageCreate`] is -received. Each handler is given a [`Context`], giving information about the -event. See the [client's module-level documentation]. +Once logged in, you can add an event handler to your client to dispatch [`Event`]s. +This will allow you to recieve and handle events as you see fit. For example, an +[`Event::MessageCreate`] event will be dispatched to you when a message is sent. +Every event will give you access to a [`Context`], giving information about the event. +See the [client's module-level documentation]. + The [`Shard`] is transparently handled by the library, removing unnecessary complexity. Sharded connections are automatically handled for @@ -41,7 +42,7 @@ docs. A basic ping-pong bot looks like: -```rust,ignore +```rust use std::env; use serenity::async_trait; @@ -52,7 +53,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 +72,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 +101,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.85. 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 +128,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 +154,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 +191,6 @@ features = [ "gateway", "http", "model", - "standard_framework", "utils", "rustls_backend", ] @@ -216,7 +216,6 @@ a Rust-native cloud development platform that allows deploying Serenity bots for [`Cache`]: https://docs.rs/serenity/*/serenity/cache/struct.Cache.html [`Client::builder`]: https://docs.rs/serenity/*/serenity/client/struct.Client.html#method.builder -[`EventHandler::message`]: https://docs.rs/serenity/*/serenity/client/trait.EventHandler.html#method.message [`Context`]: https://docs.rs/serenity/*/serenity/client/struct.Context.html [`Event`]: https://docs.rs/serenity/*/serenity/model/event/enum.Event.html [`Event::MessageCreate`]: https://docs.rs/serenity/*/serenity/model/event/enum.Event.html#variant.MessageCreate @@ -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.85.0+-93450a.svg?style=flat-square +[rust-version-link]: https://blog.rust-lang.org/2025/02/20/Rust-1.85.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 5dd86f1c878..00000000000 --- a/command_attr/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "command_attr" -version = "0.5.3" -authors = ["Alex M. M. "] -description = "Procedural macros for command creation for the Serenity library." - -documentation.workspace = true -homepage.workspace = true -repository.workspace = true -keywords.workspace = true -license.workspace = true -edition.workspace = true -rust-version.workspace = true - -[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 df620c3133a..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().is_some_and(|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 2bca1b5a439..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..b030477b41e 100644 --- a/examples/e01_basic_ping_bot/Cargo.toml +++ b/examples/e01_basic_ping_bot/Cargo.toml @@ -2,8 +2,8 @@ name = "e01_basic_ping_bot" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [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..4f5594375e0 100644 --- a/examples/e01_basic_ping_bot/src/main.rs +++ b/examples/e01_basic_ping_bot/src/main.rs @@ -1,43 +1,52 @@ -use std::env; - use serenity::async_trait; -use serenity::model::channel::Message; -use serenity::model::gateway::Ready; +use serenity::gateway::client::FullEvent; use serenity::prelude::*; struct Handler; #[async_trait] 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. - 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 - // of permissions to post in the channel, so log to stdout when some error happens, - // with a description of it. - if let Err(why) = msg.channel_id.say(&ctx.http, "Pong!").await { - println!("Error sending message: {why:?}"); - } - } - } + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + // 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. + FullEvent::Message { + new_message, .. + } => { + if new_message.content == "!ping" { + // Sending a message can fail, due to a network error, an authentication error, + // or lack of permissions to post in the channel, so log to + // stdout when some error happens, with a description of it. + if let Err(why) = new_message.channel_id.say(&ctx.http, "Pong!").await { + println!("Error sending message: {why:?}"); + } + } + }, - // Set a handler to be called on the `ready` event. This is called when a shard is booted, and - // a READY payload is sent by Discord. This payload contains data like the current user's guild - // Ids, current user data, private channels, and more. - // - // In this case, just print what the current user's username is. - async fn ready(&self, _: Context, ready: Ready) { - println!("{} is connected!", ready.user.name); + // Set a handler to be called on the `ready` event. This is called when a shard is + // booted, and a READY payload is sent by Discord. This payload contains + // data like the current user's guild Ids, current user data, private + // channels, and more. + // + // In this case, just print what the current user's username is. + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.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 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 +55,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..45ad9ee9094 100644 --- a/examples/e02_transparent_guild_sharding/Cargo.toml +++ b/examples/e02_transparent_guild_sharding/Cargo.toml @@ -2,8 +2,8 @@ name = "e02_transparent_guild_sharding" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [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..39ca71505be 100644 --- a/examples/e02_transparent_guild_sharding/src/main.rs +++ b/examples/e02_transparent_guild_sharding/src/main.rs @@ -1,8 +1,5 @@ -use std::env; - use serenity::async_trait; -use serenity::model::channel::Message; -use serenity::model::gateway::Ready; +use serenity::gateway::client::FullEvent; use serenity::prelude::*; // Serenity implements transparent sharding in a way that you do not need to handle separate @@ -24,30 +21,37 @@ struct Handler; #[async_trait] impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - if msg.content == "!ping" { - println!("Shard {}", ctx.shard_id); - - if let Err(why) = msg.channel_id.say(&ctx.http, "Pong!").await { - println!("Error sending message: {why:?}"); - } + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + FullEvent::Message { + new_message, .. + } => { + if new_message.content == "!ping" { + if let Err(why) = new_message.channel_id.say(&ctx.http, "Pong!").await { + println!("Error sending message: {why:?}"); + } + } + }, + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.user.name); + }, + _ => {}, } } - - 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 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..b2c99e3b1ad 100644 --- a/examples/e03_struct_utilities/Cargo.toml +++ b/examples/e03_struct_utilities/Cargo.toml @@ -2,8 +2,8 @@ name = "e03_struct_utilities" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [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..5b6ec331099 100644 --- a/examples/e03_struct_utilities/src/main.rs +++ b/examples/e03_struct_utilities/src/main.rs @@ -1,46 +1,54 @@ -use std::env; - use serenity::async_trait; use serenity::builder::CreateMessage; -use serenity::model::channel::Message; -use serenity::model::gateway::Ready; +use serenity::gateway::client::FullEvent; use serenity::prelude::*; struct Handler; #[async_trait] impl EventHandler for Handler { - async fn message(&self, context: Context, msg: Message) { - if msg.content == "!messageme" { - // If the `utils`-feature is enabled, then model structs will have a lot of useful - // methods implemented, to avoid using an often otherwise bulky Context, or even much - // lower-level `rest` method. - // - // 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; + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + FullEvent::Message { + new_message, .. + } => { + if new_message.content == "!messageme" { + // If the `utils`-feature is enabled, then model structs will have a lot of + // useful methods implemented, to avoid using an often + // otherwise bulky Context, or even much lower-level `rest` + // method. + // + // 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 = new_message.author.id.dm(&ctx.http, builder).await; - if let Err(why) = dm { - println!("Error when direct messaging user: {why:?}"); - } - } - } + if let Err(why) = dm { + println!("Error when direct messaging user: {why:?}"); + } + } + }, - async fn ready(&self, _: Context, ready: Ready) { - println!("{} is connected!", ready.user.name); + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.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 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..d5dd03de589 100644 --- a/examples/e04_message_builder/Cargo.toml +++ b/examples/e04_message_builder/Cargo.toml @@ -2,8 +2,8 @@ name = "e04_message_builder" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [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..c600d93a442 100644 --- a/examples/e04_message_builder/src/main.rs +++ b/examples/e04_message_builder/src/main.rs @@ -1,8 +1,5 @@ -use std::env; - use serenity::async_trait; -use serenity::model::channel::Message; -use serenity::model::gateway::Ready; +use serenity::gateway::client::FullEvent; use serenity::prelude::*; use serenity::utils::MessageBuilder; @@ -10,48 +7,57 @@ struct Handler; #[async_trait] 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 { - Ok(channel) => channel, - Err(why) => { - println!("Error getting channel: {why:?}"); + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + FullEvent::Message { + new_message, .. + } => { + if new_message.content == "!ping" { + let channel = match new_message.channel(ctx).await { + Ok(channel) => channel, + Err(why) => { + println!("Error getting channel: {why:?}"); - return; - }, - }; + return; + }, + }; - // The message builder allows for creating a message by mentioning users dynamically, - // pushing "safe" versions of content (such as bolding normalized content), displaying - // emojis, and more. - let response = MessageBuilder::new() - .push("User ") - .push_bold_safe(&msg.author.name) - .push(" used the 'ping' command in the ") - .mention(&channel) - .push(" channel") - .build(); + // The message builder allows for creating a message by mentioning users + // dynamically, pushing "safe" versions of content (such as + // bolding normalized content), displaying emojis, and more. + let response = MessageBuilder::new() + .push("User ") + .push_bold_safe(new_message.author.name.as_str()) + .push(" used the 'ping' command in the ") + .mention(&channel) + .push(" channel") + .build(); - if let Err(why) = msg.channel_id.say(&context.http, &response).await { - println!("Error sending message: {why:?}"); - } + if let Err(why) = new_message.channel_id.say(&ctx.http, &response).await { + println!("Error sending message: {why:?}"); + } + } + }, + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.user.name); + }, + _ => {}, } } - - 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 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/e11_gateway_intents/Cargo.toml b/examples/e05_sample_bot_structure/Cargo.toml similarity index 68% rename from examples/e11_gateway_intents/Cargo.toml rename to examples/e05_sample_bot_structure/Cargo.toml index 34ec998c5ca..70e58866471 100644 --- a/examples/e11_gateway_intents/Cargo.toml +++ b/examples/e05_sample_bot_structure/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "e11_gateway_intents" +name = "e05_sample_bot_structure" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +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/e05_sample_bot_structure/src/main.rs b/examples/e05_sample_bot_structure/src/main.rs new file mode 100644 index 00000000000..f2eab860827 --- /dev/null +++ b/examples/e05_sample_bot_structure/src/main.rs @@ -0,0 +1,108 @@ +mod commands; + +use std::env; + +use serenity::async_trait; +use serenity::builder::{CreateInteractionResponse, CreateInteractionResponseMessage}; +use serenity::gateway::client::FullEvent; +use serenity::model::application::{Command, Interaction}; +use serenity::model::id::GuildId; +use serenity::prelude::*; + +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + // clippy can't decide between if it wants it collapsed, or if it wants you to use if let + // because its a single pattern. + #[expect(clippy::collapsible_match)] + match event { + FullEvent::InteractionCreate { + interaction, .. + } => { + if let Interaction::Command(command) = interaction { + println!("Received command interaction: {command:#?}"); + + let content = match command.data.name.as_str() { + "ping" => Some(commands::ping::run(&command.data.options())), + "id" => Some(commands::id::run(&command.data.options())), + "attachmentinput" => { + Some(commands::attachmentinput::run(&command.data.options())) + }, + "modal" => { + commands::modal::run(ctx, command).await.unwrap(); + None + }, + _ => Some("not implemented :(".to_string()), + }; + + if let Some(content) = content { + let data = CreateInteractionResponseMessage::new().content(content); + let builder = CreateInteractionResponse::Message(data); + if let Err(why) = command.create_response(&ctx.http, builder).await { + println!("Cannot respond to slash command: {why}"); + } + } + } + }, + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.user.name); + + let guild_id = GuildId::new( + env::var("GUILD_ID") + .expect("Expected GUILD_ID in environment") + .parse() + .expect("GUILD_ID must be an integer"), + ); + + let commands = guild_id + .set_commands(&ctx.http, &[ + commands::ping::register(), + commands::id::register(), + commands::welcome::register(), + commands::numberinput::register(), + commands::attachmentinput::register(), + commands::modal::register(), + ]) + .await; + + println!("I now have the following guild slash commands: {commands:#?}"); + + let guild_command = Command::create_global_command( + &ctx.http, + commands::wonderful_command::register(), + ) + .await; + + println!("I created the following global slash command: {guild_command:#?}"); + + println!("{} is connected!", data_about_bot.user.name); + }, + _ => {}, + } + } +} + +#[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"); + + // Build our client. + let mut client = Client::builder(token, GatewayIntents::empty()) + .event_handler(Handler) + .await + .expect("Error creating client"); + + // Finally, start a single shard, and start listening to events. + // + // Shards will automatically attempt to reconnect, and will perform exponential backoff until + // it reconnects. + if let Err(why) = client.start().await { + println!("Client error: {why:?}"); + } +} diff --git a/examples/e07_env_logging/Cargo.toml b/examples/e06_env_logging/Cargo.toml similarity index 72% rename from examples/e07_env_logging/Cargo.toml rename to examples/e06_env_logging/Cargo.toml index c81e6b6d031..296d77803a0 100644 --- a/examples/e07_env_logging/Cargo.toml +++ b/examples/e06_env_logging/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "e07_env_logging" +name = "e06_env_logging" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [dependencies] tracing = "0.1.23" @@ -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/e06_env_logging/src/main.rs b/examples/e06_env_logging/src/main.rs new file mode 100644 index 00000000000..53aa3850681 --- /dev/null +++ b/examples/e06_env_logging/src/main.rs @@ -0,0 +1,59 @@ +use serenity::async_trait; +use serenity::prelude::*; +use tracing::{debug, error, info, instrument}; + +struct Handler; + +use serenity::gateway::client::FullEvent; + +#[async_trait] +impl EventHandler for Handler { + async fn dispatch(&self, _: &Context, event: &FullEvent) { + match event { + FullEvent::Ready { + data_about_bot, .. + } => { + // Log at the INFO level. This is a macro from the `tracing` crate. + info!("{} is connected!", data_about_bot.user.name); + }, + FullEvent::Resume { + .. + } => { + // Log at the DEBUG level. + // + // In this example, this will not show up in the logs because DEBUG is + // below INFO, which is the set debug level. + debug!("Resumed"); + }, + _ => {}, + } + } +} + +#[tokio::main] +#[instrument] +async fn main() { + // Call tracing_subscriber's initialize function, which configures `tracing` via environment + // variables. + // + // For example, you can say to log all levels INFO and up via setting the environment variable + // `RUST_LOG` to `INFO`. + // + // This environment variable is already preset if you use cargo-make to run the example. + tracing_subscriber::fmt::init(); + + // 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; + + 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); + } +} 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/e07_env_logging/src/main.rs b/examples/e07_env_logging/src/main.rs deleted file mode 100644 index f49348526c8..00000000000 --- a/examples/e07_env_logging/src/main.rs +++ /dev/null @@ -1,95 +0,0 @@ -#![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::*; -use tracing::{debug, error, info, instrument}; - -struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn ready(&self, _: Context, ready: Ready) { - // Log at the INFO level. This is a macro from the `tracing` crate. - info!("{} is connected!", ready.user.name); - } - - // For instrument to work, all parameters must implement Debug. - // - // Handler doesn't implement Debug here, so we specify to skip that argument. - // Context doesn't implement Debug either, so it is also skipped. - #[instrument(skip(self, _ctx))] - async fn resume(&self, _ctx: Context, _resume: ResumedEvent) { - // Log at the DEBUG level. - // - // In this example, this will not show up in the logs because DEBUG is - // below INFO, which is the set debug level. - debug!("Resumed"); - } -} - -#[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() { - // Call tracing_subscriber's initialize function, which configures `tracing` via environment - // variables. - // - // For example, you can say to log all levels INFO and up via setting the environment variable - // `RUST_LOG` to `INFO`. - // - // This environment variable is already preset if you use cargo-make to run the example. - 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 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"); - - 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/e08_shard_manager/Cargo.toml b/examples/e07_shard_manager/Cargo.toml similarity index 68% rename from examples/e08_shard_manager/Cargo.toml rename to examples/e07_shard_manager/Cargo.toml index b11f2acf544..f37ada05b1a 100644 --- a/examples/e08_shard_manager/Cargo.toml +++ b/examples/e07_shard_manager/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "e08_shard_manager" +name = "e07_shard_manager" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [dependencies] 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 65% rename from examples/e08_shard_manager/src/main.rs rename to examples/e07_shard_manager/src/main.rs index 53ddcedfd35..7c816782e27 100644 --- a/examples/e08_shard_manager/src/main.rs +++ b/examples/e07_shard_manager/src/main.rs @@ -7,8 +7,7 @@ //! //! This isn't particularly useful for small bots, but is useful for large bots that may need to //! split load on separate VPSs or dedicated servers. Additionally, Discord requires that there be -//! at least one shard for every -//! 2500 guilds that a bot is on. +//! at least one shard for every 2500 guilds that a bot is on. //! //! For the purposes of this example, we'll print the current statuses of the two shards to the //! terminal every 30 seconds. This includes the ID of the shard, the current connection stage, @@ -19,11 +18,10 @@ //! //! 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; -use serenity::model::gateway::Ready; +use serenity::gateway::client::FullEvent; use serenity::prelude::*; use tokio::time::sleep; @@ -31,12 +29,20 @@ struct Handler; #[async_trait] impl EventHandler for Handler { - async fn ready(&self, _: Context, ready: Ready) { - if let Some(shard) = ready.shard { - // Note that array index 0 is 0-indexed, while index 1 is 1-indexed. - // - // This may seem unintuitive, but it models Discord's behaviour. - println!("{} is connected on shard {}/{}!", ready.user.name, shard.id, shard.total); + async fn dispatch(&self, _: &Context, event: &FullEvent) { + if let FullEvent::Ready { + data_about_bot, .. + } = event + { + if let Some(shard) = data_about_bot.shard { + // Note that array index 0 is 0-indexed, while index 1 is 1-indexed. + // + // This may seem unintuitive, but it models Discord's behaviour. + println!( + "{} is connected on shard {}/{}!", + data_about_bot.user.name, shard.id, shard.total + ); + } } } } @@ -44,25 +50,24 @@ 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. - let manager = client.shard_manager.clone(); + // Here we get a DashMap of of the shards' status that we move into a new thread. + let runners = client.shard_manager.runners.clone(); tokio::spawn(async move { loop { sleep(Duration::from_secs(30)).await; - let shard_runners = manager.runners.lock().await; - - for (id, runner) in shard_runners.iter() { + for entry in runners.iter() { + let (id, (runner, _)) = entry.pair(); println!( "Shard ID {} is {} with a latency of {:?}", id, runner.stage, runner.latency, diff --git a/examples/e09_create_message_builder/Cargo.toml b/examples/e08_create_message_builder/Cargo.toml similarity index 65% rename from examples/e09_create_message_builder/Cargo.toml rename to examples/e08_create_message_builder/Cargo.toml index b130703c887..d252397755f 100644 --- a/examples/e09_create_message_builder/Cargo.toml +++ b/examples/e08_create_message_builder/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "e09_create_message_builder" +name = "e08_create_message_builder" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "chrono"] } +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/e08_create_message_builder/src/main.rs b/examples/e08_create_message_builder/src/main.rs new file mode 100644 index 00000000000..1bfb09fef86 --- /dev/null +++ b/examples/e08_create_message_builder/src/main.rs @@ -0,0 +1,71 @@ +use serenity::async_trait; +use serenity::builder::{CreateAttachment, CreateEmbed, CreateEmbedFooter, CreateMessage}; +use serenity::gateway::client::FullEvent; +use serenity::model::Timestamp; +use serenity::prelude::*; + +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + FullEvent::Message { + new_message, .. + } => { + if new_message.content == "!hello" { + // The create message builder allows you to easily create embeds and messages + // using a builder syntax. + // This example will create a message that says "Hello, World!", with an embed + // that has a title, description, an image, three fields, + // and a footer. + let footer = CreateEmbedFooter::new("This is a footer"); + let embed = CreateEmbed::new() + .title("This is a title") + .description("This is a description") + .image("attachment://ferris_eyes.png") + .fields([ + ("This is the first field", "This is a field body", true), + ("This is the second field", "Both fields are inline", true), + ]) + .field("This is the third field", "This is not an inline field", false) + .footer(footer) + // Add a timestamp for the current time + // This also accepts a rfc3339 Timestamp + .timestamp(Timestamp::now()); + let builder = CreateMessage::new() + .content("Hello, World!") + .embed(embed) + .add_file(CreateAttachment::path("./ferris_eyes.png".as_ref()).unwrap()); + let msg = new_message.channel_id.send_message(&ctx.http, builder).await; + + if let Err(why) = msg { + println!("Error sending message: {why:?}"); + } + } + }, + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.user.name); + }, + _ => {}, + } + } +} + +#[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; + 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/Cargo.toml b/examples/e09_collectors/Cargo.toml similarity index 62% rename from examples/e10_collectors/Cargo.toml rename to examples/e09_collectors/Cargo.toml index 148f47e3655..2653c16b700 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" +edition.workspace = true [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..f8e383849d0 --- /dev/null +++ b/examples/e09_collectors/src/main.rs @@ -0,0 +1,172 @@ +//! 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; + +use serenity::gateway::client::FullEvent; + +#[async_trait] +impl EventHandler for Handler { + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + FullEvent::Message { + new_message, .. + } => { + let mut score = 0u32; + let _ = new_message + .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 = + new_message.author.id.collect_messages(ctx).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 _ = new_message.reply(&ctx.http, "No answer within 10 seconds.").await; + }; + + let react_msg = new_message + .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) + .timeout(Duration::from_secs(10)) + .author_id(new_message.author.id); + + if let Some(reaction) = collector.await { + let _ = if reaction.emoji.as_data() == "1️⃣" { + score += 1; + new_message.reply(&ctx.http, "That's correct!").await + } else { + new_message.reply(&ctx.http, "Wrong!").await + }; + } else { + let _ = new_message.reply(&ctx.http, "No reaction within 10 seconds.").await; + }; + + let _ = new_message.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) + // Only collect messages by this user. + .author_id(new_message.author.id) + .channel_id(new_message.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, 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.message.id == msg.id) => + { + Some(event.message.id) + }, + _ => None, + }) + .take_until(Box::pin(tokio::time::sleep(Duration::from_secs(20)))); + + let _ = new_message + .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 _ = new_message.reply(&ctx.http, "Great! You edited 5 out of 5").await; + } else { + let _ = new_message + .reply(&ctx.http, format!("You only edited {} out of 5", edited.len())) + .await; + } + + let _ = new_message + .reply( + &ctx.http, + format!("TIME'S UP! You completed {score} out of 4 tasks correctly!"), + ) + .await; + }, + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.user.name); + }, + _ => {}, + } + } +} + +#[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/e09_create_message_builder/src/main.rs b/examples/e09_create_message_builder/src/main.rs deleted file mode 100644 index bb7561f1ca6..00000000000 --- a/examples/e09_create_message_builder/src/main.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::env; - -use serenity::async_trait; -use serenity::builder::{CreateAttachment, CreateEmbed, CreateEmbedFooter, CreateMessage}; -use serenity::model::channel::Message; -use serenity::model::gateway::Ready; -use serenity::model::Timestamp; -use serenity::prelude::*; - -struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - if msg.content == "!hello" { - // The create message builder allows you to easily create embeds and messages using a - // builder syntax. - // This example will create a message that says "Hello, World!", with an embed that has - // a title, description, an image, three fields, and a footer. - let footer = CreateEmbedFooter::new("This is a footer"); - let embed = CreateEmbed::new() - .title("This is a title") - .description("This is a description") - .image("attachment://ferris_eyes.png") - .fields(vec![ - ("This is the first field", "This is a field body", true), - ("This is the second field", "Both fields are inline", true), - ]) - .field("This is the third field", "This is not an inline field", false) - .footer(footer) - // Add a timestamp for the current time - // This also accepts a rfc3339 Timestamp - .timestamp(Timestamp::now()); - let builder = CreateMessage::new() - .content("Hello, World!") - .embed(embed) - .add_file(CreateAttachment::path("./ferris_eyes.png").await.unwrap()); - let msg = msg.channel_id.send_message(&ctx.http, builder).await; - - if let Err(why) = msg { - println!("Error sending message: {why:?}"); - } - } - } - - 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 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"); - - 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/e14_slash_commands/Cargo.toml b/examples/e10_gateway_intents/Cargo.toml similarity index 66% rename from examples/e14_slash_commands/Cargo.toml rename to examples/e10_gateway_intents/Cargo.toml index 4e73adeb7f2..c6b6fdb49e1 100644 --- a/examples/e14_slash_commands/Cargo.toml +++ b/examples/e10_gateway_intents/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "e14_slash_commands" +name = "e10_gateway_intents" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [dependencies] -serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "collector"] } +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/e10_gateway_intents/src/main.rs b/examples/e10_gateway_intents/src/main.rs new file mode 100644 index 00000000000..62b3d4fa2c3 --- /dev/null +++ b/examples/e10_gateway_intents/src/main.rs @@ -0,0 +1,55 @@ +use serenity::async_trait; +use serenity::prelude::*; + +struct Handler; + +use serenity::gateway::client::FullEvent; + +#[async_trait] +impl EventHandler for Handler { + async fn dispatch(&self, _: &Context, event: &FullEvent) { + match event { + // This event will be dispatched for guilds, but not for direct messages. + FullEvent::Message { + new_message, .. + } => println!("Received message: {}", new_message.content), + // As the intents set in this example, this event shall never be dispatched. + // Try it by changing your status. + FullEvent::PresenceUpdate { + .. + } => { + println!("Presence Update") + }, + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.user.name); + }, + _ => {}, + } + } +} + +#[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"); + + // Intents are a bitflag, bitwise operations can be used to dictate which intents to use + let intents = + GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; + // Build our client. + let mut client = Client::builder(token, intents) + .event_handler(Handler) + .await + .expect("Error creating client"); + + // Finally, start a single shard, and start listening to events. + // + // Shards will automatically attempt to reconnect, and will perform exponential backoff until + // it reconnects. + if let Err(why) = client.start().await { + println!("Client error: {why:?}"); + } +} diff --git a/examples/e11_gateway_intents/src/main.rs b/examples/e11_gateway_intents/src/main.rs deleted file mode 100644 index db846bc25ea..00000000000 --- a/examples/e11_gateway_intents/src/main.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::env; - -use serenity::async_trait; -use serenity::model::channel::Message; -use serenity::model::gateway::{Presence, Ready}; -use serenity::prelude::*; - -struct Handler; - -#[async_trait] -impl EventHandler for Handler { - // This event will be dispatched for guilds, but not for direct messages. - async fn message(&self, _ctx: Context, msg: Message) { - println!("Received message: {}", msg.content); - } - - // 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) { - println!("Presence Update"); - } - - 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"); - - // Intents are a bitflag, bitwise operations can be used to dictate which intents to use - let intents = - GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; - // Build our client. - let mut client = Client::builder(token, intents) - .event_handler(Handler) - .await - .expect("Error creating client"); - - // Finally, start a single shard, and start listening to events. - // - // Shards will automatically attempt to reconnect, and will perform exponential backoff until - // it reconnects. - if let Err(why) = client.start().await { - println!("Client error: {why:?}"); - } -} diff --git a/examples/e12_global_data/Cargo.toml b/examples/e11_global_data/Cargo.toml similarity index 78% rename from examples/e12_global_data/Cargo.toml rename to examples/e11_global_data/Cargo.toml index 0244a75f70c..ed79489c5af 100644 --- a/examples/e12_global_data/Cargo.toml +++ b/examples/e11_global_data/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "e12_global_data" +name = "e11_global_data" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [dependencies] serenity = { path = "../../" } 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..89875a17626 --- /dev/null +++ b/examples/e11_global_data/src/main.rs @@ -0,0 +1,98 @@ +//! In this example, you will be shown how to share data between events. + +use std::borrow::Cow; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use serenity::async_trait; +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, +} + +use serenity::gateway::client::FullEvent; +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + FullEvent::Message { + new_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 new_message.author.id != ctx.cache.current_user().id + && new_message.content.to_lowercase().contains("owo") + { + // Here, we are checking how many "owo" there are in the message content. + let owo_in_msg = + new_message.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 new_message.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) = new_message.reply(&ctx.http, response).await { + eprintln!("Error sending response: {err:?}") + }; + } + }, + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.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 74% rename from examples/e13_parallel_loops/Cargo.toml rename to examples/e12_parallel_loops/Cargo.toml index a682559aa62..01c595e5c64 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" +edition.workspace = true [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/e12_parallel_loops/src/main.rs b/examples/e12_parallel_loops/src/main.rs new file mode 100644 index 00000000000..d7189b62f4e --- /dev/null +++ b/examples/e12_parallel_loops/src/main.rs @@ -0,0 +1,128 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use chrono::offset::Utc; +use serenity::async_trait; +use serenity::builder::{CreateEmbed, CreateMessage}; +use serenity::gateway::ActivityData; +use serenity::model::id::GenericChannelId; +use serenity::prelude::*; + +struct Handler { + is_loop_running: AtomicBool, +} + +use serenity::gateway::client::FullEvent; + +#[async_trait] +impl EventHandler for Handler { + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + FullEvent::Message { + new_message, .. + } => { + if new_message.content == "!ping" { + if let Err(why) = new_message.channel_id.say(&ctx.http, "Pong!").await { + println!("Error sending message: {why:?}"); + } + } + }, + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.user.name); + }, + FullEvent::CacheReady { + .. + } => { + println!("Cache built successfully!"); + + // 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. + // + // 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 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 { + loop { + log_system_load(&ctx1).await; + tokio::time::sleep(Duration::from_secs(120)).await; + } + }); + + // And of course, we can run more than one thread at different timings. + let ctx2 = ctx.clone(); + tokio::spawn(async move { + loop { + set_activity_to_current_time(&ctx2); + tokio::time::sleep(Duration::from_secs(60)).await; + } + }); + + // Now that the loop is running, we set the bool to true + self.is_loop_running.swap(true, Ordering::Relaxed); + } + }, + _ => {}, + } + } +} + +async fn log_system_load(ctx: &Context) { + let cpu_load = sys_info::loadavg().unwrap(); + let mem_use = sys_info::mem_info().unwrap(); + + // We can use ChannelId directly to send a message to a specific channel; in this case, the + // message would be sent to the #testing channel on the discord server. + let embed = CreateEmbed::new() + .title("System Resource Load") + .field("CPU Load Average", format!("{:.2}%", cpu_load.one * 10.0), false) + .field( + "Memory Usage", + format!( + "{:.2} MB Free out of {:.2} MB", + mem_use.free as f32 / 1000.0, + mem_use.total as f32 / 1000.0 + ), + false, + ); + let builder = CreateMessage::new().embed(embed); + let message = GenericChannelId::new(381926291785383946).send_message(&ctx.http, builder).await; + if let Err(why) = message { + eprintln!("Error sending message: {why:?}"); + }; +} + +fn set_activity_to_current_time(ctx: &Context) { + let current_time = Utc::now(); + let formatted_time = current_time.to_rfc2822(); + + ctx.set_activity(Some(ActivityData::playing(formatted_time))); +} + +#[tokio::main] +async fn main() { + 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) + .event_handler(Handler { + is_loop_running: AtomicBool::new(false), + }) + .await + .expect("Error creating client"); + + if let Err(why) = client.start().await { + eprintln!("Client error: {why:?}"); + } +} diff --git a/examples/e13_parallel_loops/src/main.rs b/examples/e13_parallel_loops/src/main.rs deleted file mode 100644 index 14fc79c2737..00000000000 --- a/examples/e13_parallel_loops/src/main.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::env; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::time::Duration; - -use chrono::offset::Utc; -use serenity::async_trait; -use serenity::builder::{CreateEmbed, CreateMessage}; -use serenity::gateway::ActivityData; -use serenity::model::channel::Message; -use serenity::model::gateway::Ready; -use serenity::model::id::{ChannelId, GuildId}; -use serenity::prelude::*; - -struct Handler { - is_loop_running: AtomicBool, -} - -#[async_trait] -impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - if msg.content.starts_with("!ping") { - if let Err(why) = msg.channel_id.say(&ctx.http, "Pong!").await { - eprintln!("Error sending message: {why:?}"); - } - } - } - - async fn ready(&self, _ctx: Context, ready: Ready) { - println!("{} is connected!", ready.user.name); - } - - // We use the cache_ready event just in case some cache operation is required in whatever use - // case you have for this. - 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. - // - // 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); - // tokio::spawn creates a new green thread that can run in parallel with the rest of - // the application. - tokio::spawn(async move { - loop { - log_system_load(&ctx1).await; - tokio::time::sleep(Duration::from_secs(120)).await; - } - }); - - // 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); - tokio::time::sleep(Duration::from_secs(60)).await; - } - }); - - // Now that the loop is running, we set the bool to true - self.is_loop_running.swap(true, Ordering::Relaxed); - } - } -} - -async fn log_system_load(ctx: &Context) { - let cpu_load = sys_info::loadavg().unwrap(); - let mem_use = sys_info::mem_info().unwrap(); - - // We can use ChannelId directly to send a message to a specific channel; in this case, the - // message would be sent to the #testing channel on the discord server. - let embed = CreateEmbed::new() - .title("System Resource Load") - .field("CPU Load Average", format!("{:.2}%", cpu_load.one * 10.0), false) - .field( - "Memory Usage", - format!( - "{:.2} MB Free out of {:.2} MB", - mem_use.free as f32 / 1000.0, - mem_use.total as f32 / 1000.0 - ), - false, - ); - let builder = CreateMessage::new().embed(embed); - let message = ChannelId::new(381926291785383946).send_message(&ctx, builder).await; - if let Err(why) = message { - eprintln!("Error sending message: {why:?}"); - }; -} - -fn set_activity_to_current_time(ctx: &Context) { - let current_time = Utc::now(); - let formatted_time = current_time.to_rfc2822(); - - ctx.set_activity(Some(ActivityData::playing(formatted_time))); -} - -#[tokio::main] -async fn main() { - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - - let intents = GatewayIntents::GUILD_MESSAGES - | GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::GUILDS - | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(&token, intents) - .event_handler(Handler { - is_loop_running: AtomicBool::new(false), - }) - .await - .expect("Error creating client"); - - if let Err(why) = client.start().await { - eprintln!("Client error: {why:?}"); - } -} diff --git a/examples/e16_sqlite_database/.gitignore b/examples/e13_sqlite_database/.gitignore similarity index 100% rename from examples/e16_sqlite_database/.gitignore rename to examples/e13_sqlite_database/.gitignore diff --git a/examples/e16_sqlite_database/.sqlx/query-597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678.json b/examples/e13_sqlite_database/.sqlx/query-597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678.json similarity index 100% rename from examples/e16_sqlite_database/.sqlx/query-597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678.json rename to examples/e13_sqlite_database/.sqlx/query-597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678.json diff --git a/examples/e16_sqlite_database/.sqlx/query-7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1.json b/examples/e13_sqlite_database/.sqlx/query-7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1.json similarity index 100% rename from examples/e16_sqlite_database/.sqlx/query-7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1.json rename to examples/e13_sqlite_database/.sqlx/query-7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1.json diff --git a/examples/e16_sqlite_database/.sqlx/query-8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb.json b/examples/e13_sqlite_database/.sqlx/query-8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb.json similarity index 100% rename from examples/e16_sqlite_database/.sqlx/query-8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb.json rename to examples/e13_sqlite_database/.sqlx/query-8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb.json diff --git a/examples/e16_sqlite_database/.sqlx/query-90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a.json b/examples/e13_sqlite_database/.sqlx/query-90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a.json similarity index 100% rename from examples/e16_sqlite_database/.sqlx/query-90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a.json rename to examples/e13_sqlite_database/.sqlx/query-90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a.json diff --git a/examples/e16_sqlite_database/Cargo.toml b/examples/e13_sqlite_database/Cargo.toml similarity index 74% rename from examples/e16_sqlite_database/Cargo.toml rename to examples/e13_sqlite_database/Cargo.toml index eeda2a295b1..a196695c80f 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" +edition.workspace = true [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/e13_sqlite_database/src/main.rs b/examples/e13_sqlite_database/src/main.rs new file mode 100644 index 00000000000..673af342f86 --- /dev/null +++ b/examples/e13_sqlite_database/src/main.rs @@ -0,0 +1,119 @@ +// It is recommended that you read the README file, it is very important to this example. +// This example will help us to use a sqlite database with our bot. +use std::fmt::Write as _; + +use serenity::async_trait; +use serenity::model::prelude::*; +use serenity::prelude::*; + +struct Bot { + database: sqlx::SqlitePool, +} + +use serenity::gateway::client::FullEvent; + +#[async_trait] +impl EventHandler for Bot { + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + FullEvent::Message { + new_message, .. + } => { + let user_id = new_message.author.id.get() as i64; + + if let Some(task_description) = new_message.content.strip_prefix("~todo add") { + let task_description = task_description.trim(); + // That's how we are going to use a sqlite command. + // We are inserting into the todo table, our task_description in task column and + // our user_id in user_Id column. + sqlx::query!( + "INSERT INTO todo (task, user_id) VALUES (?, ?)", + task_description, + user_id, + ) + .execute(&self.database) // < Where the command will be executed + .await + .unwrap(); + + let response = + format!("Successfully added `{task_description}` to your todo list"); + new_message.channel_id.say(&ctx.http, response).await.unwrap(); + } else if let Some(task_index) = new_message.content.strip_prefix("~todo remove") { + let task_index = task_index.trim().parse::().unwrap() - 1; + + // "SELECT" will return the rowid of the todo rows where the user_Id column = + // user_id. + let entry = sqlx::query!( + "SELECT rowid, task FROM todo WHERE user_id = ? ORDER BY rowid LIMIT 1 OFFSET ?", + user_id, + task_index, + ) + .fetch_one(&self.database) // < Just one data will be sent to entry + .await + .unwrap(); + + // Every todo row with rowid column = entry.rowid will be deleted. + sqlx::query!("DELETE FROM todo WHERE rowid = ?", entry.rowid) + .execute(&self.database) + .await + .unwrap(); + + let response = format!("Successfully completed `{}`!", entry.task); + new_message.channel_id.say(&ctx.http, response).await.unwrap(); + } else if new_message.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) + .fetch_all(&self.database) // < All matched data will be sent to todos + .await + .unwrap(); + + let mut response = format!("You have {} pending tasks:\n", todos.len()); + for (i, todo) in todos.iter().enumerate() { + writeln!(response, "{}. {}", i + 1, todo.task).unwrap(); + } + + new_message.channel_id.say(&ctx.http, response).await.unwrap(); + } + }, + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.user.name); + }, + _ => {}, + } + } +} + +#[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"); + + // Initiate a connection to the database file, creating the file if required. + let database = sqlx::sqlite::SqlitePoolOptions::new() + .max_connections(5) + .connect_with( + sqlx::sqlite::SqliteConnectOptions::new() + .filename("database.sqlite") + .create_if_missing(true), + ) + .await + .expect("Couldn't connect to database"); + + // Run migrations, which updates the database's schema to the latest version. + sqlx::migrate!("./migrations").run(&database).await.expect("Couldn't run database migrations"); + + let bot = Bot { + database, + }; + + let intents = GatewayIntents::GUILD_MESSAGES + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::MESSAGE_CONTENT; + let mut 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 68% rename from examples/e17_message_components/Cargo.toml rename to examples/e14_message_components/Cargo.toml index f34f727f55c..76f86c8eca2 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" +edition.workspace = true [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/e14_message_components/src/main.rs b/examples/e14_message_components/src/main.rs new file mode 100644 index 00000000000..9fd67d49df5 --- /dev/null +++ b/examples/e14_message_components/src/main.rs @@ -0,0 +1,183 @@ +use std::borrow::Cow; +use std::time::Duration; + +use dotenv::dotenv; +use serenity::async_trait; +use serenity::builder::{ + CreateButton, + CreateInteractionResponse, + CreateInteractionResponseMessage, + CreateMessage, + CreateSelectMenu, + CreateSelectMenuKind, + CreateSelectMenuOption, +}; +use serenity::collector::CollectComponentInteractions; +use serenity::futures::StreamExt; +use serenity::model::prelude::*; +use serenity::prelude::*; + +fn sound_button(name: &str, emoji: ReactionType) -> CreateButton { + // To add an emoji to buttons, use .emoji(). The method accepts anything ReactionType or + // anything that can be converted to it. For a list of that, search Trait Implementations in + // the docs for From<...>. + CreateButton::new(name).emoji(emoji) +} + +struct Handler; + +use serenity::gateway::client::FullEvent; + +#[async_trait] +impl EventHandler for Handler { + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + FullEvent::Message { + new_message, .. + } => { + if new_message.content != "animal" { + return; + } + + // Ask the user for its favorite animal + let m = new_message + .channel_id + .send_message( + &ctx.http, + CreateMessage::new() + .content("Please select your favorite animal") + .select_menu( + CreateSelectMenu::new( + "animal_select", + CreateSelectMenuKind::String { + 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"), + ), + ) + .await + .unwrap(); + + // Wait for the user to make a selection + // This uses a collector to wait for an incoming event without needing to listen for + // it manually in the EventHandler. + let interaction = match m + .id + .collect_component_interactions(ctx) + .timeout(Duration::from_secs(60 * 3)) + .await + { + Some(x) => x, + None => { + m.reply(&ctx.http, "Timed out").await.unwrap(); + return; + }, + }; + + // data.values contains the selected value from each select menus. We only have one + // menu, so we retrieve the first + let animal = match &interaction.data.kind { + ComponentInteractionDataKind::StringSelect { + values, + } => &values[0], + _ => panic!("unexpected interaction data kind"), + }; + + // Acknowledge the interaction and edit the message + interaction + .create_response( + &ctx.http, + CreateInteractionResponse::UpdateMessage( + CreateInteractionResponseMessage::default() + .content(format!("You chose: **{animal}**\nNow choose a sound!")) + .button(sound_button("meow", "🐈".parse().unwrap())) + .button(sound_button("woof", "🐕".parse().unwrap())) + .button(sound_button("neigh", "🐎".parse().unwrap())) + .button(sound_button("hoooooooonk", "🦙".parse().unwrap())) + .button(sound_button( + "crab rave", + // Custom emojis in Discord are represented with + // `<:EMOJI_NAME:EMOJI_ID>`. You can see this by posting an + // emoji in your server and + // putting a backslash before the emoji. + // + // Because ReactionType implements FromStr, we can use .parse() + // to convert the textual + // emoji representation to ReactionType + "<:ferris:381919740114763787>".parse().unwrap(), + )), + ), + ) + .await + .unwrap(); + + // Wait for multiple interactions + let mut interaction_stream = + m.id.collect_component_interactions(ctx) + .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.http, + // This time we dont edit the message but reply to it + CreateInteractionResponse::Message( + CreateInteractionResponseMessage::default() + // Make the message hidden for other users by setting `ephemeral(true)`. + .ephemeral(true) + .content(format!("The **{animal}** says __{sound}__")), + ), + ) + .await + .unwrap(); + } + + // Delete the orig message or there will be dangling components (components that + // still exist, but no collector is running so any user who presses + // them sees an error) + m.delete(&ctx.http, None).await.unwrap() + }, + FullEvent::Ready { + data_about_bot, .. + } => { + println!("{} is connected!", data_about_bot.user.name); + }, + _ => {}, + } + } +} + +#[tokio::main] +async fn main() { + dotenv().ok(); + // 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"); + + // Build our client. + let intents = GatewayIntents::GUILD_MESSAGES + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::MESSAGE_CONTENT; + let mut client = Client::builder(token, intents) + .event_handler(Handler) + .await + .expect("Error creating client"); + + // Finally, start a single shard, and start listening to events. + // Shards will automatically attempt to reconnect, and will perform exponential backoff until + // it reconnects. + if let Err(why) = client.start().await { + println!("Client error: {why:?}"); + } +} diff --git a/examples/e14_slash_commands/src/main.rs b/examples/e14_slash_commands/src/main.rs deleted file mode 100644 index 70e5cea2a29..00000000000 --- a/examples/e14_slash_commands/src/main.rs +++ /dev/null @@ -1,90 +0,0 @@ -mod commands; - -use std::env; - -use serenity::async_trait; -use serenity::builder::{CreateInteractionResponse, CreateInteractionResponseMessage}; -use serenity::model::application::{Command, Interaction}; -use serenity::model::gateway::Ready; -use serenity::model::id::GuildId; -use serenity::prelude::*; - -struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn interaction_create(&self, ctx: Context, interaction: Interaction) { - if let Interaction::Command(command) = interaction { - println!("Received command interaction: {command:#?}"); - - let content = match command.data.name.as_str() { - "ping" => Some(commands::ping::run(&command.data.options())), - "id" => Some(commands::id::run(&command.data.options())), - "attachmentinput" => Some(commands::attachmentinput::run(&command.data.options())), - "modal" => { - commands::modal::run(&ctx, &command).await.unwrap(); - None - }, - _ => Some("not implemented :(".to_string()), - }; - - if let Some(content) = content { - let data = CreateInteractionResponseMessage::new().content(content); - let builder = CreateInteractionResponse::Message(data); - if let Err(why) = command.create_response(&ctx.http, builder).await { - println!("Cannot respond to slash command: {why}"); - } - } - } - } - - async fn ready(&self, ctx: Context, ready: Ready) { - println!("{} is connected!", ready.user.name); - - let guild_id = GuildId::new( - env::var("GUILD_ID") - .expect("Expected GUILD_ID in environment") - .parse() - .expect("GUILD_ID must be an integer"), - ); - - let commands = guild_id - .set_commands(&ctx.http, vec![ - commands::ping::register(), - commands::id::register(), - commands::welcome::register(), - commands::numberinput::register(), - commands::attachmentinput::register(), - commands::modal::register(), - ]) - .await; - - println!("I now have the following guild slash commands: {commands:#?}"); - - let guild_command = - Command::create_global_command(&ctx.http, commands::wonderful_command::register()) - .await; - - println!("I created the following global slash command: {guild_command:#?}"); - } -} - -#[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"); - - // Build our client. - let mut client = Client::builder(token, GatewayIntents::empty()) - .event_handler(Handler) - .await - .expect("Error creating client"); - - // Finally, start a single shard, and start listening to events. - // - // Shards will automatically attempt to reconnect, and will perform exponential backoff until - // it reconnects. - if let Err(why) = client.start().await { - println!("Client error: {why:?}"); - } -} 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 73% rename from examples/e18_webhook/Cargo.toml rename to examples/e15_webhook/Cargo.toml index ad995b8f4fe..bbd1da81a22 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" +edition.workspace = true [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 73% rename from examples/e19_interactions_endpoint/Cargo.toml rename to examples/e16_interactions_endpoint/Cargo.toml index 203ba8e157a..9aeace086c8 100644 --- a/examples/e19_interactions_endpoint/Cargo.toml +++ b/examples/e16_interactions_endpoint/Cargo.toml @@ -1,9 +1,10 @@ [package] -name = "e19_interactions_endpoint" +name = "e16_interactions_endpoint" version = "0.1.0" authors = ["my name "] -edition = "2018" +edition.workspace = true [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/e16_sqlite_database/src/main.rs b/examples/e16_sqlite_database/src/main.rs deleted file mode 100644 index 11e4b500f87..00000000000 --- a/examples/e16_sqlite_database/src/main.rs +++ /dev/null @@ -1,101 +0,0 @@ -// It is recommended that you read the README file, it is very important to this example. -// This example will help us to use a sqlite database with our bot. -use std::fmt::Write as _; - -use serenity::async_trait; -use serenity::model::prelude::*; -use serenity::prelude::*; - -struct Bot { - database: sqlx::SqlitePool, -} - -#[async_trait] -impl EventHandler for Bot { - async fn message(&self, ctx: Context, msg: Message) { - let user_id = msg.author.id.get() as i64; - - if let Some(task_description) = msg.content.strip_prefix("~todo add") { - let task_description = task_description.trim(); - // That's how we are going to use a sqlite command. - // We are inserting into the todo table, our task_description in task column and our - // user_id in user_Id column. - sqlx::query!( - "INSERT INTO todo (task, user_id) VALUES (?, ?)", - task_description, - user_id, - ) - .execute(&self.database) // < Where the command will be executed - .await - .unwrap(); - - let response = format!("Successfully added `{task_description}` to your todo list"); - msg.channel_id.say(&ctx, response).await.unwrap(); - } else if let Some(task_index) = msg.content.strip_prefix("~todo remove") { - let task_index = task_index.trim().parse::().unwrap() - 1; - - // "SELECT" will return the rowid of the todo rows where the user_Id column = user_id. - let entry = sqlx::query!( - "SELECT rowid, task FROM todo WHERE user_id = ? ORDER BY rowid LIMIT 1 OFFSET ?", - user_id, - task_index, - ) - .fetch_one(&self.database) // < Just one data will be sent to entry - .await - .unwrap(); - - // Every todo row with rowid column = entry.rowid will be deleted. - sqlx::query!("DELETE FROM todo WHERE rowid = ?", entry.rowid) - .execute(&self.database) - .await - .unwrap(); - - let response = format!("Successfully completed `{}`!", entry.task); - msg.channel_id.say(&ctx, 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) - .fetch_all(&self.database) // < All matched data will be sent to todos - .await - .unwrap(); - - let mut response = format!("You have {} pending tasks:\n", todos.len()); - for (i, todo) in todos.iter().enumerate() { - writeln!(response, "{}. {}", i + 1, todo.task).unwrap(); - } - - msg.channel_id.say(&ctx, response).await.unwrap(); - } - } -} - -#[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"); - - // Initiate a connection to the database file, creating the file if required. - let database = sqlx::sqlite::SqlitePoolOptions::new() - .max_connections(5) - .connect_with( - sqlx::sqlite::SqliteConnectOptions::new() - .filename("database.sqlite") - .create_if_missing(true), - ) - .await - .expect("Couldn't connect to database"); - - // Run migrations, which updates the database's schema to the latest version. - sqlx::migrate!("./migrations").run(&database).await.expect("Couldn't run database migrations"); - - let bot = Bot { - database, - }; - - let intents = GatewayIntents::GUILD_MESSAGES - | GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::MESSAGE_CONTENT; - let mut client = - Client::builder(&token, intents).event_handler(bot).await.expect("Err creating client"); - client.start().await.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/e17_message_components/src/main.rs b/examples/e17_message_components/src/main.rs deleted file mode 100644 index c70203354a9..00000000000 --- a/examples/e17_message_components/src/main.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::env; -use std::time::Duration; - -use dotenv::dotenv; -use serenity::async_trait; -use serenity::builder::{ - CreateButton, - CreateInteractionResponse, - CreateInteractionResponseMessage, - CreateMessage, - CreateSelectMenu, - CreateSelectMenuKind, - CreateSelectMenuOption, -}; -use serenity::futures::StreamExt; -use serenity::model::prelude::*; -use serenity::prelude::*; - -fn sound_button(name: &str, emoji: ReactionType) -> CreateButton { - // To add an emoji to buttons, use .emoji(). The method accepts anything ReactionType or - // anything that can be converted to it. For a list of that, search Trait Implementations in - // the docs for From<...>. - CreateButton::new(name).emoji(emoji) -} - -struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - if msg.content != "animal" { - return; - } - - // Ask the user for its favorite animal - let m = msg - .channel_id - .send_message( - &ctx, - CreateMessage::new().content("Please select your favorite animal").select_menu( - CreateSelectMenu::new("animal_select", CreateSelectMenuKind::String { - options: vec![ - 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"), - ), - ) - .await - .unwrap(); - - // Wait for the user to make a selection - // This uses a collector to wait for an incoming event without needing to listen for it - // manually in the EventHandler. - let interaction = match m - .await_component_interaction(&ctx.shard) - .timeout(Duration::from_secs(60 * 3)) - .await - { - Some(x) => x, - None => { - m.reply(&ctx, "Timed out").await.unwrap(); - return; - }, - }; - - // data.values contains the selected value from each select menus. We only have one menu, - // so we retrieve the first - let animal = match &interaction.data.kind { - ComponentInteractionDataKind::StringSelect { - values, - } => &values[0], - _ => panic!("unexpected interaction data kind"), - }; - - // Acknowledge the interaction and edit the message - interaction - .create_response( - &ctx, - CreateInteractionResponse::UpdateMessage( - CreateInteractionResponseMessage::default() - .content(format!("You chose: **{animal}**\nNow choose a sound!")) - .button(sound_button("meow", "🐈".parse().unwrap())) - .button(sound_button("woof", "🐕".parse().unwrap())) - .button(sound_button("neigh", "🐎".parse().unwrap())) - .button(sound_button("hoooooooonk", "🦙".parse().unwrap())) - .button(sound_button( - "crab rave", - // Custom emojis in Discord are represented with - // `<:EMOJI_NAME:EMOJI_ID>`. You can see this by posting an emoji in - // your server and putting a backslash before the emoji. - // - // Because ReactionType implements FromStr, we can use .parse() to - // convert the textual emoji representation to ReactionType - "<:ferris:381919740114763787>".parse().unwrap(), - )), - ), - ) - .await - .unwrap(); - - // Wait for multiple interactions - let mut interaction_stream = - m.await_component_interaction(&ctx.shard).timeout(Duration::from_secs(60 * 3)).stream(); - - while let Some(interaction) = interaction_stream.next().await { - let sound = &interaction.data.custom_id; - // Acknowledge the interaction and send a reply - interaction - .create_response( - &ctx, - // This time we dont edit the message but reply to it - CreateInteractionResponse::Message( - CreateInteractionResponseMessage::default() - // Make the message hidden for other users by setting `ephemeral(true)`. - .ephemeral(true) - .content(format!("The **{animal}** says __{sound}__")), - ), - ) - .await - .unwrap(); - } - - // Delete the orig message or there will be dangling components (components that still - // exist, but no collector is running so any user who presses them sees an error) - m.delete(&ctx).await.unwrap() - } -} - -#[tokio::main] -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"); - - // Build our client. - let intents = GatewayIntents::GUILD_MESSAGES - | GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(token, intents) - .event_handler(Handler) - .await - .expect("Error creating client"); - - // Finally, start a single shard, and start listening to events. - // Shards will automatically attempt to reconnect, and will perform exponential backoff until - // it reconnects. - if let Err(why) = client.start().await { - println!("Client error: {why:?}"); - } -} 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 2b2e36a04f5..a54ab26e32d 100644 --- a/examples/testing/src/main.rs +++ b/examples/testing/src/main.rs @@ -1,4 +1,8 @@ +use std::borrow::Cow; + +use serenity::async_trait; use serenity::builder::*; +use serenity::collector::CollectComponentInteractions; use serenity::model::prelude::*; use serenity::prelude::*; @@ -7,44 +11,42 @@ mod model_type_sizes; const IMAGE_URL: &str = "https://raw.githubusercontent.com/serenity-rs/serenity/current/logo.png"; const IMAGE_URL_2: &str = "https://rustacean.net/assets/rustlogo.png"; -async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { +async fn message(ctx: &Context, msg: &Message) -> Result<(), serenity::Error> { let channel_id = msg.channel_id; 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 +56,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?; @@ -81,16 +87,17 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { model_type_sizes::print_ranking(); } else if msg.content == "auditlog" { // Test special characters in audit log reason - msg.channel_id + channel_id + .expect_channel() .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 +105,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 +117,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) .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 +136,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 +149,16 @@ 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 + .expect_channel() .edit( - &ctx, + &ctx.http, EditChannel::new().available_tags(vec![ CreateForumTag::new("tag1 :)").emoji('👍'), CreateForumTag::new("tag2 (:").moderated(true), @@ -157,12 +166,14 @@ 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)), + .expect_thread() + .edit( + &ctx.http, + EditThread::new() + .applied_tags(forum.available_tags.iter().map(|t| t.id).collect::>()), ) .await?; } else if msg.content == "embedrace" { @@ -170,12 +181,12 @@ 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; - let mut message_updates = serenity::collector::collect(&ctx.shard, move |ev| match ev { - Event::MessageUpdate(x) if x.id == msg_id => Some(()), + let mut message_updates = serenity::collector::collect(ctx, move |ev| match ev { + Event::MessageUpdate(x) if x.message.id == msg_id => Some(()), _ => None, }); let _ = tokio::time::timeout(Duration::from_millis(2000), message_updates.next()).await; @@ -186,76 +197,74 @@ 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 <#{channel_id}> in 10 seconds...")).await?; + msg.channel_id.say(&ctx.http, format!("Deleting <#{channel_id}> in 10 seconds...")).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(()) } -async fn interaction( +async fn command_interaction( ctx: &Context, - interaction: CommandInteraction, + interaction: &CommandInteraction, ) -> Result<(), serenity::Error> { if interaction.data.name == "editattachments" { // 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 +274,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 +282,7 @@ async fn interaction( } else if interaction.data.name == "unifiedattachments1" { interaction .create_response( - ctx, + &ctx.http, CreateInteractionResponse::Message( CreateInteractionResponseMessage::new().content("works"), ), @@ -281,45 +290,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 +340,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", @@ -368,34 +379,41 @@ async fn interaction( } struct Handler; -#[serenity::async_trait] -impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - message(&ctx, msg).await.unwrap(); - } - async fn interaction_create(&self, ctx: Context, i: Interaction) { - match i { - Interaction::Command(i) => interaction(&ctx, i).await.unwrap(), - Interaction::Component(i) => println!("{:#?}", i.data), - Interaction::Autocomplete(i) => { - i.create_response( - &ctx, - CreateInteractionResponse::Autocomplete( - CreateAutocompleteResponse::new() - .add_string_choice("suggestion", "suggestion"), - ), - ) - .await - .unwrap(); +use serenity::gateway::client::FullEvent; + +#[async_trait] +impl EventHandler for Handler { + async fn dispatch(&self, ctx: &Context, event: &FullEvent) { + match event { + FullEvent::Message { + new_message, .. + } => { + message(ctx, new_message).await.unwrap(); }, + FullEvent::InteractionCreate { + interaction, .. + } => match interaction { + Interaction::Command(i) => command_interaction(ctx, i).await.unwrap(), + Interaction::Component(i) => println!("{:#?}", i.data), + Interaction::Autocomplete(i) => { + i.create_response( + &ctx.http, + CreateInteractionResponse::Autocomplete( + CreateAutocompleteResponse::new().add_choice("suggestion"), + ), + ) + .await + .unwrap(); + }, + _ => {}, + }, + FullEvent::ReactionRemoveEmoji { + removed_reactions, .. + } => println!("Got ReactionRemoveEmoji event: {removed_reactions:?}"), _ => {}, } } - - async fn reaction_remove_emoji(&self, _ctx: Context, removed_reactions: Reaction) { - println!("Got ReactionRemoveEmoji event: {removed_reactions:?}"); - } } #[tokio::main] @@ -408,7 +426,8 @@ async fn main() { } 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; let mut client = Client::builder(token, intents).event_handler(Handler).await.unwrap(); diff --git a/examples/testing/src/model_type_sizes.rs b/examples/testing/src/model_type_sizes.rs index 7160a2ece58..eb23e783ab4 100644 --- a/examples/testing/src/model_type_sizes.rs +++ b/examples/testing/src/model_type_sizes.rs @@ -65,6 +65,7 @@ pub fn print_ranking() { ("GuildBanAddEvent", std::mem::size_of::()), ("GuildBanRemoveEvent", std::mem::size_of::()), ("GuildChannel", std::mem::size_of::()), + ("GuildThread", std::mem::size_of::()), ("GuildCreateEvent", std::mem::size_of::()), ("GuildDeleteEvent", std::mem::size_of::()), ("GuildEmojisUpdateEvent", std::mem::size_of::()), @@ -125,17 +126,18 @@ 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::()), - ("PartialChannel", std::mem::size_of::()), + ("AuditLogEntryOptions", std::mem::size_of::()), + ("GenericInteractionChannel", std::mem::size_of::()), + ("InteractionChannel", std::mem::size_of::()), + ("InteractionGuildThread", std::mem::size_of::()), ("PartialCurrentApplicationInfo", std::mem::size_of::()), ("PartialGuild", std::mem::size_of::()), - ("PartialGuildChannel", std::mem::size_of::()), + ("PartialGuildThread", std::mem::size_of::()), ("PartialMember", std::mem::size_of::()), ("PermissionOverwrite", std::mem::size_of::()), ("Permissions", std::mem::size_of::()), @@ -157,7 +159,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 +196,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/rustfmt.toml b/rustfmt.toml index d8b6a8a8fe2..8e709eb1e62 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,3 @@ -edition = "2021" match_block_trailing_comma = true newline_style = "Unix" use_field_init_shorthand = true 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 b08fa712b30..7207a8a330f 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}; @@ -18,33 +20,31 @@ enum ParseAction { impl ParseAction { fn from_allow(allow: bool) -> Self { - if allow { - Self::Insert - } else { - Self::Remove - } + if allow { Self::Insert } else { Self::Remove } } } -/// A builder to manage the allowed mentions on a message, used by the [`ChannelId::send_message`] -/// and [`ChannelId::edit_message`] methods. +/// A builder to manage the allowed mentions on a message. /// /// # Examples /// /// ```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 +57,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 +114,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 869fecf6848..0b57d308c8b 100644 --- a/src/builder/create_attachment.rs +++ b/src/builder/create_attachment.rs @@ -1,41 +1,43 @@ +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")] -use crate::error::Error; -use crate::error::Result; +use crate::error::{Error, Result, UrlError}; #[cfg(feature = "http")] use crate::http::Http; +use crate::model::channel::Message; use crate::model::id::AttachmentId; +#[derive(Clone, Debug)] +pub enum AttachmentData<'a> { + Bytes(Bytes), + File(&'a File), + Path(&'a Path), +} + /// 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: AttachmentData<'a>, } -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, + data: AttachmentData::Bytes(data.into()), } } @@ -43,90 +45,138 @@ 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?; - + /// Returns [`Error::Io`] if the path is not a valid file path. + pub fn path(path: &'a Path) -> Result { let filename = path - .as_ref() .file_name() - .ok_or_else(|| std::io::Error::other("attachment path must not be a directory"))?; - - Ok(CreateAttachment::bytes(data, filename.to_string_lossy().to_string())) + .ok_or_else(|| std::io::Error::other("attachment path must not be a directory"))? + .to_string_lossy() + .into_owned(); + Ok(CreateAttachment { + filename: filename.into(), + description: None, + data: AttachmentData::Path(path), + }) } /// Builds an [`CreateAttachment`] by reading from a file handler. + pub fn file(file: &'a File, filename: impl Into>) -> Self { + CreateAttachment { + filename: filename.into(), + description: None, + data: AttachmentData::File(file), + } + } + + /// Builds an [`CreateAttachment`] by downloading attachment data from a URL. /// /// # Errors /// - /// [`Error::Io`] error if reading the file fails. - 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?; + /// Returns [`Error::Http`] if downloading the data fails. + #[cfg(feature = "http")] + 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)) } - /// Builds an [`CreateAttachment`] by downloading attachment data from a URL. + /// Returns the underlying data for the attachment. /// /// # Errors /// - /// [`Error::Url`] if the URL is invalid, [`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()))?; - - Ok(CreateAttachment::bytes(data, filename)) + /// Returns an error if fetching the data failed in some way. If the attachment is a + /// [`CreateAttachment::path`], then the file at the specified path was unable to be read. If + /// instead it's [`CreateAttachment::file`], then cloning the handle to the file failed, likely + /// due to hitting the system's limit on number of open file handles. + pub async fn get_data(&self) -> Result { + match &self.data { + AttachmentData::Bytes(bytes) => Ok(bytes.clone()), + AttachmentData::Path(path) => { + let mut file = File::open(path).await?; + let mut data = Vec::new(); + file.read_to_end(&mut data).await?; + Ok(data.into()) + }, + AttachmentData::File(file) => { + let mut data = Vec::new(); + file.try_clone().await?.read_to_end(&mut data).await?; + Ok(data.into()) + }, + } } - /// Converts the stored data to the base64 representation. + /// Converts the attachment data to a base64-encoded data URI. + /// + /// # Errors /// - /// This is used in the library internally because Discord expects image data as base64 in many - /// places. - #[must_use] - pub fn to_base64(&self) -> String { + /// See [`CreateAttachment::get_data`] for details. + pub async fn encode(&self) -> Result { use base64::engine::{Config, Engine}; const PREFIX: &str = "data:image/png;base64,"; + let data = self.get_data().await?; let engine = base64::prelude::BASE64_STANDARD; - let encoded_size = base64::encoded_len(self.data.len(), engine.config().encode_padding()) + let encoded_size = base64::encoded_len(data.len(), engine.config().encode_padding()) .and_then(|len| len.checked_add(PREFIX.len())) .expect("buffer capacity overflow"); let mut encoded = String::with_capacity(encoded_size); encoded.push_str(PREFIX); - engine.encode_string(&self.data, &mut encoded); - encoded + engine.encode_string(&data, &mut encoded); + Ok(ImageData(encoded.into())) } /// 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)] -struct ExistingAttachment { - id: AttachmentId, +/// A wrapper around some base64-encoded image data. Used when an endpoint expects the image +/// payload directly as part of the JSON body, instead of as a multipart upload. +#[derive(Clone, Debug, Serialize)] +#[serde(transparent)] +pub struct ImageData<'a>(Cow<'a, str>); + +impl<'a> ImageData<'a> { + /// Constructs image data from a base64-encoded blob of data. The string must be a valid data + /// URI, for example: + /// + /// ``` + /// use serenity::builder::ImageData; + /// + /// let s = "data:image/png;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="; + /// assert!(ImageData::from_base64(s).is_ok()); + /// ``` + /// + /// # Errors + /// + /// Returns a [`Error::Url`] if the string is not a valid data URI. See the [Discord + /// docs](https://discord.com/developers/docs/reference#image-data). + pub fn from_base64(s: impl Into>) -> Result { + let s = s.into(); + if let Some(("data", tail)) = s.split_once(':') { + if let Some((mimetype, encoding)) = tail.split_once(';') { + if mimetype.split_once('/').is_some() && encoding.starts_with("base64,") { + return Ok(Self(s)); + } + } + } + Err(Error::Url(UrlError::InvalidDataURI)) + } } -#[derive(Debug, Clone, serde::Serialize, PartialEq)] -#[serde(untagged)] -enum NewOrExisting { - New(CreateAttachment), - Existing(ExistingAttachment), +#[derive(Clone, Debug)] +enum EditAttachmentsInner<'a> { + New(CreateAttachment<'a>), + Existing(AttachmentId), } /// You can add new attachments and edit existing ones using this builder. @@ -151,7 +201,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?; @@ -162,7 +212,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?; @@ -173,7 +223,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?; @@ -184,14 +234,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> { + inner: Vec>, } -impl EditAttachments { +impl<'a> EditAttachments<'a> { /// An empty attachments builder. /// /// Existing attachments are not kept by default, either. See [`Self::keep_all()`] or @@ -212,15 +261,7 @@ impl EditAttachments { /// Discord will throw an error!** pub fn keep_all(msg: &Message) -> Self { Self { - new_and_existing_attachments: msg - .attachments - .iter() - .map(|a| { - NewOrExisting::Existing(ExistingAttachment { - id: a.id, - }) - }) - .collect(), + inner: msg.attachments.iter().map(|a| EditAttachmentsInner::Existing(a.id)).collect(), } } @@ -229,9 +270,7 @@ impl EditAttachments { /// /// Opposite of [`Self::remove`]. pub fn keep(mut self, id: AttachmentId) -> Self { - self.new_and_existing_attachments.push(NewOrExisting::Existing(ExistingAttachment { - id, - })); + self.inner.push(EditAttachmentsInner::Existing(id)); self } @@ -240,48 +279,74 @@ 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 - self.new_and_existing_attachments.retain(|a| match a { - NewOrExisting::Existing(a) if a.id == id => false, - _ => true, + self.inner.retain(|a| match a { + EditAttachmentsInner::Existing(existing_id) => *existing_id != id, + EditAttachmentsInner::New(_) => true, }); self } /// 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 { - self.new_and_existing_attachments.push(NewOrExisting::New(attachment)); + #[expect(clippy::should_implement_trait)] // Clippy thinks add == std::ops::Add::add + pub fn add(mut self, attachment: CreateAttachment<'a>) -> Self { + self.inner.push(EditAttachmentsInner::New(attachment)); self } /// 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; + /// are needed for the multipart form data. + #[cfg(feature = "http")] + pub(crate) fn new_attachments(&self) -> Vec> { + self.inner + .iter() + .filter_map(|attachment| { + if let EditAttachmentsInner::New(attachment) = &attachment { + Some(attachment.clone()) + } else { + None + } + }) + .collect() + } +} - 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( - std::mem::take(&mut attachment.data), - attachment.filename.clone(), - ); +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>, + } - // Assign placeholder IDs so Discord can match metadata to file contents - attachment.id = id_placeholder; - cloned_attachment.id = id_placeholder; - files.push(cloned_attachment); + #[derive(Serialize)] + struct ExistingAttachment { + id: AttachmentId, + } - id_placeholder += 1; + // 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.inner.len()))?; + for attachment in &self.inner { + match attachment { + EditAttachmentsInner::New(new_attachment) => { + let attachment = NewAttachment { + id, + filename: &new_attachment.filename, + description: &new_attachment.description, + }; + id += 1; + seq.serialize_element(&attachment)?; + }, + EditAttachmentsInner::Existing(id) => { + seq.serialize_element(&ExistingAttachment { + id: *id, + })?; + }, } } - files - } - - #[cfg(feature = "cache")] - pub(crate) fn is_empty(&self) -> bool { - self.new_and_existing_attachments.is_empty() + 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 bf5935b9200..b542c3408bf 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..4729c6dfa2a 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