├── .cargo └── config.toml ├── .editorconfig ├── .git-blame-ignore-revs ├── .github ├── labeler.yml ├── matchers │ └── rust.json └── workflows │ ├── ci.yml │ ├── docs.yml │ ├── labeler.yml │ └── lint.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE.md ├── Makefile.toml ├── README.md ├── benches └── bench_args.rs ├── build.rs ├── clippy.toml ├── command_attr ├── Cargo.toml └── src │ ├── attributes.rs │ ├── consts.rs │ ├── lib.rs │ ├── structures.rs │ └── util.rs ├── examples ├── Makefile.toml ├── README.md ├── e01_basic_ping_bot │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e02_transparent_guild_sharding │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e03_struct_utilities │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e04_message_builder │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e05_command_framework │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e06_sample_bot_structure │ ├── .env.example │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ ├── commands │ │ ├── math.rs │ │ ├── meta.rs │ │ ├── mod.rs │ │ └── owner.rs │ │ └── main.rs ├── e07_env_logging │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e08_shard_manager │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e09_create_message_builder │ ├── Cargo.toml │ ├── Makefile.toml │ ├── ferris_eyes.png │ └── src │ │ └── main.rs ├── e10_collectors │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e11_gateway_intents │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e12_global_data │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e13_parallel_loops │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e14_slash_commands │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ └── src │ │ ├── commands │ │ ├── attachmentinput.rs │ │ ├── id.rs │ │ ├── mod.rs │ │ ├── modal.rs │ │ ├── numberinput.rs │ │ ├── ping.rs │ │ ├── welcome.rs │ │ └── wonderful_command.rs │ │ └── main.rs ├── e15_simple_dashboard │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e16_sqlite_database │ ├── .gitignore │ ├── .sqlx │ │ ├── query-597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678.json │ │ ├── query-7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1.json │ │ ├── query-8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb.json │ │ └── query-90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a.json │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ ├── migrations │ │ └── 20210906145552_initial_migration.sql │ ├── pre-commit │ └── src │ │ └── main.rs ├── e17_message_components │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e18_webhook │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs ├── e19_interactions_endpoint │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ └── main.rs └── testing │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ ├── main.rs │ └── model_type_sizes.rs ├── logo.png ├── rustfmt.toml ├── src ├── builder │ ├── add_member.rs │ ├── bot_auth_parameters.rs │ ├── create_allowed_mentions.rs │ ├── create_attachment.rs │ ├── create_channel.rs │ ├── create_command.rs │ ├── create_command_permission.rs │ ├── create_components.rs │ ├── create_embed.rs │ ├── create_forum_post.rs │ ├── create_forum_tag.rs │ ├── create_interaction_response.rs │ ├── create_interaction_response_followup.rs │ ├── create_invite.rs │ ├── create_message.rs │ ├── create_poll.rs │ ├── create_scheduled_event.rs │ ├── create_stage_instance.rs │ ├── create_sticker.rs │ ├── create_thread.rs │ ├── create_webhook.rs │ ├── edit_automod_rule.rs │ ├── edit_channel.rs │ ├── edit_guild.rs │ ├── edit_guild_welcome_screen.rs │ ├── edit_guild_widget.rs │ ├── edit_interaction_response.rs │ ├── edit_member.rs │ ├── edit_message.rs │ ├── edit_profile.rs │ ├── edit_role.rs │ ├── edit_scheduled_event.rs │ ├── edit_stage_instance.rs │ ├── edit_sticker.rs │ ├── edit_thread.rs │ ├── edit_voice_state.rs │ ├── edit_webhook.rs │ ├── edit_webhook_message.rs │ ├── execute_webhook.rs │ ├── get_entitlements.rs │ ├── get_messages.rs │ └── mod.rs ├── cache │ ├── cache_update.rs │ ├── event.rs │ ├── mod.rs │ ├── settings.rs │ └── wrappers.rs ├── client │ ├── context.rs │ ├── dispatch.rs │ ├── error.rs │ ├── event_handler.rs │ └── mod.rs ├── collector.rs ├── constants.rs ├── error.rs ├── framework │ ├── mod.rs │ └── standard │ │ ├── args.rs │ │ ├── configuration.rs │ │ ├── help_commands.rs │ │ ├── mod.rs │ │ ├── parse │ │ ├── map.rs │ │ └── mod.rs │ │ └── structures │ │ ├── buckets.rs │ │ ├── check.rs │ │ └── mod.rs ├── gateway │ ├── bridge │ │ ├── event.rs │ │ ├── mod.rs │ │ ├── shard_manager.rs │ │ ├── shard_messenger.rs │ │ ├── shard_queuer.rs │ │ ├── shard_runner.rs │ │ ├── shard_runner_message.rs │ │ └── voice.rs │ ├── error.rs │ ├── mod.rs │ ├── shard.rs │ └── ws.rs ├── http │ ├── client.rs │ ├── error.rs │ ├── mod.rs │ ├── multipart.rs │ ├── ratelimiting.rs │ ├── request.rs │ ├── routing.rs │ └── typing.rs ├── interactions_endpoint.rs ├── internal │ ├── macros.rs │ ├── mod.rs │ ├── prelude.rs │ └── tokio.rs ├── json.rs ├── lib.rs ├── model │ ├── application │ │ ├── command.rs │ │ ├── command_interaction.rs │ │ ├── component.rs │ │ ├── component_interaction.rs │ │ ├── interaction.rs │ │ ├── mod.rs │ │ ├── modal_interaction.rs │ │ ├── oauth.rs │ │ └── ping_interaction.rs │ ├── channel │ │ ├── attachment.rs │ │ ├── channel_id.rs │ │ ├── embed.rs │ │ ├── guild_channel.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ ├── partial_channel.rs │ │ ├── private_channel.rs │ │ └── reaction.rs │ ├── colour.rs │ ├── connection.rs │ ├── error.rs │ ├── event.rs │ ├── gateway.rs │ ├── guild │ │ ├── audit_log │ │ │ ├── change.rs │ │ │ ├── mod.rs │ │ │ └── utils.rs │ │ ├── automod.rs │ │ ├── emoji.rs │ │ ├── guild_id.rs │ │ ├── guild_preview.rs │ │ ├── integration.rs │ │ ├── member.rs │ │ ├── mod.rs │ │ ├── partial_guild.rs │ │ ├── premium_tier.rs │ │ ├── role.rs │ │ ├── scheduled_event.rs │ │ ├── system_channel.rs │ │ └── welcome_screen.rs │ ├── id.rs │ ├── invite.rs │ ├── mention.rs │ ├── misc.rs │ ├── mod.rs │ ├── monetization.rs │ ├── permissions.rs │ ├── sticker.rs │ ├── timestamp.rs │ ├── user.rs │ ├── utils.rs │ ├── voice.rs │ └── webhook.rs ├── prelude.rs └── utils │ ├── argument_convert │ ├── _template.rs │ ├── channel.rs │ ├── emoji.rs │ ├── guild.rs │ ├── member.rs │ ├── message.rs │ ├── mod.rs │ ├── role.rs │ └── user.rs │ ├── content_safe.rs │ ├── custom_message.rs │ ├── formatted_timestamp.rs │ ├── message_builder.rs │ ├── mod.rs │ ├── quick_modal.rs │ └── token.rs ├── tests └── test_reaction.rs └── voice-model ├── Cargo.toml ├── benches └── de.rs ├── rustfmt.toml └── src ├── close_code.rs ├── constants.rs ├── event ├── from.rs ├── mod.rs └── tests.rs ├── id.rs ├── lib.rs ├── opcode.rs ├── payload.rs ├── protocol_data.rs ├── speaking_state.rs └── util.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "target-cpu=haswell"] 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # List of noisy revisions that can be ignored with git-blame(1). 2 | # 3 | # See `blame.ignoreRevsFile` in git-config(1) to enable it by default, or 4 | # use it with `--ignore-revs-file` manually with git-blame. 5 | # 6 | # To "install" it: 7 | # 8 | # git config --local blame.ignoreRevsFile .git-blame-ignore-revs 9 | 10 | # rustfmt 11 | 550030264952f0e0043b63f4582bb817ef8bbf37 12 | 13 | # Apply rustfmt 14 | dae2cb77b407044f44a7a2790d93efba3891854e 15 | 16 | # Format the repository and add a workflow for formatting and linting (#1174) 17 | 9bbb25aac4d651804286f333eb503a72d41e473b 18 | 19 | # Format imports with module granularity (#1846) 20 | 4c97810b6d25617fa0a0a4b4769deb751a17d373 21 | 22 | # Unify the rustfmt configurations 23 | aafc1d04ac04fc9691d7bf56c1b4e8eb7cfd6597 24 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | ci: 2 | - .github/**/* 3 | command_attr: 4 | - command_attr/**/* 5 | examples: 6 | - examples/**/* 7 | builder: 8 | - src/builder/**/* 9 | cache: 10 | - src/cache/**/* 11 | client: 12 | - src/client/**/* 13 | collector: 14 | - src/collector/**/* 15 | framework: 16 | - src/framework/**/* 17 | gateway: 18 | - src/gateway/**/* 19 | http: 20 | - src/http/**/* 21 | model: 22 | - src/model/**/* 23 | utils: 24 | - src/utils/**/* 25 | voice: 26 | - voice-model/**/* 27 | -------------------------------------------------------------------------------- /.github/matchers/rust.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "cargo-common", 5 | "pattern": [ 6 | { 7 | "regexp": "^(warning|warn|error)(\\[(\\S*)\\])?: (.*)$", 8 | "severity": 1, 9 | "message": 4, 10 | "code": 3 11 | }, 12 | { 13 | "regexp": "^\\s+-->\\s(\\S+):(\\d+):(\\d+)$", 14 | "file": 1, 15 | "line": 2, 16 | "column": 3 17 | } 18 | ] 19 | }, 20 | { 21 | "owner": "cargo-test", 22 | "pattern": [ 23 | { 24 | "regexp": "^.*panicked\\s+at\\s+'(.*)',\\s+(.*):(\\d+):(\\d+)$", 25 | "message": 1, 26 | "file": 2, 27 | "line": 3, 28 | "column": 4 29 | } 30 | ] 31 | }, 32 | { 33 | "owner": "cargo-fmt", 34 | "pattern": [ 35 | { 36 | "regexp": "^(Diff in (\\S+)) at line (\\d+):", 37 | "message": 1, 38 | "file": 2, 39 | "line": 3 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - current 7 | - next 8 | 9 | env: 10 | rust_toolchain: nightly 11 | 12 | jobs: 13 | docs: 14 | name: Publish docs 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v4 20 | 21 | - name: Install toolchain (${{ env.rust_toolchain }}) 22 | uses: dtolnay/rust-toolchain@master 23 | with: 24 | toolchain: ${{ env.rust_toolchain }} 25 | 26 | - name: Cache 27 | uses: Swatinem/rust-cache@v2 28 | 29 | - name: Build docs 30 | env: 31 | RUSTDOCFLAGS: --cfg docsrs -D warnings 32 | run: | 33 | cargo doc --no-deps --features full 34 | cargo doc --no-deps -p command_attr 35 | 36 | - name: Prepare docs 37 | shell: bash -e -O extglob {0} 38 | run: | 39 | DIR=${GITHUB_REF/refs\/+(heads|tags)\//} 40 | mkdir -p ./docs/$DIR 41 | touch ./docs/.nojekyll 42 | echo '' > ./docs/$DIR/index.html 43 | mv ./target/doc/* ./docs/$DIR/ 44 | 45 | - name: Deploy docs 46 | uses: peaceiris/actions-gh-pages@v3 47 | with: 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | publish_branch: gh-pages 50 | publish_dir: ./docs 51 | allow_empty_commit: false 52 | keep_files: true 53 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Labeler 2 | 3 | permissions: 4 | contents: read 5 | pull-requests: write 6 | 7 | on: [pull_request_target] 8 | 9 | jobs: 10 | label: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/labeler@v4 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | sync-labels: true 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # Copied from Twilight's Lint workflow. 2 | # 3 | # https://github.com/twilight-rs/twilight/blob/trunk/.github/workflows/lint.yml 4 | name: Lint 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | clippy: 10 | name: Clippy 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v4 16 | 17 | - name: Install toolchain 18 | uses: dtolnay/rust-toolchain@nightly 19 | with: 20 | components: clippy 21 | 22 | - name: Add problem matchers 23 | run: echo "::add-matcher::.github/matchers/rust.json" 24 | 25 | - name: Cache 26 | uses: Swatinem/rust-cache@v2 27 | 28 | - name: Run clippy 29 | run: cargo clippy --workspace --tests --features full -- -D warnings --cfg ignore_serenity_deprecated 30 | 31 | rustfmt: 32 | name: Format 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - name: Checkout sources 37 | uses: actions/checkout@v4 38 | 39 | - name: Install toolchain 40 | uses: dtolnay/rust-toolchain@nightly 41 | with: 42 | components: rustfmt 43 | 44 | - name: Add problem matchers 45 | run: echo "::add-matcher::.github/matchers/rust.json" 46 | 47 | - name: Cache 48 | uses: Swatinem/rust-cache@v2 49 | 50 | - name: Run cargo fmt 51 | run: cargo fmt --all -- --check 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE directories and folders 2 | .vscode/ 3 | .idea/ 4 | 5 | # Target directory 6 | target/ 7 | 8 | # Lockfile 9 | Cargo.lock 10 | 11 | # Misc 12 | rls/ 13 | *.iml 14 | .env 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright (c) 2016, Serenity Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 15 | THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /benches/bench_args.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | #[cfg(test)] 4 | mod benches { 5 | extern crate test; 6 | 7 | use serenity::framework::standard::{Args, Delimiter}; 8 | 9 | use self::test::Bencher; 10 | 11 | #[bench] 12 | fn single_with_one_delimiter(b: &mut Bencher) { 13 | b.iter(|| { 14 | let mut args = Args::new("1,2", &[Delimiter::Single(',')]); 15 | args.single::().unwrap(); 16 | }) 17 | } 18 | 19 | #[bench] 20 | fn single_with_one_delimiter_and_long_string(b: &mut Bencher) { 21 | b.iter(|| { 22 | let mut args = 23 | 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", &[ 24 | Delimiter::Single(','), 25 | ]); 26 | args.single::().unwrap(); 27 | }) 28 | } 29 | 30 | #[bench] 31 | fn single_with_three_delimiters(b: &mut Bencher) { 32 | b.iter(|| { 33 | let mut args = Args::new("1,2 @3@4 5,", &[ 34 | Delimiter::Single(','), 35 | Delimiter::Single(' '), 36 | Delimiter::Single('@'), 37 | ]); 38 | args.single::().unwrap(); 39 | }) 40 | } 41 | 42 | #[bench] 43 | fn single_with_three_delimiters_and_long_string(b: &mut Bencher) { 44 | b.iter(|| { 45 | let mut args = 46 | 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,", &[ 47 | Delimiter::Single(','), 48 | Delimiter::Single(' '), 49 | Delimiter::Single('@'), 50 | ]); 51 | args.single::().unwrap(); 52 | }) 53 | } 54 | 55 | #[bench] 56 | fn single_quoted_with_one_delimiter(b: &mut Bencher) { 57 | b.iter(|| { 58 | let mut args = Args::new(r#""1","2""#, &[Delimiter::Single(',')]); 59 | args.single_quoted::().unwrap(); 60 | }) 61 | } 62 | 63 | #[bench] 64 | fn iter_with_one_delimiter(b: &mut Bencher) { 65 | b.iter(|| { 66 | let mut args = Args::new("1,2,3,4,5,6,7,8,9,10", &[Delimiter::Single(',')]); 67 | args.iter::().collect::, _>>().unwrap(); 68 | }) 69 | } 70 | 71 | #[bench] 72 | fn iter_with_three_delimiters(b: &mut Bencher) { 73 | b.iter(|| { 74 | let mut args = Args::new("1-2<3,4,5,6,7<8,9,10", &[ 75 | Delimiter::Single(','), 76 | Delimiter::Single('-'), 77 | Delimiter::Single('<'), 78 | ]); 79 | args.iter::().collect::, _>>().unwrap(); 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all( 2 | any(feature = "http", feature = "gateway"), 3 | not(any(feature = "rustls_backend", feature = "native_tls_backend")) 4 | ))] 5 | compile_error!( 6 | "You have the `http` or `gateway` feature enabled, either the `rustls_backend` or \ 7 | `native_tls_backend` feature must be selected to let Serenity use `http` or `gateway`.\n\ 8 | - `rustls_backend` uses Rustls, a pure Rust TLS-implemenation.\n\ 9 | - `native_tls_backend` uses SChannel on Windows, Secure Transport on macOS, and OpenSSL on \ 10 | other platforms.\n\ 11 | If you are unsure, go with `rustls_backend`." 12 | ); 13 | 14 | fn main() { 15 | println!("cargo:rustc-check-cfg=cfg(tokio_unstable, ignore_serenity_deprecated)"); 16 | } 17 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | cognitive-complexity-threshold = 20 2 | -------------------------------------------------------------------------------- /command_attr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "command_attr" 3 | version = "0.5.3" 4 | authors = ["Alex M. M. "] 5 | description = "Procedural macros for command creation for the Serenity library." 6 | 7 | documentation.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | keywords.workspace = true 11 | license.workspace = true 12 | edition.workspace = true 13 | rust-version.workspace = true 14 | 15 | [lib] 16 | proc-macro = true 17 | 18 | [dependencies] 19 | quote = "^1.0" 20 | syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] } 21 | proc-macro2 = "^1.0.60" 22 | -------------------------------------------------------------------------------- /command_attr/src/consts.rs: -------------------------------------------------------------------------------- 1 | pub mod suffixes { 2 | pub const COMMAND: &str = "COMMAND"; 3 | pub const COMMAND_OPTIONS: &str = "COMMAND_OPTIONS"; 4 | pub const HELP_OPTIONS: &str = "OPTIONS"; 5 | pub const GROUP: &str = "GROUP"; 6 | pub const GROUP_OPTIONS: &str = "GROUP_OPTIONS"; 7 | pub const CHECK: &str = "CHECK"; 8 | } 9 | 10 | pub use self::suffixes::*; 11 | -------------------------------------------------------------------------------- /examples/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../Makefile.toml" 2 | 3 | [env] 4 | EXAMPLES_PATH = "../../examples" 5 | 6 | [tasks.build_example] 7 | command = "cargo" 8 | args = ["make", "--cwd", "./${@}", "examples_build"] 9 | dependencies = ["build"] 10 | 11 | [tasks.build_example_release] 12 | command = "cargo" 13 | args = ["make", "--cwd", "./${@}", "examples_build_release"] 14 | dependencies = ["build_release"] 15 | 16 | [tasks.run_example] 17 | command = "cargo" 18 | args = ["make", "--cwd", "./${@}", "examples_run"] 19 | dependencies = ["build"] 20 | 21 | [tasks.run_example_release] 22 | command = "cargo" 23 | args = ["make", "--cwd", "./${@}", "examples_run_release"] 24 | dependencies = ["build_release"] 25 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Serenity Examples 2 | 3 | The examples listed in each directory demonstrate different use cases of the 4 | library, and increasingly show more advanced or in-depth code. 5 | 6 | All examples have documentation for new concepts, and try to explain any new 7 | concepts. Examples should be completed in order, so as not to miss any 8 | documentation. 9 | 10 | If you are looking for voice examples, they can be found in [songbird's repository](https://github.com/serenity-rs/songbird/tree/current/examples/serenity) and on [lavalink-rs](https://gitlab.com/vicky5124/lavalink-rs/-/tree/master/examples) 11 | 12 | To provide a token for them to use, you need to set the `DISCORD_TOKEN` 13 | environmental variable to the Bot token.\ 14 | If you don't like environment tokens, you can hardcode your token in instead.\ 15 | TIP: A valid token starts with M, N or O and has 2 dots. 16 | 17 | ## Running Examples 18 | 19 | To run an example, you have various options: 20 | 21 | 1. [cargo-make](https://lib.rs/crates/cargo-make) 22 | 23 | - Install cargo-make `cargo install --force cargo-make` 24 | - Clone the repository: `git clone https://github.com/serenity-rs/serenity.git` 25 | - CD into the serenity folder: `cd serenity` 26 | - Run `cargo make 1`, where 1 is the number of the example you wish to run; these are: 27 | 28 | ``` 29 | 1 => Basic Ping Bot: A bare minimum serenity application. 30 | 2 => Transparent Guild Sharding: How to use sharding and shared cache. 31 | 3 => Structure Utilities: Simple usage of the utils feature. 32 | 4 => Message Builder: A demonstration of the message builder utility, to generate messages safely. 33 | 5 => Command Framework: The main example, where it's demonstrated how to use serenity's command framework, 34 | along with most of its utilities. 35 | This example also shows how to share data between events and commands, using `Context.data` 36 | 6 => Simple Bot Structure: An example showing the recommended file structure to use. 37 | 7 => Env Logging: How to use the tracing crate along with serenity. 38 | 8 => Shard Manager: How to get started with using the shard manager. 39 | 9 => Create Message Builder: How to send embeds and files. 40 | 10 => Collectors: How to use the collectors feature to wait for messages and reactions. 41 | 11 => Gateway Intents: How to use intents to limit the events the bot will receive. 42 | 12 => Global Data: How to use the client data to share data between commands and events safely. 43 | 13 => Parallel Loops: How to run tasks in a loop with context access. 44 | Additionally, show how to send a message to a specific channel. 45 | 14 => Slash Commands: How to use the low level slash command API. 46 | 15 => Simple Dashboard: A simple dashboard to control and monitor the bot with `rillrate`. 47 | 16 => SQLite Database: How to run an embedded SQLite database alongside the bot using SQLx 48 | 17 => Message Components: How to structure and use buttons and select menus 49 | 18 => Webhook: How to construct and call a webhook 50 | ``` 51 | 52 | 2. Manually running: 53 | 54 | - Clone the repository: `git clone https://github.com/serenity-rs/serenity.git` 55 | - Run the example of choice, selected using the `-p` flag: `cargo run --release -p e01_basic_ping_bot ` 56 | 57 | 3. Copy Paste: 58 | 59 | - Copy the contents of the example into your local binary project\ 60 | (created via `cargo new test-project --bin`)\ 61 | and ensuring that the contents of the `Cargo.toml` file 62 | contains that of the example's `[dependencies]` section,\ 63 | and _then_ executing `cargo run`. 64 | 65 | ### Questions 66 | 67 | If you have any questions, feel free to submit an issue with what can be 68 | clarified. 69 | 70 | ### Contributing 71 | 72 | If you add a new example also add it to the following files: 73 | 74 | - `.github/workflows/ci.yml` 75 | - `Makefile.toml` 76 | - `examples/README.md` 77 | -------------------------------------------------------------------------------- /examples/e01_basic_ping_bot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e01_basic_ping_bot" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/e01_basic_ping_bot/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e01_basic_ping_bot/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use serenity::async_trait; 4 | use serenity::model::channel::Message; 5 | use serenity::model::gateway::Ready; 6 | use serenity::prelude::*; 7 | 8 | struct Handler; 9 | 10 | #[async_trait] 11 | impl EventHandler for Handler { 12 | // Set a handler for the `message` event. This is called whenever a new message is received. 13 | // 14 | // Event handlers are dispatched through a threadpool, and so multiple events can be 15 | // dispatched simultaneously. 16 | async fn message(&self, ctx: Context, msg: Message) { 17 | if msg.content == "!ping" { 18 | // Sending a message can fail, due to a network error, an authentication error, or lack 19 | // of permissions to post in the channel, so log to stdout when some error happens, 20 | // with a description of it. 21 | if let Err(why) = msg.channel_id.say(&ctx.http, "Pong!").await { 22 | println!("Error sending message: {why:?}"); 23 | } 24 | } 25 | } 26 | 27 | // Set a handler to be called on the `ready` event. This is called when a shard is booted, and 28 | // a READY payload is sent by Discord. This payload contains data like the current user's guild 29 | // Ids, current user data, private channels, and more. 30 | // 31 | // In this case, just print what the current user's username is. 32 | async fn ready(&self, _: Context, ready: Ready) { 33 | println!("{} is connected!", ready.user.name); 34 | } 35 | } 36 | 37 | #[tokio::main] 38 | async fn main() { 39 | // Configure the client with your Discord bot token in the environment. 40 | let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); 41 | // Set gateway intents, which decides what events the bot will be notified about 42 | let intents = GatewayIntents::GUILD_MESSAGES 43 | | GatewayIntents::DIRECT_MESSAGES 44 | | GatewayIntents::MESSAGE_CONTENT; 45 | 46 | // Create a new instance of the Client, logging in as a bot. This will automatically prepend 47 | // your bot token with "Bot ", which is a requirement by Discord for bot users. 48 | let mut client = 49 | Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); 50 | 51 | // Finally, start a single shard, and start listening to events. 52 | // 53 | // Shards will automatically attempt to reconnect, and will perform exponential backoff until 54 | // it reconnects. 55 | if let Err(why) = client.start().await { 56 | println!("Client error: {why:?}"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/e02_transparent_guild_sharding/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e02_transparent_guild_sharding" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/e02_transparent_guild_sharding/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e02_transparent_guild_sharding/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use serenity::async_trait; 4 | use serenity::model::channel::Message; 5 | use serenity::model::gateway::Ready; 6 | use serenity::prelude::*; 7 | 8 | // Serenity implements transparent sharding in a way that you do not need to handle separate 9 | // processes or connections manually. 10 | // 11 | // Transparent sharding is useful for a shared cache. Instead of having caches with duplicated 12 | // data, a shared cache means all your data can be easily accessible across all shards. 13 | // 14 | // If your bot is on many guilds - or over the maximum of 2500 - then you should/must use guild 15 | // sharding. 16 | // 17 | // This is an example file showing how guild sharding works. For this to properly be able to be 18 | // seen in effect, your bot should be in at least 2 guilds. 19 | // 20 | // Taking a scenario of 2 guilds, try saying "!ping" in one guild. It should print either "0" or 21 | // "1" in the console. Saying "!ping" in the other guild, it should cache the other number in the 22 | // console. This confirms that guild sharding works. 23 | struct Handler; 24 | 25 | #[async_trait] 26 | impl EventHandler for Handler { 27 | async fn message(&self, ctx: Context, msg: Message) { 28 | if msg.content == "!ping" { 29 | println!("Shard {}", ctx.shard_id); 30 | 31 | if let Err(why) = msg.channel_id.say(&ctx.http, "Pong!").await { 32 | println!("Error sending message: {why:?}"); 33 | } 34 | } 35 | } 36 | 37 | async fn ready(&self, _: Context, ready: Ready) { 38 | println!("{} is connected!", ready.user.name); 39 | } 40 | } 41 | 42 | #[tokio::main] 43 | async fn main() { 44 | // Configure the client with your Discord bot token in the environment. 45 | let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); 46 | let intents = GatewayIntents::GUILD_MESSAGES 47 | | GatewayIntents::DIRECT_MESSAGES 48 | | GatewayIntents::MESSAGE_CONTENT; 49 | let mut client = 50 | Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); 51 | 52 | // The total number of shards to use. The "current shard number" of a shard - that is, the 53 | // shard it is assigned to - is indexed at 0, while the total shard count is indexed at 1. 54 | // 55 | // This means if you have 5 shards, your total shard count will be 5, while each shard will be 56 | // assigned numbers 0 through 4. 57 | if let Err(why) = client.start_shards(2).await { 58 | println!("Client error: {why:?}"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/e03_struct_utilities/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e03_struct_utilities" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/e03_struct_utilities/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e03_struct_utilities/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use serenity::async_trait; 4 | use serenity::builder::CreateMessage; 5 | use serenity::model::channel::Message; 6 | use serenity::model::gateway::Ready; 7 | use serenity::prelude::*; 8 | 9 | struct Handler; 10 | 11 | #[async_trait] 12 | impl EventHandler for Handler { 13 | async fn message(&self, context: Context, msg: Message) { 14 | if msg.content == "!messageme" { 15 | // If the `utils`-feature is enabled, then model structs will have a lot of useful 16 | // methods implemented, to avoid using an often otherwise bulky Context, or even much 17 | // lower-level `rest` method. 18 | // 19 | // In this case, you can direct message a User directly by simply calling a method on 20 | // its instance, with the content of the message. 21 | let builder = CreateMessage::new().content("Hello!"); 22 | let dm = msg.author.dm(&context, builder).await; 23 | 24 | if let Err(why) = dm { 25 | println!("Error when direct messaging user: {why:?}"); 26 | } 27 | } 28 | } 29 | 30 | async fn ready(&self, _: Context, ready: Ready) { 31 | println!("{} is connected!", ready.user.name); 32 | } 33 | } 34 | 35 | #[tokio::main] 36 | async fn main() { 37 | // Configure the client with your Discord bot token in the environment. 38 | let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); 39 | let intents = GatewayIntents::GUILD_MESSAGES 40 | | GatewayIntents::DIRECT_MESSAGES 41 | | GatewayIntents::MESSAGE_CONTENT; 42 | let mut client = 43 | Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); 44 | 45 | if let Err(why) = client.start().await { 46 | println!("Client error: {why:?}"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/e04_message_builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e04_message_builder" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/e04_message_builder/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e04_message_builder/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use serenity::async_trait; 4 | use serenity::model::channel::Message; 5 | use serenity::model::gateway::Ready; 6 | use serenity::prelude::*; 7 | use serenity::utils::MessageBuilder; 8 | 9 | struct Handler; 10 | 11 | #[async_trait] 12 | impl EventHandler for Handler { 13 | async fn message(&self, context: Context, msg: Message) { 14 | if msg.content == "!ping" { 15 | let channel = match msg.channel_id.to_channel(&context).await { 16 | Ok(channel) => channel, 17 | Err(why) => { 18 | println!("Error getting channel: {why:?}"); 19 | 20 | return; 21 | }, 22 | }; 23 | 24 | // The message builder allows for creating a message by mentioning users dynamically, 25 | // pushing "safe" versions of content (such as bolding normalized content), displaying 26 | // emojis, and more. 27 | let response = MessageBuilder::new() 28 | .push("User ") 29 | .push_bold_safe(&msg.author.name) 30 | .push(" used the 'ping' command in the ") 31 | .mention(&channel) 32 | .push(" channel") 33 | .build(); 34 | 35 | if let Err(why) = msg.channel_id.say(&context.http, &response).await { 36 | println!("Error sending message: {why:?}"); 37 | } 38 | } 39 | } 40 | 41 | async fn ready(&self, _: Context, ready: Ready) { 42 | println!("{} is connected!", ready.user.name); 43 | } 44 | } 45 | 46 | #[tokio::main] 47 | async fn main() { 48 | // Configure the client with your Discord bot token in the environment. 49 | let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); 50 | let intents = GatewayIntents::GUILD_MESSAGES 51 | | GatewayIntents::DIRECT_MESSAGES 52 | | GatewayIntents::MESSAGE_CONTENT; 53 | let mut client = 54 | Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); 55 | 56 | if let Err(why) = client.start().await { 57 | println!("Client error: {why:?}"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/e05_command_framework/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e05_command_framework" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies.serenity] 8 | features = ["framework", "standard_framework", "rustls_backend"] 9 | path = "../../" 10 | 11 | [dependencies.tokio] 12 | version = "1.0" 13 | features = ["macros", "rt-multi-thread"] 14 | -------------------------------------------------------------------------------- /examples/e05_command_framework/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e06_sample_bot_structure/.env.example: -------------------------------------------------------------------------------- 1 | # This declares an environment variable named "DISCORD_TOKEN" with the given 2 | # value. When calling `dotenv::dotenv()`, it will read the `.env` file and parse 3 | # these key-value pairs and insert them into the environment. 4 | # 5 | # Environment variables are separated by newlines and must not have space 6 | # around the equals sign (`=`). 7 | DISCORD_TOKEN=put your token here 8 | # Declares the level of logging to use. Read the documentation for the `log` 9 | # and `env_logger` crates for more information. 10 | RUST_LOG=debug 11 | -------------------------------------------------------------------------------- /examples/e06_sample_bot_structure/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e06_sample_bot_structure" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | dotenv = "0.15" 9 | tracing = "0.1.23" 10 | tracing-subscriber = "0.3" 11 | 12 | [dependencies.tokio] 13 | version = "1.0" 14 | features = ["macros", "signal", "rt-multi-thread"] 15 | 16 | [dependencies.serenity] 17 | features = ["cache", "framework", "standard_framework", "rustls_backend"] 18 | path = "../../" 19 | -------------------------------------------------------------------------------- /examples/e06_sample_bot_structure/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e06_sample_bot_structure/src/commands/math.rs: -------------------------------------------------------------------------------- 1 | use serenity::framework::standard::macros::command; 2 | use serenity::framework::standard::{Args, CommandResult}; 3 | use serenity::model::prelude::*; 4 | use serenity::prelude::*; 5 | 6 | #[command] 7 | pub async fn multiply(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { 8 | let one = args.single::()?; 9 | let two = args.single::()?; 10 | 11 | let product = one * two; 12 | 13 | msg.channel_id.say(&ctx.http, product.to_string()).await?; 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /examples/e06_sample_bot_structure/src/commands/meta.rs: -------------------------------------------------------------------------------- 1 | use serenity::framework::standard::macros::command; 2 | use serenity::framework::standard::CommandResult; 3 | use serenity::model::prelude::*; 4 | use serenity::prelude::*; 5 | 6 | #[command] 7 | async fn ping(ctx: &Context, msg: &Message) -> CommandResult { 8 | msg.channel_id.say(&ctx.http, "Pong!").await?; 9 | 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /examples/e06_sample_bot_structure/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod math; 2 | pub mod meta; 3 | pub mod owner; 4 | -------------------------------------------------------------------------------- /examples/e06_sample_bot_structure/src/commands/owner.rs: -------------------------------------------------------------------------------- 1 | use serenity::framework::standard::macros::command; 2 | use serenity::framework::standard::CommandResult; 3 | use serenity::model::prelude::*; 4 | use serenity::prelude::*; 5 | 6 | use crate::ShardManagerContainer; 7 | 8 | #[command] 9 | #[owners_only] 10 | async fn quit(ctx: &Context, msg: &Message) -> CommandResult { 11 | let data = ctx.data.read().await; 12 | 13 | if let Some(manager) = data.get::() { 14 | msg.reply(ctx, "Shutting down!").await?; 15 | manager.shutdown_all().await; 16 | } else { 17 | msg.reply(ctx, "There was a problem getting the shard manager").await?; 18 | 19 | return Ok(()); 20 | } 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /examples/e06_sample_bot_structure/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Requires the 'framework' feature flag be enabled in your project's `Cargo.toml`. 2 | //! 3 | //! This can be enabled by specifying the feature in the dependency section: 4 | //! 5 | //! ```toml 6 | //! [dependencies.serenity] 7 | //! git = "https://github.com/serenity-rs/serenity.git" 8 | //! features = ["framework", "standard_framework"] 9 | //! ``` 10 | #![allow(deprecated)] // We recommend migrating to poise, instead of using the standard command framework. 11 | mod commands; 12 | 13 | use std::collections::HashSet; 14 | use std::env; 15 | use std::sync::Arc; 16 | 17 | use serenity::async_trait; 18 | use serenity::framework::standard::macros::group; 19 | use serenity::framework::standard::Configuration; 20 | use serenity::framework::StandardFramework; 21 | use serenity::gateway::ShardManager; 22 | use serenity::http::Http; 23 | use serenity::model::event::ResumedEvent; 24 | use serenity::model::gateway::Ready; 25 | use serenity::prelude::*; 26 | use tracing::{error, info}; 27 | 28 | use crate::commands::math::*; 29 | use crate::commands::meta::*; 30 | use crate::commands::owner::*; 31 | 32 | pub struct ShardManagerContainer; 33 | 34 | impl TypeMapKey for ShardManagerContainer { 35 | type Value = Arc; 36 | } 37 | 38 | struct Handler; 39 | 40 | #[async_trait] 41 | impl EventHandler for Handler { 42 | async fn ready(&self, _: Context, ready: Ready) { 43 | info!("Connected as {}", ready.user.name); 44 | } 45 | 46 | async fn resume(&self, _: Context, _: ResumedEvent) { 47 | info!("Resumed"); 48 | } 49 | } 50 | 51 | #[group] 52 | #[commands(multiply, ping, quit)] 53 | struct General; 54 | 55 | #[tokio::main] 56 | async fn main() { 57 | // This will load the environment variables located at `./.env`, relative to the CWD. 58 | // See `./.env.example` for an example on how to structure this. 59 | dotenv::dotenv().expect("Failed to load .env file"); 60 | 61 | // Initialize the logger to use environment variables. 62 | // 63 | // In this case, a good default is setting the environment variable `RUST_LOG` to `debug`. 64 | tracing_subscriber::fmt::init(); 65 | 66 | let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); 67 | 68 | let http = Http::new(&token); 69 | 70 | // We will fetch your bot's owners and id 71 | let (owners, _bot_id) = match http.get_current_application_info().await { 72 | Ok(info) => { 73 | let mut owners = HashSet::new(); 74 | if let Some(owner) = &info.owner { 75 | owners.insert(owner.id); 76 | } 77 | 78 | (owners, info.id) 79 | }, 80 | Err(why) => panic!("Could not access application info: {:?}", why), 81 | }; 82 | 83 | // Create the framework 84 | let framework = StandardFramework::new().group(&GENERAL_GROUP); 85 | framework.configure(Configuration::new().owners(owners).prefix("~")); 86 | 87 | let intents = GatewayIntents::GUILD_MESSAGES 88 | | GatewayIntents::DIRECT_MESSAGES 89 | | GatewayIntents::MESSAGE_CONTENT; 90 | let mut client = Client::builder(&token, intents) 91 | .framework(framework) 92 | .event_handler(Handler) 93 | .await 94 | .expect("Err creating client"); 95 | 96 | { 97 | let mut data = client.data.write().await; 98 | data.insert::(client.shard_manager.clone()); 99 | } 100 | 101 | let shard_manager = client.shard_manager.clone(); 102 | 103 | tokio::spawn(async move { 104 | tokio::signal::ctrl_c().await.expect("Could not register ctrl+c handler"); 105 | shard_manager.shutdown_all().await; 106 | }); 107 | 108 | if let Err(why) = client.start().await { 109 | error!("Client error: {:?}", why); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /examples/e07_env_logging/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e07_env_logging" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | tracing = "0.1.23" 9 | tracing-subscriber = "0.3" 10 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 11 | 12 | [dependencies.serenity] 13 | features = ["client", "rustls_backend"] 14 | path = "../../" 15 | -------------------------------------------------------------------------------- /examples/e07_env_logging/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [env] 4 | RUST_LOG = "info" 5 | 6 | [tasks.examples_build] 7 | alias = "build" 8 | 9 | [tasks.examples_build_release] 10 | alias = "build_release" 11 | 12 | [tasks.examples_run] 13 | alias = "run" 14 | 15 | [tasks.examples_run_release] 16 | alias = "run_release" 17 | -------------------------------------------------------------------------------- /examples/e07_env_logging/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] // We recommend migrating to poise, instead of using the standard command framework. 2 | 3 | use std::env; 4 | 5 | use serenity::async_trait; 6 | use serenity::framework::standard::macros::{command, group, hook}; 7 | use serenity::framework::standard::{CommandResult, Configuration, StandardFramework}; 8 | use serenity::model::channel::Message; 9 | use serenity::model::event::ResumedEvent; 10 | use serenity::model::gateway::Ready; 11 | use serenity::prelude::*; 12 | use tracing::{debug, error, info, instrument}; 13 | 14 | struct Handler; 15 | 16 | #[async_trait] 17 | impl EventHandler for Handler { 18 | async fn ready(&self, _: Context, ready: Ready) { 19 | // Log at the INFO level. This is a macro from the `tracing` crate. 20 | info!("{} is connected!", ready.user.name); 21 | } 22 | 23 | // For instrument to work, all parameters must implement Debug. 24 | // 25 | // Handler doesn't implement Debug here, so we specify to skip that argument. 26 | // Context doesn't implement Debug either, so it is also skipped. 27 | #[instrument(skip(self, _ctx))] 28 | async fn resume(&self, _ctx: Context, _resume: ResumedEvent) { 29 | // Log at the DEBUG level. 30 | // 31 | // In this example, this will not show up in the logs because DEBUG is 32 | // below INFO, which is the set debug level. 33 | debug!("Resumed"); 34 | } 35 | } 36 | 37 | #[hook] 38 | // instrument will show additional information on all the logs that happen inside the function. 39 | // 40 | // This additional information includes the function name, along with all it's arguments formatted 41 | // with the Debug impl. This additional information will also only be shown if the LOG level is set 42 | // to `debug` 43 | #[instrument] 44 | async fn before(_: &Context, msg: &Message, command_name: &str) -> bool { 45 | info!("Got command '{}' by user '{}'", command_name, msg.author.name); 46 | 47 | true 48 | } 49 | 50 | #[group] 51 | #[commands(ping)] 52 | struct General; 53 | 54 | #[tokio::main] 55 | #[instrument] 56 | async fn main() { 57 | // Call tracing_subscriber's initialize function, which configures `tracing` via environment 58 | // variables. 59 | // 60 | // For example, you can say to log all levels INFO and up via setting the environment variable 61 | // `RUST_LOG` to `INFO`. 62 | // 63 | // This environment variable is already preset if you use cargo-make to run the example. 64 | tracing_subscriber::fmt::init(); 65 | 66 | // Configure the client with your Discord bot token in the environment. 67 | let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); 68 | 69 | let framework = StandardFramework::new().before(before).group(&GENERAL_GROUP); 70 | framework.configure(Configuration::new().prefix("~")); 71 | 72 | let intents = GatewayIntents::GUILD_MESSAGES 73 | | GatewayIntents::DIRECT_MESSAGES 74 | | GatewayIntents::MESSAGE_CONTENT; 75 | let mut client = Client::builder(&token, intents) 76 | .event_handler(Handler) 77 | .framework(framework) 78 | .await 79 | .expect("Err creating client"); 80 | 81 | if let Err(why) = client.start().await { 82 | error!("Client error: {:?}", why); 83 | } 84 | } 85 | 86 | // Currently, the instrument macro doesn't work with commands. 87 | // If you wish to instrument commands, use it on the before function. 88 | #[command] 89 | async fn ping(ctx: &Context, msg: &Message) -> CommandResult { 90 | if let Err(why) = msg.channel_id.say(&ctx.http, "Pong! : )").await { 91 | error!("Error sending message: {:?}", why); 92 | } 93 | 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /examples/e08_shard_manager/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e08_shard_manager" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "time"] } 9 | 10 | [dependencies.serenity] 11 | default-features = false 12 | features = ["client", "gateway", "rustls_backend", "model"] 13 | path = "../../" 14 | -------------------------------------------------------------------------------- /examples/e08_shard_manager/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e08_shard_manager/src/main.rs: -------------------------------------------------------------------------------- 1 | //! This is an example showing how to interact with the client's `ShardManager`, which is a struct 2 | //! that can be used to interact with shards. This allows an easy method of retrieving shards' 3 | //! current status, restarting them, or shutting them down. 4 | //! 5 | //! In this example, we run two shards; this means that there will be two WebSocket connections to 6 | //! Discord, and each will receive events for _approximately_ 1/2 of all guilds that the bot is on. 7 | //! 8 | //! This isn't particularly useful for small bots, but is useful for large bots that may need to 9 | //! split load on separate VPSs or dedicated servers. Additionally, Discord requires that there be 10 | //! at least one shard for every 11 | //! 2500 guilds that a bot is on. 12 | //! 13 | //! For the purposes of this example, we'll print the current statuses of the two shards to the 14 | //! terminal every 30 seconds. This includes the ID of the shard, the current connection stage, 15 | //! (e.g. "Connecting" or "Connected"), and the approximate WebSocket latency (time between when a 16 | //! heartbeat is sent to Discord and when a heartbeat acknowledgement is received). 17 | //! 18 | //! # Notes 19 | //! 20 | //! Note that it may take a minute or more for a latency to be recorded or to update, depending on 21 | //! how often Discord tells the client to send a heartbeat. 22 | use std::env; 23 | use std::time::Duration; 24 | 25 | use serenity::async_trait; 26 | use serenity::model::gateway::Ready; 27 | use serenity::prelude::*; 28 | use tokio::time::sleep; 29 | 30 | struct Handler; 31 | 32 | #[async_trait] 33 | impl EventHandler for Handler { 34 | async fn ready(&self, _: Context, ready: Ready) { 35 | if let Some(shard) = ready.shard { 36 | // Note that array index 0 is 0-indexed, while index 1 is 1-indexed. 37 | // 38 | // This may seem unintuitive, but it models Discord's behaviour. 39 | println!("{} is connected on shard {}/{}!", ready.user.name, shard.id, shard.total); 40 | } 41 | } 42 | } 43 | 44 | #[tokio::main] 45 | async fn main() { 46 | // Configure the client with your Discord bot token in the environment. 47 | let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); 48 | 49 | let intents = GatewayIntents::GUILD_MESSAGES 50 | | GatewayIntents::DIRECT_MESSAGES 51 | | GatewayIntents::MESSAGE_CONTENT; 52 | let mut client = 53 | Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); 54 | 55 | // Here we clone a lock to the Shard Manager, and then move it into a new thread. The thread 56 | // will unlock the manager and print shards' status on a loop. 57 | let manager = client.shard_manager.clone(); 58 | 59 | tokio::spawn(async move { 60 | loop { 61 | sleep(Duration::from_secs(30)).await; 62 | 63 | let shard_runners = manager.runners.lock().await; 64 | 65 | for (id, runner) in shard_runners.iter() { 66 | println!( 67 | "Shard ID {} is {} with a latency of {:?}", 68 | id, runner.stage, runner.latency, 69 | ); 70 | } 71 | } 72 | }); 73 | 74 | // Start two shards. Note that there is an ~5 second ratelimit period between when one shard 75 | // can start after another. 76 | if let Err(why) = client.start_shards(2).await { 77 | println!("Client error: {why:?}"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/e09_create_message_builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e09_create_message_builder" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "chrono"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/e09_create_message_builder/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e09_create_message_builder/ferris_eyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/serenity/bb9610216eed49ba0c1fb9bc54fd0219d759df00/examples/e09_create_message_builder/ferris_eyes.png -------------------------------------------------------------------------------- /examples/e09_create_message_builder/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use serenity::async_trait; 4 | use serenity::builder::{CreateAttachment, CreateEmbed, CreateEmbedFooter, CreateMessage}; 5 | use serenity::model::channel::Message; 6 | use serenity::model::gateway::Ready; 7 | use serenity::model::Timestamp; 8 | use serenity::prelude::*; 9 | 10 | struct Handler; 11 | 12 | #[async_trait] 13 | impl EventHandler for Handler { 14 | async fn message(&self, ctx: Context, msg: Message) { 15 | if msg.content == "!hello" { 16 | // The create message builder allows you to easily create embeds and messages using a 17 | // builder syntax. 18 | // This example will create a message that says "Hello, World!", with an embed that has 19 | // a title, description, an image, three fields, and a footer. 20 | let footer = CreateEmbedFooter::new("This is a footer"); 21 | let embed = CreateEmbed::new() 22 | .title("This is a title") 23 | .description("This is a description") 24 | .image("attachment://ferris_eyes.png") 25 | .fields(vec![ 26 | ("This is the first field", "This is a field body", true), 27 | ("This is the second field", "Both fields are inline", true), 28 | ]) 29 | .field("This is the third field", "This is not an inline field", false) 30 | .footer(footer) 31 | // Add a timestamp for the current time 32 | // This also accepts a rfc3339 Timestamp 33 | .timestamp(Timestamp::now()); 34 | let builder = CreateMessage::new() 35 | .content("Hello, World!") 36 | .embed(embed) 37 | .add_file(CreateAttachment::path("./ferris_eyes.png").await.unwrap()); 38 | let msg = msg.channel_id.send_message(&ctx.http, builder).await; 39 | 40 | if let Err(why) = msg { 41 | println!("Error sending message: {why:?}"); 42 | } 43 | } 44 | } 45 | 46 | async fn ready(&self, _: Context, ready: Ready) { 47 | println!("{} is connected!", ready.user.name); 48 | } 49 | } 50 | 51 | #[tokio::main] 52 | async fn main() { 53 | // Configure the client with your Discord bot token in the environment. 54 | let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); 55 | let intents = GatewayIntents::GUILD_MESSAGES 56 | | GatewayIntents::DIRECT_MESSAGES 57 | | GatewayIntents::MESSAGE_CONTENT; 58 | let mut client = 59 | Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); 60 | 61 | if let Err(why) = client.start().await { 62 | println!("Client error: {why:?}"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/e10_collectors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e10_collectors" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies.serenity] 8 | features = ["framework", "standard_framework", "rustls_backend", "collector"] 9 | path = "../../" 10 | 11 | [dependencies] 12 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 13 | -------------------------------------------------------------------------------- /examples/e10_collectors/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e11_gateway_intents/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e11_gateway_intents" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/e11_gateway_intents/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e11_gateway_intents/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use serenity::async_trait; 4 | use serenity::model::channel::Message; 5 | use serenity::model::gateway::{Presence, Ready}; 6 | use serenity::prelude::*; 7 | 8 | struct Handler; 9 | 10 | #[async_trait] 11 | impl EventHandler for Handler { 12 | // This event will be dispatched for guilds, but not for direct messages. 13 | async fn message(&self, _ctx: Context, msg: Message) { 14 | println!("Received message: {}", msg.content); 15 | } 16 | 17 | // As the intents set in this example, this event shall never be dispatched. 18 | // Try it by changing your status. 19 | async fn presence_update(&self, _ctx: Context, _new_data: Presence) { 20 | println!("Presence Update"); 21 | } 22 | 23 | async fn ready(&self, _: Context, ready: Ready) { 24 | println!("{} is connected!", ready.user.name); 25 | } 26 | } 27 | 28 | #[tokio::main] 29 | async fn main() { 30 | // Configure the client with your Discord bot token in the environment. 31 | let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); 32 | 33 | // Intents are a bitflag, bitwise operations can be used to dictate which intents to use 34 | let intents = 35 | GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; 36 | // Build our client. 37 | let mut client = Client::builder(token, intents) 38 | .event_handler(Handler) 39 | .await 40 | .expect("Error creating client"); 41 | 42 | // Finally, start a single shard, and start listening to events. 43 | // 44 | // Shards will automatically attempt to reconnect, and will perform exponential backoff until 45 | // it reconnects. 46 | if let Err(why) = client.start().await { 47 | println!("Client error: {why:?}"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/e12_global_data/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e12_global_data" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../" } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/e12_global_data/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e13_parallel_loops/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e13_parallel_loops" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | sys-info = "0.9" 11 | chrono = { version = "0.4", default-features = false, features = ["clock"] } 12 | -------------------------------------------------------------------------------- /examples/e13_parallel_loops/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e14_slash_commands" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "collector"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/README.md: -------------------------------------------------------------------------------- 1 | # Slash commands 2 | 3 | This example demonstrates serenity's low-level slash command functions. It is 4 | possible to write a bot just with these, but it's usually easier to use the 5 | [poise] framework, a high-level framework for slash commands (and text 6 | commands, too). 7 | 8 | [poise]: https://github.com/serenity-rs/poise/ 9 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/src/commands/attachmentinput.rs: -------------------------------------------------------------------------------- 1 | use serenity::builder::{CreateCommand, CreateCommandOption}; 2 | use serenity::model::application::{CommandOptionType, ResolvedOption, ResolvedValue}; 3 | 4 | pub fn run(options: &[ResolvedOption]) -> String { 5 | if let Some(ResolvedOption { 6 | value: ResolvedValue::Attachment(attachment), .. 7 | }) = options.first() 8 | { 9 | format!("Attachment name: {}, attachment size: {}", attachment.filename, attachment.size) 10 | } else { 11 | "Please provide a valid attachment".to_string() 12 | } 13 | } 14 | 15 | pub fn register() -> CreateCommand { 16 | CreateCommand::new("attachmentinput") 17 | .description("Test command for attachment input") 18 | .add_option( 19 | CreateCommandOption::new(CommandOptionType::Attachment, "attachment", "A file") 20 | .required(true), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/src/commands/id.rs: -------------------------------------------------------------------------------- 1 | use serenity::builder::{CreateCommand, CreateCommandOption}; 2 | use serenity::model::application::{CommandOptionType, ResolvedOption, ResolvedValue}; 3 | 4 | pub fn run(options: &[ResolvedOption]) -> String { 5 | if let Some(ResolvedOption { 6 | value: ResolvedValue::User(user, _), .. 7 | }) = options.first() 8 | { 9 | format!("{}'s id is {}", user.tag(), user.id) 10 | } else { 11 | "Please provide a valid user".to_string() 12 | } 13 | } 14 | 15 | pub fn register() -> CreateCommand { 16 | CreateCommand::new("id").description("Get a user id").add_option( 17 | CreateCommandOption::new(CommandOptionType::User, "id", "The user to lookup") 18 | .required(true), 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod attachmentinput; 2 | pub mod id; 3 | pub mod modal; 4 | pub mod numberinput; 5 | pub mod ping; 6 | pub mod welcome; 7 | pub mod wonderful_command; 8 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/src/commands/modal.rs: -------------------------------------------------------------------------------- 1 | use serenity::builder::*; 2 | use serenity::model::prelude::*; 3 | use serenity::prelude::*; 4 | use serenity::utils::CreateQuickModal; 5 | 6 | pub async fn run(ctx: &Context, interaction: &CommandInteraction) -> Result<(), serenity::Error> { 7 | let modal = CreateQuickModal::new("About you") 8 | .timeout(std::time::Duration::from_secs(600)) 9 | .short_field("First name") 10 | .short_field("Last name") 11 | .paragraph_field("Hobbies and interests"); 12 | let response = interaction.quick_modal(ctx, modal).await?.unwrap(); 13 | 14 | let inputs = response.inputs; 15 | let (first_name, last_name, hobbies) = (&inputs[0], &inputs[1], &inputs[2]); 16 | 17 | response 18 | .interaction 19 | .create_response( 20 | ctx, 21 | CreateInteractionResponse::Message(CreateInteractionResponseMessage::new().content( 22 | format!("**Name**: {first_name} {last_name}\n\nHobbies and interests: {hobbies}"), 23 | )), 24 | ) 25 | .await?; 26 | Ok(()) 27 | } 28 | 29 | pub fn register() -> CreateCommand { 30 | CreateCommand::new("modal").description("Asks some details about you") 31 | } 32 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/src/commands/numberinput.rs: -------------------------------------------------------------------------------- 1 | use serenity::builder::{CreateCommand, CreateCommandOption}; 2 | use serenity::model::application::CommandOptionType; 3 | 4 | pub fn register() -> CreateCommand { 5 | CreateCommand::new("numberinput") 6 | .description("Test command for number input") 7 | .add_option( 8 | CreateCommandOption::new(CommandOptionType::Integer, "int", "An integer from 5 to 10") 9 | .min_int_value(5) 10 | .max_int_value(10) 11 | .required(true), 12 | ) 13 | .add_option( 14 | CreateCommandOption::new( 15 | CommandOptionType::Number, 16 | "number", 17 | "A float from -3.3 to 234.5", 18 | ) 19 | .min_number_value(-3.3) 20 | .max_number_value(234.5) 21 | .required(true), 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/src/commands/ping.rs: -------------------------------------------------------------------------------- 1 | use serenity::builder::CreateCommand; 2 | use serenity::model::application::ResolvedOption; 3 | 4 | pub fn run(_options: &[ResolvedOption]) -> String { 5 | "Hey, I'm alive!".to_string() 6 | } 7 | 8 | pub fn register() -> CreateCommand { 9 | CreateCommand::new("ping").description("A ping command") 10 | } 11 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/src/commands/welcome.rs: -------------------------------------------------------------------------------- 1 | use serenity::builder::{CreateCommand, CreateCommandOption}; 2 | use serenity::model::application::CommandOptionType; 3 | 4 | pub fn register() -> CreateCommand { 5 | CreateCommand::new("welcome") 6 | .description("Welcome a user") 7 | .name_localized("de", "begrüßen") 8 | .description_localized("de", "Einen Nutzer begrüßen") 9 | .add_option( 10 | CreateCommandOption::new(CommandOptionType::User, "user", "The user to welcome") 11 | .name_localized("de", "nutzer") 12 | .description_localized("de", "Der zu begrüßende Nutzer") 13 | .required(true), 14 | ) 15 | .add_option( 16 | CreateCommandOption::new(CommandOptionType::String, "message", "The message to send") 17 | .name_localized("de", "nachricht") 18 | .description_localized("de", "Die versendete Nachricht") 19 | .required(true) 20 | .add_string_choice_localized( 21 | "Welcome to our cool server! Ask me if you need help", 22 | "pizza", 23 | [( 24 | "de", 25 | "Willkommen auf unserem coolen Server! Frag mich, falls du Hilfe brauchst", 26 | )], 27 | ) 28 | .add_string_choice_localized("Hey, do you want a coffee?", "coffee", [( 29 | "de", 30 | "Hey, willst du einen Kaffee?", 31 | )]) 32 | .add_string_choice_localized( 33 | "Welcome to the club, you're now a good person. Well, I hope.", 34 | "club", 35 | [( 36 | "de", 37 | "Willkommen im Club, du bist jetzt ein guter Mensch. Naja, hoffentlich.", 38 | )], 39 | ) 40 | .add_string_choice_localized( 41 | "I hope that you brought a controller to play together!", 42 | "game", 43 | [("de", "Ich hoffe du hast einen Controller zum Spielen mitgebracht!")], 44 | ), 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/src/commands/wonderful_command.rs: -------------------------------------------------------------------------------- 1 | use serenity::builder::CreateCommand; 2 | 3 | pub fn register() -> CreateCommand { 4 | CreateCommand::new("wonderful_command").description("An amazing command") 5 | } 6 | -------------------------------------------------------------------------------- /examples/e14_slash_commands/src/main.rs: -------------------------------------------------------------------------------- 1 | mod commands; 2 | 3 | use std::env; 4 | 5 | use serenity::async_trait; 6 | use serenity::builder::{CreateInteractionResponse, CreateInteractionResponseMessage}; 7 | use serenity::model::application::{Command, Interaction}; 8 | use serenity::model::gateway::Ready; 9 | use serenity::model::id::GuildId; 10 | use serenity::prelude::*; 11 | 12 | struct Handler; 13 | 14 | #[async_trait] 15 | impl EventHandler for Handler { 16 | async fn interaction_create(&self, ctx: Context, interaction: Interaction) { 17 | if let Interaction::Command(command) = interaction { 18 | println!("Received command interaction: {command:#?}"); 19 | 20 | let content = match command.data.name.as_str() { 21 | "ping" => Some(commands::ping::run(&command.data.options())), 22 | "id" => Some(commands::id::run(&command.data.options())), 23 | "attachmentinput" => Some(commands::attachmentinput::run(&command.data.options())), 24 | "modal" => { 25 | commands::modal::run(&ctx, &command).await.unwrap(); 26 | None 27 | }, 28 | _ => Some("not implemented :(".to_string()), 29 | }; 30 | 31 | if let Some(content) = content { 32 | let data = CreateInteractionResponseMessage::new().content(content); 33 | let builder = CreateInteractionResponse::Message(data); 34 | if let Err(why) = command.create_response(&ctx.http, builder).await { 35 | println!("Cannot respond to slash command: {why}"); 36 | } 37 | } 38 | } 39 | } 40 | 41 | async fn ready(&self, ctx: Context, ready: Ready) { 42 | println!("{} is connected!", ready.user.name); 43 | 44 | let guild_id = GuildId::new( 45 | env::var("GUILD_ID") 46 | .expect("Expected GUILD_ID in environment") 47 | .parse() 48 | .expect("GUILD_ID must be an integer"), 49 | ); 50 | 51 | let commands = guild_id 52 | .set_commands(&ctx.http, vec![ 53 | commands::ping::register(), 54 | commands::id::register(), 55 | commands::welcome::register(), 56 | commands::numberinput::register(), 57 | commands::attachmentinput::register(), 58 | commands::modal::register(), 59 | ]) 60 | .await; 61 | 62 | println!("I now have the following guild slash commands: {commands:#?}"); 63 | 64 | let guild_command = 65 | Command::create_global_command(&ctx.http, commands::wonderful_command::register()) 66 | .await; 67 | 68 | println!("I created the following global slash command: {guild_command:#?}"); 69 | } 70 | } 71 | 72 | #[tokio::main] 73 | async fn main() { 74 | // Configure the client with your Discord bot token in the environment. 75 | let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); 76 | 77 | // Build our client. 78 | let mut client = Client::builder(token, GatewayIntents::empty()) 79 | .event_handler(Handler) 80 | .await 81 | .expect("Error creating client"); 82 | 83 | // Finally, start a single shard, and start listening to events. 84 | // 85 | // Shards will automatically attempt to reconnect, and will perform exponential backoff until 86 | // it reconnects. 87 | if let Err(why) = client.start().await { 88 | println!("Client error: {why:?}"); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/e15_simple_dashboard/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e15_simple_dashboard" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | rillrate = "0.41" 10 | notify = "=5.0.0-pre.14" 11 | 12 | tracing = "0.1" 13 | tracing-subscriber = "0.3" 14 | 15 | webbrowser = "0.8" 16 | 17 | [dependencies.serenity] 18 | path = "../../" 19 | 20 | [dependencies.tokio] 21 | version = "1" 22 | features = ["full"] 23 | 24 | [dependencies.reqwest] 25 | version = "0.11" 26 | default-features = false 27 | features = ["json", "rustls-tls"] 28 | 29 | [features] 30 | post-ping = [] 31 | -------------------------------------------------------------------------------- /examples/e15_simple_dashboard/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e16_sqlite_database/.gitignore: -------------------------------------------------------------------------------- 1 | database.sqlite* 2 | -------------------------------------------------------------------------------- /examples/e16_sqlite_database/.sqlx/query-597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT rowid, task FROM todo WHERE user_id = ? ORDER BY rowid LIMIT 1 OFFSET ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "rowid", 8 | "ordinal": 0, 9 | "type_info": "Int64" 10 | }, 11 | { 12 | "name": "task", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | } 16 | ], 17 | "parameters": { 18 | "Right": 2 19 | }, 20 | "nullable": [ 21 | false, 22 | false 23 | ] 24 | }, 25 | "hash": "597707a72d1ed8eab0cb48a3bef8cdb981362e089a462fa6d156b27b57468678" 26 | } 27 | -------------------------------------------------------------------------------- /examples/e16_sqlite_database/.sqlx/query-7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO todo (task, user_id) VALUES (?, ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "7636fc64c882305305814ffb66676ef09a92d3f1d46021b94ded4e9c073775d1" 12 | } 13 | -------------------------------------------------------------------------------- /examples/e16_sqlite_database/.sqlx/query-8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT task FROM todo WHERE user_id = ? ORDER BY rowid", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "task", 8 | "ordinal": 0, 9 | "type_info": "Text" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "8a7bb6fe3b960d1d10bc8442bb1494f2c758dd890293c313811a8c4acb8edaeb" 20 | } 21 | -------------------------------------------------------------------------------- /examples/e16_sqlite_database/.sqlx/query-90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM todo WHERE rowid = ?", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 1 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "90153b8cd85a905a1d5557ad4eb190e9be4cf55d7308973d74cb180cd2323f8a" 12 | } 13 | -------------------------------------------------------------------------------- /examples/e16_sqlite_database/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e16_sqlite_database" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] } 11 | -------------------------------------------------------------------------------- /examples/e16_sqlite_database/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e16_sqlite_database/README.md: -------------------------------------------------------------------------------- 1 | # Setting up the database 2 | 3 | In order to compile the project, a database needs to be set-up. That's because SQLx accesses the 4 | database at compile time to make sure your SQL queries are correct. 5 | 6 | To set up the database, download the [SQLx CLI](https://github.com/launchbadge/sqlx/tree/master/sqlx-cli) 7 | and run `sqlx database setup`. This command will create the database and its tables by applying 8 | the migration files in `migrations/`. 9 | 10 | Most SQLx CLI commands require the `DATABASE_URL` environment variable to be set to the database 11 | URL, for example `sqlite:database.sqlite` (where `sqlite:` is the protocol and `database.sqlite` the 12 | actual filename). A convenient way to supply this information to SQLx is to create a `.env` file 13 | which SQLx automatically detects and reads: 14 | 15 | ```rust 16 | DATABASE_URL=sqlite:database.sqlite 17 | ``` 18 | 19 | # Running the example 20 | 21 | ```sh 22 | # Note: due to a bug in SQLx (https://github.com/launchbadge/sqlx/issues/3099), 23 | # you have to provide the full path to `DATABASE_URL` when compiling. 24 | # Once the bug is fixed, you can omit `DATABASE_URL=...` and let SQLx read the `.env` file. 25 | DATABASE_URL=sqlite:examples/e16_sqlite_database/database.sqlite DISCORD_TOKEN=... cargo run 26 | ``` 27 | 28 | Interact with the bot via `~todo list`, `~todo add` and `~todo remove`. 29 | 30 | # What are migrations 31 | 32 | In SQLx, migrations are SQL query files that update the database schema. Most SQLx project have at 33 | least one migration file, often called `initial_migration`, which sets up tables initially. 34 | 35 | If you need to modify the database schema in the future, call `sqlx migrate add "MIGRATION NAME"` 36 | and write the migration queries into the newly created .sql file in `migrations/`. 37 | 38 | # Make it easy to host your bot 39 | 40 | Normally, users have to download and install SQLx CLI in order to compile your bot. Remember: 41 | SQLx accesses the database at compile time. However, you can enable building in "offline mode": 42 | https://github.com/launchbadge/sqlx/tree/master/sqlx-cli#enable-building-in-offline-mode-with-query. 43 | That way, your bot will work out-of-the-box with `cargo run`. 44 | 45 | Note that users still have to set `SQLX_OFFLINE` to `true` even if `sqlx-data.json` is present. 46 | 47 | Tip: create a git pre-commit hook which executes `cargo sqlx prepare` for you before every commit. 48 | See the `pre-commit` file for an example. Copy the file into `.git/hooks` to install the pre-commit 49 | hook. 50 | 51 | # Using SQLx 52 | 53 | SQLx's GitHub repository explains a lot about SQLx, like the difference between `query!` and 54 | `query_as!`. Please follow the links to learn more: 55 | 56 | - SQLx: https://github.com/launchbadge/sqlx 57 | - SQLx CLI: https://github.com/launchbadge/sqlx/tree/master/sqlx-cli 58 | -------------------------------------------------------------------------------- /examples/e16_sqlite_database/migrations/20210906145552_initial_migration.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | CREATE TABLE todo ( 3 | task TEXT NOT NULL, 4 | user_id INTEGER NOT NULL 5 | ) 6 | -------------------------------------------------------------------------------- /examples/e16_sqlite_database/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Caching SQL query analysis..." 4 | cargo sqlx prepare 5 | git add sqlx-data.json 6 | -------------------------------------------------------------------------------- /examples/e17_message_components/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e17_message_components" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "collector"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | dotenv = { version = "0.15.0" } 11 | -------------------------------------------------------------------------------- /examples/e17_message_components/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e18_webhook/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e18_webhook" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["rustls_backend", "model"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/e18_webhook/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e18_webhook/src/main.rs: -------------------------------------------------------------------------------- 1 | use serenity::builder::ExecuteWebhook; 2 | use serenity::http::Http; 3 | use serenity::model::webhook::Webhook; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | // You don't need a token when you are only dealing with webhooks. 8 | let http = Http::new(""); 9 | let webhook = Webhook::from_url(&http, "https://discord.com/api/webhooks/133742013374206969/hello-there-oPNtRN5UY5DVmBe7m1N0HE-replace-me-Dw9LRkgq3zI7LoW3Rb-k-q") 10 | .await 11 | .expect("Replace the webhook with your own"); 12 | 13 | let builder = ExecuteWebhook::new().content("hello there").username("Webhook test"); 14 | webhook.execute(&http, false, builder).await.expect("Could not execute webhook."); 15 | } 16 | -------------------------------------------------------------------------------- /examples/e19_interactions_endpoint/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e19_interactions_endpoint" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["builder", "interactions_endpoint"] } 9 | tiny_http = "0.12.0" 10 | -------------------------------------------------------------------------------- /examples/e19_interactions_endpoint/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /examples/e19_interactions_endpoint/src/main.rs: -------------------------------------------------------------------------------- 1 | use serenity::builder::*; 2 | use serenity::interactions_endpoint::Verifier; 3 | use serenity::json; 4 | use serenity::model::application::*; 5 | 6 | type Error = Box; 7 | 8 | fn handle_command(interaction: CommandInteraction) -> CreateInteractionResponse { 9 | CreateInteractionResponse::Message(CreateInteractionResponseMessage::new().content(format!( 10 | "Hello from interactions webhook HTTP server! <@{}>", 11 | interaction.user.id 12 | ))) 13 | } 14 | 15 | fn handle_request( 16 | mut request: tiny_http::Request, 17 | body: &mut Vec, 18 | verifier: &Verifier, 19 | ) -> Result<(), Error> { 20 | println!("Received request from {:?}", request.remote_addr()); 21 | 22 | // Read the request body (containing the interaction JSON) 23 | body.clear(); 24 | request.as_reader().read_to_end(body)?; 25 | 26 | // Reject request if it fails cryptographic verification 27 | // Discord rejects the interaction endpoints URL if this check is not done 28 | // (This part is very specific to your HTTP server crate of choice, so serenity cannot abstract 29 | // away the boilerplate) 30 | let find_header = 31 | |name| Some(request.headers().iter().find(|h| h.field.equiv(name))?.value.as_str()); 32 | let signature = find_header("X-Signature-Ed25519").ok_or("missing signature header")?; 33 | let timestamp = find_header("X-Signature-Timestamp").ok_or("missing timestamp header")?; 34 | if verifier.verify(signature, timestamp, body).is_err() { 35 | request.respond(tiny_http::Response::empty(401))?; 36 | return Ok(()); 37 | } 38 | 39 | // Build Discord response 40 | let response = match json::from_slice::(body)? { 41 | // Discord rejects the interaction endpoints URL if pings are not acknowledged 42 | Interaction::Ping(_) => CreateInteractionResponse::Pong, 43 | Interaction::Command(interaction) => handle_command(interaction), 44 | _ => return Ok(()), 45 | }; 46 | 47 | // Send the Discord response back via HTTP 48 | request.respond( 49 | tiny_http::Response::from_data(json::to_vec(&response)?) 50 | .with_header("Content-Type: application/json".parse::().unwrap()), 51 | )?; 52 | 53 | Ok(()) 54 | } 55 | 56 | fn main() -> Result<(), Error> { 57 | // Change this string to the Public Key value in your bot dashboard 58 | let verifier = 59 | Verifier::new("67c6bd767ca099e79efac9fcce4d2022a63bf7dea780e7f3d813f694c1597089"); 60 | 61 | // Setup an HTTP server and listen for incoming interaction requests 62 | // Choose any port here (but be consistent with the interactions endpoint URL in your bot 63 | // dashboard) 64 | let server = tiny_http::Server::http("0.0.0.0:8787")?; 65 | let mut body = Vec::new(); 66 | loop { 67 | let request = server.recv()?; 68 | if let Err(e) = handle_request(request, &mut body, &verifier) { 69 | eprintln!("Error while handling request: {e}"); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/testing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "testing" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | serenity = { path = "../../", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache", "collector"] } 9 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 10 | env_logger = "0.10.0" 11 | -------------------------------------------------------------------------------- /examples/testing/Makefile.toml: -------------------------------------------------------------------------------- 1 | extend = "../../Makefile.toml" 2 | 3 | [tasks.examples_build] 4 | alias = "build" 5 | 6 | [tasks.examples_build_release] 7 | alias = "build_release" 8 | 9 | [tasks.examples_run] 10 | alias = "run" 11 | 12 | [tasks.examples_run_release] 13 | alias = "run_release" 14 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/serenity/bb9610216eed49ba0c1fb9bc54fd0219d759df00/logo.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | match_block_trailing_comma = true 3 | newline_style = "Unix" 4 | use_field_init_shorthand = true 5 | use_small_heuristics = "Max" 6 | use_try_shorthand = true 7 | 8 | # Turn on once the rustfmt supporting these becomes available on rustup 9 | # width_heuristics = "Max" 10 | 11 | # nightly/unstable features 12 | wrap_comments = true 13 | comment_width = 100 14 | format_code_in_doc_comments = true 15 | group_imports = "StdExternalCrate" 16 | imports_granularity = "Module" 17 | imports_layout = "HorizontalVertical" 18 | match_arm_blocks = true 19 | normalize_comments = true 20 | overflow_delimited_expr = true 21 | struct_lit_single_line = false 22 | -------------------------------------------------------------------------------- /src/builder/add_member.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use super::Builder; 3 | #[cfg(feature = "http")] 4 | use crate::http::CacheHttp; 5 | #[cfg(feature = "http")] 6 | use crate::internal::prelude::*; 7 | use crate::model::prelude::*; 8 | 9 | /// A builder to add parameters when using [`GuildId::add_member`]. 10 | /// 11 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#add-guild-member). 12 | #[derive(Clone, Debug, Serialize)] 13 | #[must_use] 14 | pub struct AddMember { 15 | access_token: String, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | nick: Option, 18 | #[serde(skip_serializing_if = "Vec::is_empty")] 19 | roles: Vec, 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | mute: Option, 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | deaf: Option, 24 | } 25 | 26 | impl AddMember { 27 | /// Constructs a new builder with the given access token, leaving all other fields empty. 28 | pub fn new(access_token: String) -> Self { 29 | Self { 30 | access_token, 31 | nick: None, 32 | roles: Vec::new(), 33 | mute: None, 34 | deaf: None, 35 | } 36 | } 37 | 38 | /// Sets the OAuth2 access token for this request, replacing the current one. 39 | /// 40 | /// Requires the access token to have the `guilds.join` scope granted. 41 | pub fn access_token(mut self, access_token: impl Into) -> Self { 42 | self.access_token = access_token.into(); 43 | self 44 | } 45 | 46 | /// Sets the member's nickname. 47 | /// 48 | /// Requires the [Manage Nicknames] permission. 49 | /// 50 | /// [Manage Nicknames]: crate::model::permissions::Permissions::MANAGE_NICKNAMES 51 | pub fn nickname(mut self, nickname: impl Into) -> Self { 52 | self.nick = Some(nickname.into()); 53 | self 54 | } 55 | 56 | /// Sets the list of roles that the member should have. 57 | /// 58 | /// Requires the [Manage Roles] permission. 59 | /// 60 | /// [Manage Roles]: crate::model::permissions::Permissions::MANAGE_ROLES 61 | pub fn roles(mut self, roles: impl IntoIterator>) -> Self { 62 | self.roles = roles.into_iter().map(Into::into).collect(); 63 | self 64 | } 65 | 66 | /// Whether to mute the member. 67 | /// 68 | /// Requires the [Mute Members] permission. 69 | /// 70 | /// [Mute Members]: crate::model::permissions::Permissions::MUTE_MEMBERS 71 | pub fn mute(mut self, mute: bool) -> Self { 72 | self.mute = Some(mute); 73 | self 74 | } 75 | 76 | /// Whether to deafen the member. 77 | /// 78 | /// Requires the [Deafen Members] permission. 79 | /// 80 | /// [Deafen Members]: crate::model::permissions::Permissions::DEAFEN_MEMBERS 81 | pub fn deafen(mut self, deafen: bool) -> Self { 82 | self.deaf = Some(deafen); 83 | self 84 | } 85 | } 86 | 87 | #[cfg(feature = "http")] 88 | #[async_trait::async_trait] 89 | impl Builder for AddMember { 90 | type Context<'ctx> = (GuildId, UserId); 91 | type Built = Option; 92 | 93 | /// Adds a [`User`] to this guild with a valid OAuth2 access token. 94 | /// 95 | /// Returns the created [`Member`] object, or nothing if the user is already a member of the 96 | /// guild. 97 | /// 98 | /// # Errors 99 | /// 100 | /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. 101 | async fn execute( 102 | self, 103 | cache_http: impl CacheHttp, 104 | ctx: Self::Context<'_>, 105 | ) -> Result { 106 | cache_http.http().add_guild_member(ctx.0, ctx.1, &self).await 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/builder/bot_auth_parameters.rs: -------------------------------------------------------------------------------- 1 | use arrayvec::ArrayVec; 2 | use url::Url; 3 | 4 | #[cfg(feature = "http")] 5 | use crate::http::Http; 6 | #[cfg(feature = "http")] 7 | use crate::internal::prelude::*; 8 | use crate::model::prelude::*; 9 | 10 | /// A builder for constructing an invite link with custom OAuth2 scopes. 11 | #[derive(Debug, Clone, Default)] 12 | #[must_use] 13 | pub struct CreateBotAuthParameters { 14 | client_id: Option, 15 | scopes: Vec, 16 | permissions: Permissions, 17 | guild_id: Option, 18 | disable_guild_select: bool, 19 | } 20 | 21 | impl CreateBotAuthParameters { 22 | /// Equivalent to [`Self::default`]. 23 | pub fn new() -> Self { 24 | Self::default() 25 | } 26 | 27 | /// Builds the url with the provided data. 28 | #[must_use] 29 | pub fn build(self) -> String { 30 | let mut valid_data = ArrayVec::<_, 5>::new(); 31 | let bits = self.permissions.bits(); 32 | 33 | if let Some(client_id) = self.client_id { 34 | valid_data.push(("client_id", client_id.to_string())); 35 | } 36 | 37 | if !self.scopes.is_empty() { 38 | valid_data.push(( 39 | "scope", 40 | self.scopes.iter().map(ToString::to_string).collect::>().join(" "), 41 | )); 42 | } 43 | 44 | if bits != 0 { 45 | valid_data.push(("permissions", bits.to_string())); 46 | } 47 | 48 | if let Some(guild_id) = self.guild_id { 49 | valid_data.push(("guild", guild_id.to_string())); 50 | } 51 | 52 | if self.disable_guild_select { 53 | valid_data.push(("disable_guild_select", self.disable_guild_select.to_string())); 54 | } 55 | 56 | let url = Url::parse_with_params("https://discord.com/api/oauth2/authorize", &valid_data) 57 | .expect("failed to construct URL"); 58 | 59 | url.to_string() 60 | } 61 | 62 | /// Specify the client Id of your application. 63 | pub fn client_id(mut self, client_id: impl Into) -> Self { 64 | self.client_id = Some(client_id.into()); 65 | self 66 | } 67 | 68 | /// Automatically fetch and set the client Id of your application by inquiring Discord's API. 69 | /// 70 | /// # Errors 71 | /// 72 | /// Returns an [`HttpError::UnsuccessfulRequest`] if the user is not authorized for this 73 | /// endpoint. 74 | /// 75 | /// [`HttpError::UnsuccessfulRequest`]: crate::http::HttpError::UnsuccessfulRequest 76 | #[cfg(feature = "http")] 77 | pub async fn auto_client_id(mut self, http: impl AsRef) -> Result { 78 | self.client_id = http.as_ref().get_current_application_info().await.map(|v| Some(v.id))?; 79 | Ok(self) 80 | } 81 | 82 | /// Specify the scopes for your application. 83 | /// 84 | /// **Note**: This needs to include the [`Bot`] scope. 85 | /// 86 | /// [`Bot`]: Scope::Bot 87 | pub fn scopes(mut self, scopes: &[Scope]) -> Self { 88 | self.scopes = scopes.to_vec(); 89 | self 90 | } 91 | 92 | /// Specify the permissions your application requires. 93 | pub fn permissions(mut self, permissions: Permissions) -> Self { 94 | self.permissions = permissions; 95 | self 96 | } 97 | 98 | /// Specify the Id of the guild to prefill the dropdown picker for the user. 99 | pub fn guild_id(mut self, guild_id: impl Into) -> Self { 100 | self.guild_id = Some(guild_id.into()); 101 | self 102 | } 103 | 104 | /// Specify whether the user cannot change the guild in the dropdown picker. 105 | pub fn disable_guild_select(mut self, disable: bool) -> Self { 106 | self.disable_guild_select = disable; 107 | self 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/builder/create_forum_tag.rs: -------------------------------------------------------------------------------- 1 | use crate::model::prelude::*; 2 | 3 | /// [Discord docs](https://discord.com/developers/docs/resources/channel#forum-tag-object-forum-tag-structure) 4 | /// 5 | /// Contrary to the [`ForumTag`] struct, only the name field is required. 6 | #[must_use] 7 | #[derive(Clone, Debug, Serialize)] 8 | pub struct CreateForumTag { 9 | name: String, 10 | moderated: bool, 11 | emoji_id: Option, 12 | emoji_name: Option, 13 | } 14 | 15 | impl CreateForumTag { 16 | pub fn new(name: impl Into) -> Self { 17 | Self { 18 | name: name.into(), 19 | moderated: false, 20 | emoji_id: None, 21 | emoji_name: None, 22 | } 23 | } 24 | 25 | pub fn moderated(mut self, moderated: bool) -> Self { 26 | self.moderated = moderated; 27 | self 28 | } 29 | 30 | pub fn emoji(mut self, emoji: impl Into) -> Self { 31 | match emoji.into() { 32 | ReactionType::Custom { 33 | id, .. 34 | } => { 35 | self.emoji_id = Some(id); 36 | self.emoji_name = None; 37 | }, 38 | ReactionType::Unicode(unicode_emoji) => { 39 | self.emoji_id = None; 40 | self.emoji_name = Some(unicode_emoji); 41 | }, 42 | } 43 | self 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/builder/create_stage_instance.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use super::Builder; 3 | #[cfg(feature = "http")] 4 | use crate::http::CacheHttp; 5 | #[cfg(feature = "http")] 6 | use crate::internal::prelude::*; 7 | use crate::model::prelude::*; 8 | 9 | /// Builder for creating a stage instance 10 | /// 11 | /// [Discord docs](https://discord.com/developers/docs/resources/stage-instance#create-stage-instance) 12 | #[derive(Clone, Debug, Serialize)] 13 | #[must_use] 14 | pub struct CreateStageInstance<'a> { 15 | channel_id: Option, // required field, filled in Builder impl 16 | topic: String, 17 | privacy_level: StageInstancePrivacyLevel, 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | send_start_notification: Option, 20 | 21 | #[serde(skip)] 22 | audit_log_reason: Option<&'a str>, 23 | } 24 | 25 | impl<'a> CreateStageInstance<'a> { 26 | /// Creates a builder with the provided topic. 27 | pub fn new(topic: impl Into) -> Self { 28 | Self { 29 | channel_id: None, 30 | topic: topic.into(), 31 | privacy_level: StageInstancePrivacyLevel::default(), 32 | send_start_notification: None, 33 | audit_log_reason: None, 34 | } 35 | } 36 | 37 | /// Sets the topic of the stage channel instance, replacing the current value as set in 38 | /// [`Self::new`]. 39 | pub fn topic(mut self, topic: impl Into) -> Self { 40 | self.topic = topic.into(); 41 | self 42 | } 43 | 44 | /// Whether or not to notify @everyone that a stage instance has started. 45 | pub fn send_start_notification(mut self, send_start_notification: bool) -> Self { 46 | self.send_start_notification = Some(send_start_notification); 47 | self 48 | } 49 | 50 | /// Sets the request's audit log reason. 51 | pub fn audit_log_reason(mut self, reason: &'a str) -> Self { 52 | self.audit_log_reason = Some(reason); 53 | self 54 | } 55 | } 56 | 57 | #[cfg(feature = "http")] 58 | #[async_trait::async_trait] 59 | impl Builder for CreateStageInstance<'_> { 60 | type Context<'ctx> = ChannelId; 61 | type Built = StageInstance; 62 | 63 | /// Creates the stage instance. 64 | /// 65 | /// # Errors 66 | /// 67 | /// Returns [`Error::Http`] if there is already a stage instance currently. 68 | async fn execute( 69 | mut self, 70 | cache_http: impl CacheHttp, 71 | ctx: Self::Context<'_>, 72 | ) -> Result { 73 | self.channel_id = Some(ctx); 74 | cache_http.http().create_stage_instance(&self, self.audit_log_reason).await 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/builder/create_sticker.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use super::Builder; 3 | use super::CreateAttachment; 4 | #[cfg(feature = "http")] 5 | use crate::http::CacheHttp; 6 | #[cfg(feature = "http")] 7 | use crate::internal::prelude::*; 8 | #[cfg(feature = "http")] 9 | use crate::model::prelude::*; 10 | 11 | /// A builder to create a guild sticker 12 | /// 13 | /// [Discord docs](https://discord.com/developers/docs/resources/sticker#create-guild-sticker) 14 | #[derive(Clone, Debug)] 15 | #[must_use] 16 | pub struct CreateSticker<'a> { 17 | name: String, 18 | description: String, 19 | tags: String, 20 | file: CreateAttachment, 21 | audit_log_reason: Option<&'a str>, 22 | } 23 | 24 | impl<'a> CreateSticker<'a> { 25 | /// Creates a new builder with the given data. All of this builder's fields are required. 26 | pub fn new(name: impl Into, file: CreateAttachment) -> Self { 27 | Self { 28 | name: name.into(), 29 | tags: String::new(), 30 | description: String::new(), 31 | file, 32 | audit_log_reason: None, 33 | } 34 | } 35 | 36 | /// Set the name of the sticker, replacing the current value as set in [`Self::new`]. 37 | /// 38 | /// **Note**: Must be between 2 and 30 characters long. 39 | pub fn name(mut self, name: impl Into) -> Self { 40 | self.name = name.into(); 41 | self 42 | } 43 | 44 | /// Set the description of the sticker. 45 | /// 46 | /// **Note**: Must be empty or 2-100 characters. 47 | pub fn description(mut self, description: impl Into) -> Self { 48 | self.description = description.into(); 49 | self 50 | } 51 | 52 | /// The Discord name of a unicode emoji representing the sticker's expression. 53 | /// 54 | /// **Note**: Max 200 characters long. 55 | pub fn tags(mut self, tags: impl Into) -> Self { 56 | self.tags = tags.into(); 57 | self 58 | } 59 | 60 | /// Set the sticker file. Replaces the current value as set in [`Self::new`]. 61 | /// 62 | /// **Note**: Must be a PNG, APNG, or Lottie JSON file, max 500 KB. 63 | pub fn file(mut self, file: CreateAttachment) -> Self { 64 | self.file = file; 65 | self 66 | } 67 | 68 | /// Sets the request's audit log reason. 69 | pub fn audit_log_reason(mut self, reason: &'a str) -> Self { 70 | self.audit_log_reason = Some(reason); 71 | self 72 | } 73 | } 74 | 75 | #[cfg(feature = "http")] 76 | #[async_trait::async_trait] 77 | impl Builder for CreateSticker<'_> { 78 | type Context<'ctx> = GuildId; 79 | type Built = Sticker; 80 | 81 | /// Creates a new sticker in the guild with the data set, if any. 82 | /// 83 | /// **Note**: Requires the [Create Guild Expressions] permission. 84 | /// 85 | /// # Errors 86 | /// 87 | /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user 88 | /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. 89 | /// 90 | /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS 91 | async fn execute( 92 | self, 93 | cache_http: impl CacheHttp, 94 | ctx: Self::Context<'_>, 95 | ) -> Result { 96 | #[cfg(feature = "cache")] 97 | crate::utils::user_has_guild_perms( 98 | &cache_http, 99 | ctx, 100 | Permissions::CREATE_GUILD_EXPRESSIONS, 101 | )?; 102 | 103 | let map = [("name", self.name), ("tags", self.tags), ("description", self.description)]; 104 | cache_http.http().create_sticker(ctx, map, self.file, self.audit_log_reason).await 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/builder/create_webhook.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use super::Builder; 3 | use super::CreateAttachment; 4 | #[cfg(feature = "http")] 5 | use crate::http::CacheHttp; 6 | #[cfg(feature = "http")] 7 | use crate::internal::prelude::*; 8 | #[cfg(feature = "http")] 9 | use crate::model::prelude::*; 10 | 11 | /// [Discord docs](https://discord.com/developers/docs/resources/webhook#create-webhook) 12 | #[derive(Clone, Debug, Serialize)] 13 | #[must_use] 14 | pub struct CreateWebhook<'a> { 15 | name: String, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | avatar: Option, 18 | 19 | #[serde(skip)] 20 | audit_log_reason: Option<&'a str>, 21 | } 22 | 23 | impl<'a> CreateWebhook<'a> { 24 | /// Creates a new builder with the given webhook name, leaving all other fields empty. 25 | pub fn new(name: impl Into) -> Self { 26 | Self { 27 | name: name.into(), 28 | avatar: None, 29 | audit_log_reason: None, 30 | } 31 | } 32 | 33 | /// Set the webhook's name, replacing the current value as set in [`Self::new`]. 34 | /// 35 | /// This must be between 1-80 characters. 36 | pub fn name(mut self, name: impl Into) -> Self { 37 | self.name = name.into(); 38 | self 39 | } 40 | 41 | /// Set the webhook's default avatar. 42 | pub fn avatar(mut self, avatar: &CreateAttachment) -> Self { 43 | self.avatar = Some(avatar.to_base64()); 44 | self 45 | } 46 | 47 | /// Sets the request's audit log reason. 48 | pub fn audit_log_reason(mut self, reason: &'a str) -> Self { 49 | self.audit_log_reason = Some(reason); 50 | self 51 | } 52 | } 53 | 54 | #[cfg(feature = "http")] 55 | #[async_trait::async_trait] 56 | impl Builder for CreateWebhook<'_> { 57 | type Context<'ctx> = ChannelId; 58 | type Built = Webhook; 59 | 60 | /// Creates the webhook. 61 | /// 62 | /// # Errors 63 | /// 64 | /// If the provided name is less than 2 characters, returns [`ModelError::NameTooShort`]. If it 65 | /// is more than 100 characters, returns [`ModelError::NameTooLong`]. 66 | /// 67 | /// Returns a [`Error::Http`] if the current user lacks permission, or if invalid data is 68 | /// given. 69 | /// 70 | /// [`Text`]: ChannelType::Text 71 | /// [`News`]: ChannelType::News 72 | async fn execute( 73 | self, 74 | cache_http: impl CacheHttp, 75 | ctx: Self::Context<'_>, 76 | ) -> Result { 77 | if self.name.len() < 2 { 78 | return Err(Error::Model(ModelError::NameTooShort)); 79 | } else if self.name.len() > 100 { 80 | return Err(Error::Model(ModelError::NameTooLong)); 81 | } 82 | 83 | cache_http.http().create_webhook(ctx, &self, self.audit_log_reason).await 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/builder/edit_guild_widget.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use super::Builder; 3 | #[cfg(feature = "http")] 4 | use crate::http::CacheHttp; 5 | #[cfg(feature = "http")] 6 | use crate::internal::prelude::*; 7 | use crate::model::prelude::*; 8 | 9 | /// A builder to specify the fields to edit in a [`GuildWidget`]. 10 | /// 11 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#modify-guild-widget) 12 | #[derive(Clone, Debug, Default, Serialize)] 13 | #[must_use] 14 | pub struct EditGuildWidget<'a> { 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | enabled: Option, 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | channel_id: Option, 19 | 20 | #[serde(skip)] 21 | audit_log_reason: Option<&'a str>, 22 | } 23 | 24 | impl<'a> EditGuildWidget<'a> { 25 | /// Equivalent to [`Self::default`]. 26 | pub fn new() -> Self { 27 | Self::default() 28 | } 29 | 30 | /// Whether the widget is enabled or not. 31 | pub fn enabled(mut self, enabled: bool) -> Self { 32 | self.enabled = Some(enabled); 33 | self 34 | } 35 | 36 | /// The server description shown in the welcome screen. 37 | pub fn channel_id(mut self, id: impl Into) -> Self { 38 | self.channel_id = Some(id.into()); 39 | self 40 | } 41 | 42 | /// Sets the request's audit log reason. 43 | pub fn audit_log_reason(mut self, reason: &'a str) -> Self { 44 | self.audit_log_reason = Some(reason); 45 | self 46 | } 47 | } 48 | 49 | #[cfg(feature = "http")] 50 | #[async_trait::async_trait] 51 | impl Builder for EditGuildWidget<'_> { 52 | type Context<'ctx> = GuildId; 53 | type Built = GuildWidget; 54 | 55 | /// Edits the guild's widget. 56 | /// 57 | /// **Note**: Requires the [Manage Guild] permission. 58 | /// 59 | /// # Errors 60 | /// 61 | /// Returns [`Error::Http`] if the current user lacks permission. 62 | /// 63 | /// [Manage Guild]: Permissions::MANAGE_GUILD 64 | async fn execute( 65 | self, 66 | cache_http: impl CacheHttp, 67 | ctx: Self::Context<'_>, 68 | ) -> Result { 69 | cache_http.http().edit_guild_widget(ctx, &self, self.audit_log_reason).await 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/builder/edit_profile.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use super::Builder; 3 | use super::CreateAttachment; 4 | #[cfg(feature = "http")] 5 | use crate::http::CacheHttp; 6 | #[cfg(feature = "http")] 7 | use crate::internal::prelude::*; 8 | #[cfg(feature = "http")] 9 | use crate::model::user::CurrentUser; 10 | 11 | /// A builder to edit the current user's settings, to be used in conjunction with 12 | /// [`CurrentUser::edit`]. 13 | /// 14 | /// [Discord docs](https://discord.com/developers/docs/resources/user#modify-current-user) 15 | #[derive(Clone, Debug, Default, Serialize)] 16 | #[must_use] 17 | pub struct EditProfile { 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | username: Option, 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | avatar: Option>, 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | banner: Option>, 24 | } 25 | 26 | impl EditProfile { 27 | /// Equivalent to [`Self::default`]. 28 | pub fn new() -> Self { 29 | Self::default() 30 | } 31 | 32 | /// Set the avatar of the current user. 33 | /// 34 | /// # Examples 35 | /// 36 | /// ```rust,no_run 37 | /// # use serenity::builder::{EditProfile, CreateAttachment}; 38 | /// # use serenity::prelude::*; 39 | /// # use serenity::model::prelude::*; 40 | /// # use serenity::http::Http; 41 | /// # 42 | /// # #[cfg(feature = "http")] 43 | /// # async fn foo_(http: &Http, current_user: &mut CurrentUser) -> Result<(), SerenityError> { 44 | /// let avatar = CreateAttachment::path("./my_image.jpg").await.expect("Failed to read image."); 45 | /// current_user.edit(http, EditProfile::new().avatar(&avatar)).await?; 46 | /// # Ok(()) 47 | /// # } 48 | /// ``` 49 | pub fn avatar(mut self, avatar: &CreateAttachment) -> Self { 50 | self.avatar = Some(Some(avatar.to_base64())); 51 | self 52 | } 53 | 54 | /// Delete the current user's avatar, resetting it to the default logo. 55 | pub fn delete_avatar(mut self) -> Self { 56 | self.avatar = Some(None); 57 | self 58 | } 59 | 60 | /// Modifies the current user's username. 61 | /// 62 | /// When modifying the username, if another user has the same _new_ username and current 63 | /// discriminator, a new unique discriminator will be assigned. If there are no available 64 | /// discriminators with the requested username, an error will occur. 65 | pub fn username(mut self, username: impl Into) -> Self { 66 | self.username = Some(username.into()); 67 | self 68 | } 69 | 70 | /// Sets the banner of the current user. 71 | pub fn banner(mut self, banner: &CreateAttachment) -> Self { 72 | self.banner = Some(Some(banner.to_base64())); 73 | self 74 | } 75 | 76 | /// Deletes the current user's banner, resetting it to the default. 77 | pub fn delete_banner(mut self) -> Self { 78 | self.banner = Some(None); 79 | self 80 | } 81 | } 82 | 83 | #[cfg(feature = "http")] 84 | #[async_trait::async_trait] 85 | impl Builder for EditProfile { 86 | type Context<'ctx> = (); 87 | type Built = CurrentUser; 88 | 89 | /// Edit the current user's profile with the fields set. 90 | /// 91 | /// # Errors 92 | /// 93 | /// Returns an [`Error::Http`] if an invalid value is set. May also return an [`Error::Json`] 94 | /// if there is an error in deserializing the API response. 95 | async fn execute( 96 | self, 97 | cache_http: impl CacheHttp, 98 | _ctx: Self::Context<'_>, 99 | ) -> Result { 100 | cache_http.http().edit_profile(&self).await 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/builder/edit_stage_instance.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use super::Builder; 3 | #[cfg(feature = "http")] 4 | use crate::http::CacheHttp; 5 | #[cfg(feature = "http")] 6 | use crate::internal::prelude::*; 7 | use crate::model::prelude::*; 8 | 9 | /// Edits a [`StageInstance`]. 10 | /// 11 | /// [Discord docs](https://discord.com/developers/docs/resources/stage-instance#modify-stage-instance) 12 | #[derive(Clone, Debug, Default, Serialize)] 13 | #[must_use] 14 | pub struct EditStageInstance<'a> { 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | topic: Option, 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | privacy_level: Option, 19 | 20 | #[serde(skip)] 21 | audit_log_reason: Option<&'a str>, 22 | } 23 | 24 | impl<'a> EditStageInstance<'a> { 25 | /// Equivalent to [`Self::default`]. 26 | pub fn new() -> Self { 27 | Self::default() 28 | } 29 | 30 | /// Sets the topic of the stage channel instance. 31 | pub fn topic(mut self, topic: impl Into) -> Self { 32 | self.topic = Some(topic.into()); 33 | self 34 | } 35 | 36 | /// Sets the privacy level of the stage instance 37 | pub fn privacy_level(mut self, privacy_level: StageInstancePrivacyLevel) -> Self { 38 | self.privacy_level = Some(privacy_level); 39 | self 40 | } 41 | 42 | /// Sets the request's audit log reason. 43 | pub fn audit_log_reason(mut self, reason: &'a str) -> Self { 44 | self.audit_log_reason = Some(reason); 45 | self 46 | } 47 | } 48 | 49 | #[cfg(feature = "http")] 50 | #[async_trait::async_trait] 51 | impl Builder for EditStageInstance<'_> { 52 | type Context<'ctx> = ChannelId; 53 | type Built = StageInstance; 54 | 55 | /// Edits the stage instance 56 | /// 57 | /// # Errors 58 | /// 59 | /// Returns [`Error::Http`] if the channel is not a stage channel, or there is no stage 60 | /// instance currently. 61 | async fn execute( 62 | self, 63 | cache_http: impl CacheHttp, 64 | ctx: Self::Context<'_>, 65 | ) -> Result { 66 | cache_http.http().edit_stage_instance(ctx, &self, self.audit_log_reason).await 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/builder/edit_sticker.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use super::Builder; 3 | #[cfg(feature = "http")] 4 | use crate::http::CacheHttp; 5 | #[cfg(feature = "http")] 6 | use crate::internal::prelude::*; 7 | #[cfg(any(feature = "http", doc))] 8 | use crate::model::prelude::*; 9 | 10 | /// A builder to create or edit a [`Sticker`] for use via a number of model methods. 11 | /// 12 | /// These are: 13 | /// 14 | /// - [`Guild::edit_sticker`] 15 | /// - [`PartialGuild::edit_sticker`] 16 | /// - [`GuildId::edit_sticker`] 17 | /// - [`Sticker::edit`] 18 | /// 19 | /// [Discord docs](https://discord.com/developers/docs/resources/sticker#modify-guild-sticker) 20 | #[derive(Clone, Debug, Default, Serialize)] 21 | #[must_use] 22 | pub struct EditSticker<'a> { 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | name: Option, 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | description: Option, 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | tags: Option, 29 | 30 | #[serde(skip)] 31 | audit_log_reason: Option<&'a str>, 32 | } 33 | 34 | impl<'a> EditSticker<'a> { 35 | /// Equivalent to [`Self::default`]. 36 | pub fn new() -> Self { 37 | Self::default() 38 | } 39 | 40 | /// The name of the sticker to set. 41 | /// 42 | /// **Note**: Must be between 2 and 30 characters long. 43 | pub fn name(mut self, name: impl Into) -> Self { 44 | self.name = Some(name.into()); 45 | self 46 | } 47 | 48 | /// The description of the sticker. 49 | /// 50 | /// **Note**: If not empty, must be between 2 and 100 characters long. 51 | pub fn description(mut self, description: impl Into) -> Self { 52 | self.description = Some(description.into()); 53 | self 54 | } 55 | 56 | /// The Discord name of a unicode emoji representing the sticker's expression. 57 | /// 58 | /// **Note**: Must be between 2 and 200 characters long. 59 | pub fn tags(mut self, tags: impl Into) -> Self { 60 | self.tags = Some(tags.into()); 61 | self 62 | } 63 | 64 | /// Sets the request's audit log reason. 65 | pub fn audit_log_reason(mut self, reason: &'a str) -> Self { 66 | self.audit_log_reason = Some(reason); 67 | self 68 | } 69 | } 70 | 71 | #[cfg(feature = "http")] 72 | #[async_trait::async_trait] 73 | impl Builder for EditSticker<'_> { 74 | type Context<'ctx> = (GuildId, StickerId); 75 | type Built = Sticker; 76 | 77 | /// Edits the sticker. 78 | /// 79 | /// **Note**: If the sticker was created by the current user, requires either the [Create Guild 80 | /// Expressions] or the [Manage Guild Expressions] permission. Otherwise, the [Manage Guild 81 | /// Expressions] permission is required. 82 | /// 83 | /// # Errors 84 | /// 85 | /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. 86 | /// 87 | /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS 88 | /// [Manage Guild Expressions]: Permissions::MANAGE_GUILD_EXPRESSIONS 89 | async fn execute( 90 | self, 91 | cache_http: impl CacheHttp, 92 | ctx: Self::Context<'_>, 93 | ) -> Result { 94 | cache_http.http().edit_sticker(ctx.0, ctx.1, &self, self.audit_log_reason).await 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/builder/edit_voice_state.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use super::Builder; 3 | #[cfg(feature = "http")] 4 | use crate::http::CacheHttp; 5 | #[cfg(feature = "http")] 6 | use crate::internal::prelude::*; 7 | use crate::model::prelude::*; 8 | 9 | /// A builder which edits a user's voice state, to be used in conjunction with 10 | /// [`GuildChannel::edit_voice_state`]. 11 | /// 12 | /// Discord docs: 13 | /// - [current user](https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state) 14 | /// - [other users](https://discord.com/developers/docs/resources/guild#modify-user-voice-state) 15 | #[derive(Clone, Debug, Default, Serialize)] 16 | #[must_use] 17 | pub struct EditVoiceState { 18 | channel_id: Option, 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | suppress: Option, 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | request_to_speak_timestamp: Option>, 23 | } 24 | 25 | impl EditVoiceState { 26 | /// Equivalent to [`Self::default`]. 27 | pub fn new() -> Self { 28 | Self::default() 29 | } 30 | 31 | /// Whether to suppress the user. Setting this to false will invite a user to speak. 32 | /// 33 | /// **Note**: Requires the [Mute Members] permission to suppress another user or unsuppress the 34 | /// current user. This is not required if suppressing the current user. 35 | /// 36 | /// [Mute Members]: Permissions::MUTE_MEMBERS 37 | pub fn suppress(mut self, deafen: bool) -> Self { 38 | self.suppress = Some(deafen); 39 | self 40 | } 41 | 42 | /// Requests or clears a request to speak. Passing `true` is equivalent to passing the current 43 | /// time to [`Self::request_to_speak_timestamp`]. 44 | /// 45 | /// **Note**: Requires the [Request to Speak] permission. 46 | /// 47 | /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK 48 | pub fn request_to_speak(mut self, request: bool) -> Self { 49 | self.request_to_speak_timestamp = Some(request.then(Timestamp::now)); 50 | self 51 | } 52 | 53 | /// Sets the current bot user's request to speak timestamp. This can be any present or future 54 | /// time. 55 | /// 56 | /// **Note**: Requires the [Request to Speak] permission. 57 | /// 58 | /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK 59 | pub fn request_to_speak_timestamp(mut self, timestamp: impl Into) -> Self { 60 | self.request_to_speak_timestamp = Some(Some(timestamp.into())); 61 | self 62 | } 63 | } 64 | 65 | #[cfg(feature = "http")] 66 | #[async_trait::async_trait] 67 | impl Builder for EditVoiceState { 68 | type Context<'ctx> = (GuildId, ChannelId, Option); 69 | type Built = (); 70 | 71 | /// Edits the given user's voice state in a stage channel. Providing a [`UserId`] will edit 72 | /// that user's voice state, otherwise the current user's voice state will be edited. 73 | /// 74 | /// **Note**: Requires the [Request to Speak] permission. Also requires the [Mute Members] 75 | /// permission to suppress another user or unsuppress the current user. This is not required if 76 | /// suppressing the current user. 77 | /// 78 | /// # Errors 79 | /// 80 | /// Returns [`Error::Http`] if the user lacks permission, or if invalid data is given. 81 | /// 82 | /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK 83 | /// [Mute Members]: Permissions::MUTE_MEMBERS 84 | async fn execute( 85 | mut self, 86 | cache_http: impl CacheHttp, 87 | ctx: Self::Context<'_>, 88 | ) -> Result { 89 | let (guild_id, channel_id, user_id) = ctx; 90 | 91 | self.channel_id = Some(channel_id); 92 | if let Some(user_id) = user_id { 93 | cache_http.http().edit_voice_state(guild_id, user_id, &self).await 94 | } else { 95 | cache_http.http().edit_voice_state_me(guild_id, &self).await 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/builder/edit_webhook.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use super::Builder; 3 | use super::CreateAttachment; 4 | #[cfg(feature = "http")] 5 | use crate::http::CacheHttp; 6 | #[cfg(feature = "http")] 7 | use crate::internal::prelude::*; 8 | use crate::model::prelude::*; 9 | 10 | /// [Discord docs](https://discord.com/developers/docs/resources/webhook#modify-webhook) 11 | #[derive(Debug, Default, Clone, Serialize)] 12 | #[must_use] 13 | pub struct EditWebhook<'a> { 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | name: Option, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | avatar: Option>, 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | channel_id: Option, 20 | 21 | #[serde(skip)] 22 | audit_log_reason: Option<&'a str>, 23 | } 24 | 25 | impl<'a> EditWebhook<'a> { 26 | /// Equivalent to [`Self::default`]. 27 | pub fn new() -> Self { 28 | Self::default() 29 | } 30 | 31 | /// Set the webhook's name. 32 | /// 33 | /// This must be between 1-80 characters. 34 | pub fn name(mut self, name: impl Into) -> Self { 35 | self.name = Some(name.into()); 36 | self 37 | } 38 | 39 | /// Set the channel to move the webhook to. 40 | pub fn channel_id(mut self, channel_id: impl Into) -> Self { 41 | self.channel_id = Some(channel_id.into()); 42 | self 43 | } 44 | 45 | /// Set the webhook's default avatar. 46 | pub fn avatar(mut self, avatar: &CreateAttachment) -> Self { 47 | self.avatar = Some(Some(avatar.to_base64())); 48 | self 49 | } 50 | 51 | /// Delete the webhook's avatar, resetting it to the default logo. 52 | pub fn delete_avatar(mut self) -> Self { 53 | self.avatar = Some(None); 54 | self 55 | } 56 | 57 | /// Sets the request's audit log reason. 58 | pub fn audit_log_reason(mut self, reason: &'a str) -> Self { 59 | self.audit_log_reason = Some(reason); 60 | self 61 | } 62 | } 63 | 64 | #[cfg(feature = "http")] 65 | #[async_trait::async_trait] 66 | impl Builder for EditWebhook<'_> { 67 | type Context<'ctx> = (WebhookId, Option<&'ctx str>); 68 | type Built = Webhook; 69 | 70 | /// Edits the webhook corresponding to the provided [`WebhookId`] and token, and returns the 71 | /// resulting new [`Webhook`]. 72 | /// 73 | /// # Errors 74 | /// 75 | /// Returns [`Error::Http`] if the content is malformed, or if the token is invalid. 76 | /// 77 | /// Returns [`Error::Json`] if there is an error in deserialising Discord's response. 78 | async fn execute( 79 | self, 80 | cache_http: impl CacheHttp, 81 | ctx: Self::Context<'_>, 82 | ) -> Result { 83 | match ctx.1 { 84 | Some(token) => { 85 | cache_http 86 | .http() 87 | .edit_webhook_with_token(ctx.0, token, &self, self.audit_log_reason) 88 | .await 89 | }, 90 | None => cache_http.http().edit_webhook(ctx.0, &self, self.audit_log_reason).await, 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/builder/get_entitlements.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use crate::http::CacheHttp; 3 | use crate::internal::prelude::Result; 4 | use crate::model::id::{EntitlementId, GuildId, SkuId, UserId}; 5 | use crate::model::monetization::Entitlement; 6 | 7 | /// Builds a request to fetch active and ended [`Entitlement`]s. 8 | /// 9 | /// This is a helper for [`Http::get_entitlements`] used via [`Entitlement::list`]. 10 | /// 11 | /// [`Http::get_entitlements`]: crate::http::Http::get_entitlements 12 | #[derive(Clone, Debug, Default)] 13 | #[must_use] 14 | pub struct GetEntitlements { 15 | user_id: Option, 16 | sku_ids: Option>, 17 | before: Option, 18 | after: Option, 19 | limit: Option, 20 | guild_id: Option, 21 | exclude_ended: Option, 22 | } 23 | 24 | impl GetEntitlements { 25 | /// Filters the returned entitlements by the given [`UserId`]. 26 | pub fn user_id(mut self, user_id: UserId) -> Self { 27 | self.user_id = Some(user_id); 28 | self 29 | } 30 | 31 | /// Filters the returned entitlements by the given [`SkuId`]s. 32 | pub fn sku_ids(mut self, sku_ids: Vec) -> Self { 33 | self.sku_ids = Some(sku_ids); 34 | self 35 | } 36 | 37 | /// Filters the returned entitlements to before the given [`EntitlementId`]. 38 | pub fn before(mut self, before: EntitlementId) -> Self { 39 | self.before = Some(before); 40 | self 41 | } 42 | 43 | /// Filters the returned entitlements to after the given [`EntitlementId`]. 44 | pub fn after(mut self, after: EntitlementId) -> Self { 45 | self.after = Some(after); 46 | self 47 | } 48 | 49 | /// Limits the number of entitlements that may be returned. 50 | /// 51 | /// This is limited to `0..=100`. 52 | pub fn limit(mut self, limit: u8) -> Self { 53 | self.limit = Some(limit); 54 | self 55 | } 56 | 57 | /// Filters the returned entitlements by the given [`GuildId`]. 58 | pub fn guild_id(mut self, guild_id: GuildId) -> Self { 59 | self.guild_id = Some(guild_id); 60 | self 61 | } 62 | 63 | /// Filters the returned entitlements to only active entitlements, if `true`. 64 | pub fn exclude_ended(mut self, exclude_ended: bool) -> Self { 65 | self.exclude_ended = Some(exclude_ended); 66 | self 67 | } 68 | } 69 | 70 | #[cfg(feature = "http")] 71 | #[async_trait::async_trait] 72 | impl super::Builder for GetEntitlements { 73 | type Context<'ctx> = (); 74 | type Built = Vec; 75 | 76 | async fn execute( 77 | self, 78 | cache_http: impl CacheHttp, 79 | _: Self::Context<'_>, 80 | ) -> Result { 81 | let http = cache_http.http(); 82 | http.get_entitlements( 83 | self.user_id, 84 | self.sku_ids, 85 | self.before, 86 | self.after, 87 | self.limit, 88 | self.guild_id, 89 | self.exclude_ended, 90 | ) 91 | .await 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/cache/cache_update.rs: -------------------------------------------------------------------------------- 1 | use super::Cache; 2 | 3 | /// Trait used for updating the cache with a type. 4 | /// 5 | /// This may be implemented on a type and used to update the cache via [`Cache::update`]. 6 | /// 7 | /// **Info**: You may not access the fields of the cache, as they are public for the crate only. 8 | /// 9 | /// # Examples 10 | /// 11 | /// Creating a custom struct implementation to update the cache with: 12 | /// 13 | /// ```rust,ignore 14 | /// use std::collections::hash_map::Entry; 15 | /// 16 | /// use serenity::json::json; 17 | /// use serenity::cache::{Cache, CacheUpdate}; 18 | /// use serenity::model::id::UserId; 19 | /// use serenity::model::user::User; 20 | /// 21 | /// // For example, an update to the user's record in the database was 22 | /// // published to a pubsub channel. 23 | /// struct DatabaseUserUpdate { 24 | /// user_avatar: Option, 25 | /// user_discriminator: u16, 26 | /// user_id: UserId, 27 | /// user_is_bot: bool, 28 | /// user_name: String, 29 | /// } 30 | /// 31 | /// #[serenity::async_trait] 32 | /// impl CacheUpdate for DatabaseUserUpdate { 33 | /// // A copy of the old user's data, if it existed in the cache. 34 | /// type Output = User; 35 | /// 36 | /// async fn update(&mut self, cache: &Cache) -> Option { 37 | /// // If an entry for the user already exists, update its fields. 38 | /// match cache.users.entry(self.user_id) { 39 | /// Entry::Occupied(entry) => { 40 | /// let user = entry.get(); 41 | /// let old_user = user.clone(); 42 | /// 43 | /// user.bot = self.user_is_bot; 44 | /// user.discriminator = self.user_discriminator; 45 | /// user.id = self.user_id; 46 | /// 47 | /// if user.avatar != self.user_avatar { 48 | /// user.avatar = self.user_avatar.clone(); 49 | /// } 50 | /// 51 | /// if user.name != self.user_name { 52 | /// user.name = self.user_name.clone(); 53 | /// } 54 | /// 55 | /// // Return the old copy for the user's sake. 56 | /// Some(old_user) 57 | /// }, 58 | /// Entry::Vacant(entry) => { 59 | /// // We can convert a [`json::Value`] to a User for test 60 | /// // purposes. 61 | /// let user = from_value::(json!({ 62 | /// "id": self.user_id, 63 | /// "avatar": self.user_avatar.clone(), 64 | /// "bot": self.user_is_bot, 65 | /// "discriminator": self.user_discriminator, 66 | /// "username": self.user_name.clone(), 67 | /// })).expect("Error making user"); 68 | /// 69 | /// entry.insert(user); 70 | /// 71 | /// // There was no old copy, so return None. 72 | /// None 73 | /// }, 74 | /// } 75 | /// } 76 | /// } 77 | /// 78 | /// # async fn run() { 79 | /// // Create an instance of the cache. 80 | /// let mut cache = Cache::new(); 81 | /// 82 | /// // This is a sample pubsub message that you might receive from your 83 | /// // database. 84 | /// let mut update_message = DatabaseUserUpdate { 85 | /// user_avatar: None, 86 | /// user_discriminator: 6082, 87 | /// user_id: UserId::new(379740138303127564), 88 | /// user_is_bot: true, 89 | /// user_name: "TofuBot".to_owned(), 90 | /// }; 91 | /// 92 | /// // Update the cache with the message. 93 | /// cache.update(&mut update_message).await; 94 | /// # } 95 | /// ``` 96 | pub trait CacheUpdate { 97 | /// The return type of an update. 98 | /// 99 | /// If there is nothing to return, specify this type as an unit (`()`). 100 | type Output; 101 | 102 | /// Updates the cache with the implementation. 103 | fn update(&mut self, _: &Cache) -> Option; 104 | } 105 | -------------------------------------------------------------------------------- /src/cache/settings.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | /// Settings for the cache. 4 | /// 5 | /// # Examples 6 | /// 7 | /// Create new settings, specifying the maximum number of messages: 8 | /// 9 | /// ```rust 10 | /// use serenity::cache::Settings as CacheSettings; 11 | /// 12 | /// let mut settings = CacheSettings::default(); 13 | /// settings.max_messages = 10; 14 | /// ``` 15 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 16 | #[derive(Clone, Debug)] 17 | #[non_exhaustive] 18 | pub struct Settings { 19 | /// The maximum number of messages to store in a channel's message cache. 20 | /// 21 | /// Defaults to 0. 22 | pub max_messages: usize, 23 | /// How long temporarily-cached data should be stored before being thrown out. 24 | /// 25 | /// Defaults to one hour. 26 | pub time_to_live: Duration, 27 | /// Whether to cache guild data received from gateway. 28 | /// 29 | /// Defaults to true. 30 | pub cache_guilds: bool, 31 | /// Whether to cache channel data received from gateway. 32 | /// 33 | /// Defaults to true. 34 | pub cache_channels: bool, 35 | /// Whether to cache user data received from gateway. 36 | /// 37 | /// Defaults to true. 38 | pub cache_users: bool, 39 | } 40 | 41 | impl Default for Settings { 42 | fn default() -> Self { 43 | Self { 44 | max_messages: 0, 45 | time_to_live: Duration::from_secs(60 * 60), 46 | cache_guilds: true, 47 | cache_channels: true, 48 | cache_users: true, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/client/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::fmt; 3 | 4 | /// An error returned from the [`Client`]. 5 | /// 6 | /// This is always wrapped within the library's generic [`Error::Client`] variant. 7 | /// 8 | /// [`Client`]: super::Client 9 | /// [`Error::Client`]: crate::Error::Client 10 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 11 | #[non_exhaustive] 12 | pub enum Error { 13 | /// When a shard has completely failed to reboot after resume and/or reconnect attempts. 14 | ShardBootFailure, 15 | /// When all shards that the client is responsible for have shutdown with an error. 16 | Shutdown, 17 | } 18 | 19 | impl fmt::Display for Error { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | match self { 22 | Self::ShardBootFailure => f.write_str("Failed to (re-)boot a shard"), 23 | Self::Shutdown => f.write_str("The clients shards shutdown"), 24 | } 25 | } 26 | } 27 | 28 | impl StdError for Error {} 29 | -------------------------------------------------------------------------------- /src/framework/standard/parse/map.rs: -------------------------------------------------------------------------------- 1 | use crate::framework::standard::*; 2 | 3 | #[derive(Debug)] 4 | pub enum Map { 5 | WithPrefixes(GroupMap), 6 | Prefixless(GroupMap, CommandMap), 7 | } 8 | 9 | pub trait ParseMap { 10 | type Storage; 11 | 12 | fn get(&self, n: &str) -> Option; 13 | fn min_length(&self) -> usize; 14 | fn max_length(&self) -> usize; 15 | fn is_empty(&self) -> bool; 16 | } 17 | 18 | #[derive(Debug, Default)] 19 | pub struct CommandMap { 20 | cmds: HashMap)>, 21 | min_length: usize, 22 | max_length: usize, 23 | } 24 | 25 | impl CommandMap { 26 | pub fn new(cmds: &[&'static Command], conf: &Configuration) -> Self { 27 | let mut map = Self::default(); 28 | 29 | for cmd in cmds { 30 | let sub_map = Arc::new(Self::new(cmd.options.sub_commands, conf)); 31 | 32 | for name in cmd.options.names { 33 | let len = name.chars().count(); 34 | map.min_length = std::cmp::min(len, map.min_length); 35 | map.max_length = std::cmp::max(len, map.max_length); 36 | 37 | let name = 38 | if conf.case_insensitive { name.to_lowercase() } else { (*name).to_string() }; 39 | 40 | map.cmds.insert(name, (*cmd, Arc::clone(&sub_map))); 41 | } 42 | } 43 | 44 | map 45 | } 46 | } 47 | 48 | impl ParseMap for CommandMap { 49 | type Storage = (&'static Command, Arc); 50 | 51 | #[inline] 52 | fn min_length(&self) -> usize { 53 | self.min_length 54 | } 55 | 56 | #[inline] 57 | fn max_length(&self) -> usize { 58 | self.max_length 59 | } 60 | 61 | #[inline] 62 | fn get(&self, name: &str) -> Option { 63 | self.cmds.get(name).cloned() 64 | } 65 | 66 | #[inline] 67 | fn is_empty(&self) -> bool { 68 | self.cmds.is_empty() 69 | } 70 | } 71 | 72 | #[derive(Debug, Default)] 73 | pub struct GroupMap { 74 | groups: HashMap<&'static str, (&'static CommandGroup, Arc, Arc)>, 75 | min_length: usize, 76 | max_length: usize, 77 | } 78 | 79 | impl GroupMap { 80 | pub fn new(groups: &[&'static CommandGroup], conf: &Configuration) -> Self { 81 | let mut map = Self::default(); 82 | 83 | for group in groups { 84 | let subgroups_map = Arc::new(Self::new(group.options.sub_groups, conf)); 85 | let commands_map = Arc::new(CommandMap::new(group.options.commands, conf)); 86 | 87 | for prefix in group.options.prefixes { 88 | let len = prefix.chars().count(); 89 | map.min_length = std::cmp::min(len, map.min_length); 90 | map.max_length = std::cmp::max(len, map.max_length); 91 | 92 | map.groups.insert( 93 | *prefix, 94 | (*group, Arc::clone(&subgroups_map), Arc::clone(&commands_map)), 95 | ); 96 | } 97 | } 98 | 99 | map 100 | } 101 | } 102 | 103 | impl ParseMap for GroupMap { 104 | type Storage = (&'static CommandGroup, Arc, Arc); 105 | 106 | #[inline] 107 | fn min_length(&self) -> usize { 108 | self.min_length 109 | } 110 | 111 | #[inline] 112 | fn max_length(&self) -> usize { 113 | self.max_length 114 | } 115 | 116 | #[inline] 117 | fn get(&self, name: &str) -> Option { 118 | self.groups.get(&name).cloned() 119 | } 120 | 121 | #[inline] 122 | fn is_empty(&self) -> bool { 123 | self.groups.is_empty() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/framework/standard/structures/check.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | use futures::future::BoxFuture; 5 | 6 | use crate::client::Context; 7 | use crate::framework::standard::{Args, CommandOptions}; 8 | use crate::model::channel::Message; 9 | 10 | /// This type describes why a check has failed. 11 | /// 12 | /// **Note**: The bot-developer is supposed to process this `enum` as the framework is not. It 13 | /// solely serves as a way to inform a user about why a check has failed and for the developer to 14 | /// log given failure (e.g. bugs or statistics) occurring in [`Check`]s. 15 | #[derive(Clone, Debug)] 16 | #[non_exhaustive] 17 | pub enum Reason { 18 | /// No information on the failure. 19 | Unknown, 20 | /// Information dedicated to the user. 21 | User(String), 22 | /// Information purely for logging purposes. 23 | Log(String), 24 | /// Information for the user but also for logging purposes. 25 | UserAndLog { user: String, log: String }, 26 | } 27 | 28 | impl Error for Reason {} 29 | 30 | pub type CheckFunction = for<'fut> fn( 31 | &'fut Context, 32 | &'fut Message, 33 | &'fut mut Args, 34 | &'fut CommandOptions, 35 | ) -> BoxFuture<'fut, Result<(), Reason>>; 36 | 37 | /// A check can be part of a command or group and will be executed to determine whether a user is 38 | /// permitted to use related item. 39 | /// 40 | /// Additionally, a check may hold additional settings. 41 | pub struct Check { 42 | /// Name listed in help-system. 43 | pub name: &'static str, 44 | /// Function that will be executed. 45 | pub function: CheckFunction, 46 | /// Whether a check should be evaluated in the help-system. `false` will ignore check and won't 47 | /// fail execution. 48 | pub check_in_help: bool, 49 | /// Whether a check shall be listed in the help-system. `false` won't affect whether the check 50 | /// will be evaluated help, solely [`Self::check_in_help`] sets this. 51 | pub display_in_help: bool, 52 | } 53 | 54 | impl fmt::Debug for Check { 55 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 56 | f.debug_struct("Check") 57 | .field("name", &self.name) 58 | .field("function", &"") 59 | .field("check_in_help", &self.check_in_help) 60 | .field("display_in_help", &self.display_in_help) 61 | .finish() 62 | } 63 | } 64 | 65 | impl fmt::Display for Reason { 66 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 67 | match self { 68 | Self::Unknown => f.write_str("Unknown"), 69 | Self::User(reason) => write!(f, "User {reason}"), 70 | Self::Log(reason) => write!(f, "Log {reason}"), 71 | Self::UserAndLog { 72 | user, 73 | log, 74 | } => { 75 | write!(f, "UserAndLog {{user: {user}, log: {log}}}") 76 | }, 77 | } 78 | } 79 | } 80 | 81 | impl PartialEq for Check { 82 | fn eq(&self, other: &Self) -> bool { 83 | self.name == other.name 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/gateway/bridge/event.rs: -------------------------------------------------------------------------------- 1 | //! A collection of events created by the client, not a part of the Discord API itself. 2 | 3 | use crate::gateway::ConnectionStage; 4 | use crate::model::id::ShardId; 5 | 6 | /// An event denoting that a shard's connection stage was changed. 7 | /// 8 | /// # Examples 9 | /// 10 | /// This might happen when a shard changes from [`ConnectionStage::Identifying`] to 11 | /// [`ConnectionStage::Connected`]. 12 | #[derive(Clone, Debug)] 13 | pub struct ShardStageUpdateEvent { 14 | /// The new connection stage. 15 | pub new: ConnectionStage, 16 | /// The old connection stage. 17 | pub old: ConnectionStage, 18 | /// The ID of the shard that had its connection stage change. 19 | pub shard_id: ShardId, 20 | } 21 | -------------------------------------------------------------------------------- /src/gateway/bridge/shard_runner_message.rs: -------------------------------------------------------------------------------- 1 | use tokio_tungstenite::tungstenite::Message; 2 | 3 | use super::ShardId; 4 | use crate::gateway::{ActivityData, ChunkGuildFilter}; 5 | use crate::model::id::GuildId; 6 | use crate::model::user::OnlineStatus; 7 | 8 | /// A message to send from a shard over a WebSocket. 9 | #[derive(Debug)] 10 | pub enum ShardRunnerMessage { 11 | /// Indicator that a shard should be restarted. 12 | Restart(ShardId), 13 | /// Indicator that a shard should be fully shutdown without bringing it 14 | /// back up. 15 | Shutdown(ShardId, u16), 16 | /// Indicates that the client is to send a member chunk message. 17 | ChunkGuild { 18 | /// The IDs of the [`Guild`] to chunk. 19 | /// 20 | /// [`Guild`]: crate::model::guild::Guild 21 | guild_id: GuildId, 22 | /// The maximum number of members to receive [`GuildMembersChunkEvent`]s for. 23 | /// 24 | /// [`GuildMembersChunkEvent`]: crate::model::event::GuildMembersChunkEvent 25 | limit: Option, 26 | /// Used to specify if we want the presences of the matched members. 27 | /// 28 | /// Requires [`crate::model::gateway::GatewayIntents::GUILD_PRESENCES`]. 29 | presences: bool, 30 | /// A filter to apply to the returned members. 31 | filter: ChunkGuildFilter, 32 | /// Optional nonce to identify [`GuildMembersChunkEvent`] responses. 33 | /// 34 | /// [`GuildMembersChunkEvent`]: crate::model::event::GuildMembersChunkEvent 35 | nonce: Option, 36 | }, 37 | /// Indicates that the client is to close with the given status code and reason. 38 | /// 39 | /// You should rarely - if _ever_ - need this, but the option is available. Prefer to use the 40 | /// [`ShardManager`] to shutdown WebSocket clients if you are intending to send a 1000 close 41 | /// code. 42 | /// 43 | /// [`ShardManager`]: super::ShardManager 44 | Close(u16, Option), 45 | /// Indicates that the client is to send a custom WebSocket message. 46 | Message(Message), 47 | /// Indicates that the client is to update the shard's presence's activity. 48 | SetActivity(Option), 49 | /// Indicates that the client is to update the shard's presence in its entirety. 50 | SetPresence(Option, OnlineStatus), 51 | /// Indicates that the client is to update the shard's presence's status. 52 | SetStatus(OnlineStatus), 53 | } 54 | -------------------------------------------------------------------------------- /src/gateway/bridge/voice.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use futures::channel::mpsc::UnboundedSender as Sender; 3 | 4 | use crate::gateway::ShardRunnerMessage; 5 | use crate::model::id::{GuildId, UserId}; 6 | use crate::model::voice::VoiceState; 7 | 8 | /// Interface for any compatible voice plugin. 9 | /// 10 | /// This interface covers several serenity-specific hooks, as well as packet handlers for 11 | /// voice-specific gateway messages. 12 | #[async_trait] 13 | pub trait VoiceGatewayManager: Send + Sync { 14 | /// Performs initial setup at the start of a connection to Discord. 15 | /// 16 | /// This will only occur once, and provides the bot's ID and shard count. 17 | async fn initialise(&self, shard_count: u32, user_id: UserId); 18 | 19 | /// Handler fired in response to a [`Ready`] event. 20 | /// 21 | /// This provides the voice plugin with a channel to send gateway messages to Discord, once per 22 | /// active shard. 23 | /// 24 | /// [`Ready`]: crate::model::event::Event 25 | async fn register_shard(&self, shard_id: u32, sender: Sender); 26 | 27 | /// Handler fired in response to a disconnect, reconnection, or rebalance. 28 | /// 29 | /// This event invalidates the last sender associated with `shard_id`. Unless the bot is fully 30 | /// disconnecting, this is often followed by a call to [`Self::register_shard`]. Users may wish 31 | /// to buffer manually any gateway messages sent between these calls. 32 | async fn deregister_shard(&self, shard_id: u32); 33 | 34 | /// Handler for VOICE_SERVER_UPDATE messages. 35 | /// 36 | /// These contain the endpoint and token needed to form a voice connection session. 37 | async fn server_update(&self, guild_id: GuildId, endpoint: &Option, token: &str); 38 | 39 | /// Handler for VOICE_STATE_UPDATE messages. 40 | /// 41 | /// These contain the session ID needed to form a voice connection session. 42 | async fn state_update(&self, guild_id: GuildId, voice_state: &VoiceState); 43 | } 44 | -------------------------------------------------------------------------------- /src/gateway/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::fmt; 3 | 4 | use tokio_tungstenite::tungstenite::protocol::CloseFrame; 5 | 6 | /// An error that occurred while attempting to deal with the gateway. 7 | /// 8 | /// Note that - from a user standpoint - there should be no situation in which you manually handle 9 | /// these. 10 | #[derive(Clone, Debug)] 11 | #[non_exhaustive] 12 | pub enum Error { 13 | /// There was an error building a URL. 14 | BuildingUrl, 15 | /// The connection closed, potentially uncleanly. 16 | Closed(Option>), 17 | /// Expected a Hello during a handshake 18 | ExpectedHello, 19 | /// When there was an error sending a heartbeat. 20 | HeartbeatFailed, 21 | /// When invalid authentication (a bad token) was sent in the IDENTIFY. 22 | InvalidAuthentication, 23 | /// Expected a Ready or an InvalidateSession 24 | InvalidHandshake, 25 | /// When invalid sharding data was sent in the IDENTIFY. 26 | /// 27 | /// # Examples 28 | /// 29 | /// Sending a shard ID of 5 when sharding with 3 total is considered invalid. 30 | InvalidShardData, 31 | /// When no authentication was sent in the IDENTIFY. 32 | NoAuthentication, 33 | /// When a session Id was expected (for resuming), but was not present. 34 | NoSessionId, 35 | /// When a shard would have too many guilds assigned to it. 36 | /// 37 | /// # Examples 38 | /// 39 | /// When sharding 5500 guilds on 2 shards, at least one of the shards will have over the 40 | /// maximum number of allowed guilds per shard. 41 | /// 42 | /// This limit is currently 2500 guilds per shard. 43 | OverloadedShard, 44 | /// Failed to reconnect after a number of attempts. 45 | ReconnectFailure, 46 | /// When undocumented gateway intents are provided. 47 | InvalidGatewayIntents, 48 | /// When disallowed gateway intents are provided. 49 | /// 50 | /// If an connection has been established but privileged gateway intents were provided without 51 | /// enabling them prior. 52 | DisallowedGatewayIntents, 53 | } 54 | 55 | impl fmt::Display for Error { 56 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 57 | match self { 58 | Self::BuildingUrl => f.write_str("Error building url"), 59 | Self::Closed(_) => f.write_str("Connection closed"), 60 | Self::ExpectedHello => f.write_str("Expected a Hello"), 61 | Self::HeartbeatFailed => f.write_str("Failed sending a heartbeat"), 62 | Self::InvalidAuthentication => f.write_str("Sent invalid authentication"), 63 | Self::InvalidHandshake => f.write_str("Expected a valid Handshake"), 64 | Self::InvalidShardData => f.write_str("Sent invalid shard data"), 65 | Self::NoAuthentication => f.write_str("Sent no authentication"), 66 | Self::NoSessionId => f.write_str("No Session Id present when required"), 67 | Self::OverloadedShard => f.write_str("Shard has too many guilds"), 68 | Self::ReconnectFailure => f.write_str("Failed to Reconnect"), 69 | Self::InvalidGatewayIntents => f.write_str("Invalid gateway intents were provided"), 70 | Self::DisallowedGatewayIntents => { 71 | f.write_str("Disallowed gateway intents were provided") 72 | }, 73 | } 74 | } 75 | } 76 | 77 | impl StdError for Error {} 78 | -------------------------------------------------------------------------------- /src/http/multipart.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use reqwest::multipart::{Form, Part}; 4 | 5 | use crate::builder::CreateAttachment; 6 | use crate::internal::prelude::*; 7 | 8 | impl CreateAttachment { 9 | fn into_part(self) -> Result { 10 | let mut part = Part::bytes(self.data); 11 | part = guess_mime_str(part, &self.filename)?; 12 | part = part.file_name(self.filename); 13 | Ok(part) 14 | } 15 | } 16 | 17 | #[derive(Clone, Debug)] 18 | pub enum MultipartUpload { 19 | /// A file sent with the form data as an individual upload. For example, a sticker. 20 | File(CreateAttachment), 21 | /// Files sent with the form as message attachments. 22 | Attachments(Vec), 23 | } 24 | 25 | /// Holder for multipart body. Contains upload data, multipart fields, and payload_json for 26 | /// creating requests with attachments. 27 | #[derive(Clone, Debug)] 28 | pub struct Multipart { 29 | pub upload: MultipartUpload, 30 | /// Multipart text fields that are sent with the form data as individual fields. If a certain 31 | /// endpoint does not support passing JSON body via `payload_json`, this must be used instead. 32 | pub fields: Vec<(Cow<'static, str>, Cow<'static, str>)>, 33 | /// JSON body that will set as the form value as `payload_json`. 34 | pub payload_json: Option, 35 | } 36 | 37 | impl Multipart { 38 | pub(crate) fn build_form(self) -> Result
{ 39 | let mut multipart = Form::new(); 40 | 41 | match self.upload { 42 | MultipartUpload::File(upload_file) => { 43 | multipart = multipart.part("file", upload_file.into_part()?); 44 | }, 45 | MultipartUpload::Attachments(attachment_files) => { 46 | for file in attachment_files { 47 | multipart = multipart.part(format!("files[{}]", file.id), file.into_part()?); 48 | } 49 | }, 50 | } 51 | 52 | for (name, value) in self.fields { 53 | multipart = multipart.text(name, value); 54 | } 55 | 56 | if let Some(payload_json) = self.payload_json { 57 | multipart = multipart.text("payload_json", payload_json); 58 | } 59 | 60 | Ok(multipart) 61 | } 62 | } 63 | 64 | fn guess_mime_str(part: Part, filename: &str) -> Result { 65 | // This is required for certain endpoints like create sticker, otherwise the Discord API will 66 | // respond with a 500 Internal Server Error. The mime type chosen is the same as what reqwest 67 | // does internally when using Part::file(), but it is not done for any of the other methods we 68 | // use. 69 | // https://datatracker.ietf.org/doc/html/rfc7578#section-4.4 70 | let mime_type = mime_guess::from_path(filename).first_or_octet_stream(); 71 | part.mime_str(mime_type.essence_str()).map_err(Into::into) 72 | } 73 | -------------------------------------------------------------------------------- /src/http/typing.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use tokio::sync::oneshot::error::TryRecvError; 4 | use tokio::sync::oneshot::{self, Sender}; 5 | use tokio::time::{sleep, Duration}; 6 | 7 | use crate::http::Http; 8 | use crate::internal::prelude::*; 9 | use crate::internal::tokio::spawn_named; 10 | use crate::model::id::ChannelId; 11 | 12 | /// A struct to start typing in a [`Channel`] for an indefinite period of time. 13 | /// 14 | /// It indicates that the current user is currently typing in the channel. 15 | /// 16 | /// Typing is started by using the [`Typing::start`] method and stopped by using the 17 | /// [`Typing::stop`] method. Note that on some clients, typing may persist for a few seconds after 18 | /// [`Typing::stop`] is called. Typing is also stopped when the struct is dropped. 19 | /// 20 | /// If a message is sent while typing is triggered, the user will stop typing for a brief period of 21 | /// time and then resume again until either [`Typing::stop`] is called or the struct is dropped. 22 | /// 23 | /// This should rarely be used for bots, although it is a good indicator that a long-running 24 | /// command is still being processed. 25 | /// 26 | /// ## Examples 27 | /// 28 | /// ```rust,no_run 29 | /// # use serenity::{http::{Http, Typing}, Result, model::prelude::*}; 30 | /// # use std::sync::Arc; 31 | /// # 32 | /// # fn long_process() {} 33 | /// # fn main() { 34 | /// # let http: Http = unimplemented!(); 35 | /// let channel_id = ChannelId::new(7); 36 | /// // Initiate typing (assuming `http` is bound) 37 | /// let typing = Typing::start(Arc::new(http), channel_id); 38 | /// 39 | /// // Run some long-running process 40 | /// long_process(); 41 | /// 42 | /// // Stop typing 43 | /// typing.stop(); 44 | /// # } 45 | /// ``` 46 | /// 47 | /// [`Channel`]: crate::model::channel::Channel 48 | #[derive(Debug)] 49 | pub struct Typing(Sender<()>); 50 | 51 | impl Typing { 52 | /// Starts typing in the specified [`Channel`] for an indefinite period of time. 53 | /// 54 | /// Returns [`Typing`]. To stop typing, you must call the [`Typing::stop`] method on the 55 | /// returned [`Typing`] object or wait for it to be dropped. Note that on some clients, typing 56 | /// may persist for a few seconds after stopped. 57 | /// 58 | /// # Errors 59 | /// 60 | /// Returns an [`Error::Http`] if there is an error. 61 | /// 62 | /// [`Channel`]: crate::model::channel::Channel 63 | pub fn start(http: Arc, channel_id: ChannelId) -> Self { 64 | let (sx, mut rx) = oneshot::channel(); 65 | 66 | spawn_named::<_, Result<_>>("typing::start", async move { 67 | loop { 68 | match rx.try_recv() { 69 | Ok(()) | Err(TryRecvError::Closed) => break, 70 | _ => (), 71 | } 72 | 73 | http.broadcast_typing(channel_id).await?; 74 | 75 | // It is unclear for how long typing persists after this method is called. 76 | // It is generally assumed to be 7 or 10 seconds, so we use 7 to be safe. 77 | sleep(Duration::from_secs(7)).await; 78 | } 79 | 80 | Ok(()) 81 | }); 82 | 83 | Self(sx) 84 | } 85 | 86 | /// Stops typing in [`Channel`]. 87 | /// 88 | /// This should be used to stop typing after it is started using [`Typing::start`]. Typing may 89 | /// persist for a few seconds on some clients after this is called. Returns false if typing has 90 | /// already stopped. 91 | /// 92 | /// [`Channel`]: crate::model::channel::Channel 93 | #[allow(clippy::must_use_candidate)] 94 | pub fn stop(self) -> bool { 95 | self.0.send(()).is_ok() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/internal/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod macros; 3 | 4 | pub mod prelude; 5 | 6 | pub mod tokio; 7 | -------------------------------------------------------------------------------- /src/internal/prelude.rs: -------------------------------------------------------------------------------- 1 | //! These prelude re-exports are a set of exports that are commonly used from within the library. 2 | //! 3 | //! These are not publicly re-exported to the end user, and must stay as a private module. 4 | 5 | pub use std::result::Result as StdResult; 6 | 7 | pub use crate::error::{Error, Result}; 8 | pub use crate::json::{JsonMap, Value}; 9 | -------------------------------------------------------------------------------- /src/internal/tokio.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | use std::future::Future; 3 | 4 | #[cfg(feature = "http")] 5 | pub fn spawn_named(_name: &str, future: F) -> tokio::task::JoinHandle 6 | where 7 | F: Future + Send + 'static, 8 | T: Send + 'static, 9 | { 10 | #[cfg(all(tokio_unstable, feature = "tokio_task_builder"))] 11 | let handle = tokio::task::Builder::new() 12 | .name(&*format!("serenity::{}", _name)) 13 | .spawn(future) 14 | .expect("called outside tokio runtime"); 15 | #[cfg(not(all(tokio_unstable, feature = "tokio_task_builder")))] 16 | let handle = tokio::spawn(future); 17 | handle 18 | } 19 | -------------------------------------------------------------------------------- /src/model/application/ping_interaction.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::model::id::{ApplicationId, InteractionId}; 4 | 5 | /// A ping interaction, which can only be received through an endpoint url. 6 | /// 7 | /// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-structure). 8 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 9 | #[derive(Clone, Debug, Deserialize, Serialize)] 10 | #[non_exhaustive] 11 | pub struct PingInteraction { 12 | /// Id of the interaction. 13 | pub id: InteractionId, 14 | /// Id of the application this interaction is for. 15 | pub application_id: ApplicationId, 16 | /// A continuation token for responding to the interaction. 17 | pub token: String, 18 | /// Always `1`. 19 | pub version: u8, 20 | } 21 | -------------------------------------------------------------------------------- /src/model/channel/partial_channel.rs: -------------------------------------------------------------------------------- 1 | use crate::model::channel::{ChannelType, ThreadMetadata}; 2 | use crate::model::id::{ChannelId, WebhookId}; 3 | use crate::model::Permissions; 4 | 5 | /// A container for any partial channel. 6 | /// 7 | /// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object), 8 | /// [subset specification](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure). 9 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 10 | #[derive(Clone, Debug, Deserialize, Serialize)] 11 | #[non_exhaustive] 12 | pub struct PartialChannel { 13 | /// The channel Id. 14 | pub id: ChannelId, 15 | /// The channel name. 16 | pub name: Option, 17 | /// The channel type. 18 | #[serde(rename = "type")] 19 | pub kind: ChannelType, 20 | /// The channel permissions. 21 | pub permissions: Option, 22 | /// The thread metadata. 23 | /// 24 | /// **Note**: This is only available on thread channels. 25 | pub thread_metadata: Option, 26 | /// The Id of the parent category for a channel, or of the parent text channel for a thread. 27 | /// 28 | /// **Note**: This is only available on thread channels. 29 | pub parent_id: Option, 30 | } 31 | 32 | /// A container for the IDs returned by following a news channel. 33 | /// 34 | /// [Discord docs](https://discord.com/developers/docs/resources/channel#followed-channel-object). 35 | #[derive(Clone, Debug, Deserialize, Serialize)] 36 | #[non_exhaustive] 37 | pub struct FollowedChannel { 38 | /// The source news channel 39 | pub channel_id: ChannelId, 40 | /// The created webhook ID in the target channel 41 | pub webhook_id: WebhookId, 42 | } 43 | -------------------------------------------------------------------------------- /src/model/connection.rs: -------------------------------------------------------------------------------- 1 | //! Models for user connections. 2 | 3 | use super::prelude::*; 4 | 5 | /// Information about a connection between the current user and a third party service. 6 | /// 7 | /// [Discord docs](https://discord.com/developers/docs/resources/user#connection-object-connection-structure). 8 | #[derive(Clone, Debug, Deserialize, Serialize)] 9 | #[non_exhaustive] 10 | pub struct Connection { 11 | /// The ID of the account on the other side of this connection. 12 | pub id: String, 13 | /// The username of the account on the other side of this connection. 14 | pub name: String, 15 | /// The service that this connection represents (e.g. twitch, youtube) 16 | /// 17 | /// [Discord docs](https://discord.com/developers/docs/resources/user#connection-object-services). 18 | #[serde(rename = "type")] 19 | pub kind: String, 20 | /// Whether this connection has been revoked and is no longer valid. 21 | #[serde(default)] 22 | pub revoked: bool, 23 | /// A list of partial guild [`Integration`]s that use this connection. 24 | #[serde(default)] 25 | pub integrations: Vec, 26 | /// Whether this connection has been verified and the user has proven they own the account. 27 | pub verified: bool, 28 | /// Whether friend sync is enabled for this connection. 29 | pub friend_sync: bool, 30 | /// Whether activities related to this connection will be shown in presence updates. 31 | pub show_activity: bool, 32 | /// Whether this connection has a corresponding third party OAuth2 token. 33 | pub two_way_link: bool, 34 | /// The visibility of this connection. 35 | pub visibility: ConnectionVisibility, 36 | } 37 | 38 | enum_number! { 39 | /// The visibility of a user connection on a user's profile. 40 | /// 41 | /// [Discord docs](https://discord.com/developers/docs/resources/user#connection-object-visibility-types). 42 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] 43 | #[serde(from = "u8", into = "u8")] 44 | #[non_exhaustive] 45 | pub enum ConnectionVisibility { 46 | /// Invisible to everyone except the user themselves 47 | None = 0, 48 | /// Visible to everyone 49 | Everyone = 1, 50 | _ => Unknown(u8), 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/model/guild/audit_log/utils.rs: -------------------------------------------------------------------------------- 1 | /// Used with `#[serde(with = "users")]` 2 | pub mod users { 3 | use std::collections::HashMap; 4 | 5 | use serde::Deserializer; 6 | 7 | use crate::model::id::UserId; 8 | use crate::model::user::User; 9 | use crate::model::utils::SequenceToMapVisitor; 10 | 11 | pub fn deserialize<'de, D: Deserializer<'de>>( 12 | deserializer: D, 13 | ) -> Result, D::Error> { 14 | deserializer.deserialize_seq(SequenceToMapVisitor::new(|u: &User| u.id)) 15 | } 16 | 17 | pub use crate::model::utils::serialize_map_values as serialize; 18 | } 19 | 20 | /// Used with `#[serde(with = "webhooks")]` 21 | pub mod webhooks { 22 | use std::collections::HashMap; 23 | 24 | use serde::Deserializer; 25 | 26 | use crate::model::id::WebhookId; 27 | use crate::model::utils::SequenceToMapVisitor; 28 | use crate::model::webhook::Webhook; 29 | 30 | pub fn deserialize<'de, D: Deserializer<'de>>( 31 | deserializer: D, 32 | ) -> Result, D::Error> { 33 | deserializer.deserialize_seq(SequenceToMapVisitor::new(|h: &Webhook| h.id)) 34 | } 35 | 36 | pub use crate::model::utils::serialize_map_values as serialize; 37 | } 38 | 39 | /// Deserializes an optional string containing a valid integer as `Option`. 40 | /// 41 | /// Used with `#[serde(with = "optional_string")]`. 42 | pub mod optional_string { 43 | use std::fmt; 44 | 45 | use serde::de::{Deserializer, Error, Visitor}; 46 | use serde::ser::Serializer; 47 | 48 | pub fn deserialize<'de, D: Deserializer<'de>>( 49 | deserializer: D, 50 | ) -> Result, D::Error> { 51 | deserializer.deserialize_option(OptionalStringVisitor) 52 | } 53 | 54 | #[allow(clippy::ref_option)] 55 | pub fn serialize(value: &Option, serializer: S) -> Result { 56 | match value { 57 | Some(value) => serializer.serialize_some(&value.to_string()), 58 | None => serializer.serialize_none(), 59 | } 60 | } 61 | 62 | struct OptionalStringVisitor; 63 | 64 | impl<'de> Visitor<'de> for OptionalStringVisitor { 65 | type Value = Option; 66 | 67 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | formatter.write_str("an optional integer or a string with a valid number inside") 69 | } 70 | 71 | fn visit_some>( 72 | self, 73 | deserializer: D, 74 | ) -> Result { 75 | deserializer.deserialize_any(OptionalStringVisitor) 76 | } 77 | 78 | fn visit_none(self) -> Result { 79 | Ok(None) 80 | } 81 | 82 | /// Called by the `simd_json` crate 83 | fn visit_unit(self) -> Result { 84 | Ok(None) 85 | } 86 | 87 | fn visit_u64(self, val: u64) -> Result, E> { 88 | Ok(Some(val)) 89 | } 90 | 91 | fn visit_str(self, string: &str) -> Result, E> { 92 | string.parse().map(Some).map_err(Error::custom) 93 | } 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::optional_string; 100 | use crate::json::{assert_json, json}; 101 | 102 | #[test] 103 | fn optional_string_module() { 104 | #[derive(Debug, PartialEq, Deserialize, Serialize)] 105 | struct T { 106 | #[serde(with = "optional_string")] 107 | opt: Option, 108 | } 109 | 110 | let value = T { 111 | opt: Some(12345), 112 | }; 113 | 114 | assert_json(&value, json!({"opt": "12345"})); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/model/guild/guild_preview.rs: -------------------------------------------------------------------------------- 1 | use crate::model::guild::Emoji; 2 | use crate::model::id::GuildId; 3 | use crate::model::misc::ImageHash; 4 | use crate::model::sticker::Sticker; 5 | 6 | /// Preview [`Guild`] information. 7 | /// 8 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-preview-object). 9 | /// 10 | /// [`Guild`]: super::Guild 11 | #[derive(Clone, Debug, Deserialize, Serialize)] 12 | #[non_exhaustive] 13 | pub struct GuildPreview { 14 | /// The guild Id. 15 | pub id: GuildId, 16 | /// The guild name. 17 | pub name: String, 18 | /// The guild icon hash if it has one. 19 | pub icon: Option, 20 | /// The guild splash hash if it has one. 21 | pub splash: Option, 22 | /// The guild discovery splash hash it it has one. 23 | pub discovery_splash: Option, 24 | /// The custom guild emojis. 25 | pub emojis: Vec, 26 | /// The guild features. See [`Guild::features`] 27 | /// 28 | /// [`Guild::features`]: super::Guild::features 29 | pub features: Vec, 30 | /// Approximate number of members in this guild. 31 | pub approximate_member_count: u64, 32 | /// Approximate number of online members in this guild. 33 | pub approximate_presence_count: u64, 34 | /// The description for the guild, if the guild has the `DISCOVERABLE` feature. 35 | pub description: Option, 36 | /// Custom guild stickers. 37 | pub stickers: Vec, 38 | } 39 | -------------------------------------------------------------------------------- /src/model/guild/integration.rs: -------------------------------------------------------------------------------- 1 | use crate::model::prelude::*; 2 | 3 | /// Various information about integrations. 4 | /// 5 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#integration-object), 6 | /// [extra fields 1](https://discord.com/developers/docs/topics/gateway-events#integration-create), 7 | /// [extra fields 2](https://discord.com/developers/docs/topics/gateway-events#integration-update), 8 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 9 | #[derive(Clone, Debug, Deserialize, Serialize)] 10 | #[non_exhaustive] 11 | pub struct Integration { 12 | pub id: IntegrationId, 13 | pub name: String, 14 | #[serde(rename = "type")] 15 | pub kind: String, 16 | pub enabled: bool, 17 | pub syncing: Option, 18 | pub role_id: Option, 19 | pub enable_emoticons: Option, 20 | #[serde(rename = "expire_behavior")] 21 | pub expire_behaviour: Option, 22 | pub expire_grace_period: Option, 23 | pub user: Option, 24 | pub account: IntegrationAccount, 25 | pub synced_at: Option, 26 | pub subscriber_count: Option, 27 | pub revoked: Option, 28 | pub application: Option, 29 | pub scopes: Option>, 30 | /// Only present in [`IntegrationCreateEvent`] and [`IntegrationUpdateEvent`]. 31 | pub guild_id: Option, 32 | } 33 | 34 | enum_number! { 35 | /// The behavior once the integration expires. 36 | /// 37 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#integration-object-integration-expire-behaviors). 38 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] 39 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 40 | #[serde(from = "u8", into = "u8")] 41 | #[non_exhaustive] 42 | pub enum IntegrationExpireBehaviour { 43 | RemoveRole = 0, 44 | Kick = 1, 45 | _ => Unknown(u8), 46 | } 47 | } 48 | 49 | impl From for IntegrationId { 50 | /// Gets the Id of integration. 51 | fn from(integration: Integration) -> IntegrationId { 52 | integration.id 53 | } 54 | } 55 | 56 | /// Integration account object. 57 | /// 58 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#integration-account-object). 59 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 60 | #[derive(Clone, Debug, Deserialize, Serialize)] 61 | #[non_exhaustive] 62 | pub struct IntegrationAccount { 63 | pub id: String, 64 | pub name: String, 65 | } 66 | 67 | /// Integration application object. 68 | /// 69 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#integration-application-object). 70 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 71 | #[derive(Clone, Debug, Deserialize, Serialize)] 72 | #[non_exhaustive] 73 | pub struct IntegrationApplication { 74 | pub id: ApplicationId, 75 | pub name: String, 76 | pub icon: Option, 77 | pub description: String, 78 | pub bot: Option, 79 | } 80 | -------------------------------------------------------------------------------- /src/model/guild/premium_tier.rs: -------------------------------------------------------------------------------- 1 | enum_number! { 2 | /// The guild's premium tier, depends on the amount of users boosting the guild currently 3 | /// 4 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-object-premium-tier). 5 | #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] 6 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 7 | #[serde(from = "u8", into = "u8")] 8 | #[non_exhaustive] 9 | pub enum PremiumTier { 10 | /// Guild has not unlocked any Server Boost perks 11 | #[default] 12 | Tier0 = 0, 13 | /// Guild has unlocked Server Boost level 1 perks 14 | Tier1 = 1, 15 | /// Guild has unlocked Server Boost level 2 perks 16 | Tier2 = 2, 17 | /// Guild has unlocked Server Boost level 3 perks 18 | Tier3 = 3, 19 | _ => Unknown(u8), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/model/guild/system_channel.rs: -------------------------------------------------------------------------------- 1 | bitflags! { 2 | /// Describes a system channel flags. 3 | /// 4 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags). 5 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 6 | #[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq)] 7 | pub struct SystemChannelFlags: u64 { 8 | /// Suppress member join notifications. 9 | const SUPPRESS_JOIN_NOTIFICATIONS = 1 << 0; 10 | /// Suppress server boost notifications. 11 | const SUPPRESS_PREMIUM_SUBSCRIPTIONS = 1 << 1; 12 | /// Suppress server setup tips. 13 | const SUPPRESS_GUILD_REMINDER_NOTIFICATIONS = 1 << 2; 14 | /// Hide member join sticker reply buttons. 15 | const SUPPRESS_JOIN_NOTIFICATION_REPLIES = 1 << 3; 16 | /// Suppress role subscription purchase and renewal notifications. 17 | const SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATIONS = 1 << 4; 18 | /// Hide role subscription sticker reply buttons. 19 | const SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATION_REPLIES = 1 << 5; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/model/guild/welcome_screen.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 2 | 3 | use crate::model::id::{ChannelId, EmojiId}; 4 | 5 | /// Information relating to a guild's welcome screen. 6 | /// 7 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#welcome-screen-object). 8 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 9 | #[derive(Clone, Debug, Deserialize, Serialize)] 10 | #[non_exhaustive] 11 | pub struct GuildWelcomeScreen { 12 | /// The server description shown in the welcome screen. 13 | pub description: Option, 14 | /// The channels shown in the welcome screen. 15 | /// 16 | /// **Note**: There can only be only up to 5 channels. 17 | pub welcome_channels: Vec, 18 | } 19 | 20 | /// A channel shown in the [`GuildWelcomeScreen`]. 21 | /// 22 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#welcome-screen-object-welcome-screen-channel-structure). 23 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 24 | #[derive(Clone, Debug)] 25 | #[non_exhaustive] 26 | pub struct GuildWelcomeChannel { 27 | /// The channel Id. 28 | pub channel_id: ChannelId, 29 | /// The description shown for the channel. 30 | pub description: String, 31 | /// The emoji shown, if there is one. 32 | pub emoji: Option, 33 | } 34 | 35 | // Manual impl needed to deserialize emoji_id and emoji_name into a single GuildWelcomeChannelEmoji 36 | impl<'de> Deserialize<'de> for GuildWelcomeChannel { 37 | fn deserialize>(deserializer: D) -> Result { 38 | #[derive(Deserialize)] 39 | struct Helper { 40 | channel_id: ChannelId, 41 | description: String, 42 | emoji_id: Option, 43 | emoji_name: Option, 44 | } 45 | let Helper { 46 | channel_id, 47 | description, 48 | emoji_id, 49 | emoji_name, 50 | } = Helper::deserialize(deserializer)?; 51 | 52 | let emoji = match (emoji_id, emoji_name) { 53 | (Some(id), Some(name)) => Some(GuildWelcomeChannelEmoji::Custom { 54 | id, 55 | name, 56 | }), 57 | (None, Some(name)) => Some(GuildWelcomeChannelEmoji::Unicode(name)), 58 | _ => None, 59 | }; 60 | 61 | Ok(Self { 62 | channel_id, 63 | description, 64 | emoji, 65 | }) 66 | } 67 | } 68 | 69 | impl Serialize for GuildWelcomeChannel { 70 | fn serialize(&self, serializer: S) -> Result { 71 | use serde::ser::SerializeStruct; 72 | 73 | let mut s = serializer.serialize_struct("GuildWelcomeChannel", 4)?; 74 | s.serialize_field("channel_id", &self.channel_id)?; 75 | s.serialize_field("description", &self.description)?; 76 | let (emoji_id, emoji_name) = match &self.emoji { 77 | Some(GuildWelcomeChannelEmoji::Custom { 78 | id, 79 | name, 80 | }) => (Some(id), Some(name)), 81 | Some(GuildWelcomeChannelEmoji::Unicode(name)) => (None, Some(name)), 82 | None => (None, None), 83 | }; 84 | s.serialize_field("emoji_id", &emoji_id)?; 85 | s.serialize_field("emoji_name", &emoji_name)?; 86 | s.end() 87 | } 88 | } 89 | 90 | /// A [`GuildWelcomeScreen`] emoji. 91 | /// 92 | /// [Discord docs](https://discord.com/developers/docs/resources/guild#welcome-screen-object-welcome-screen-channel-structure). 93 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 94 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 95 | #[non_exhaustive] 96 | pub enum GuildWelcomeChannelEmoji { 97 | /// A custom emoji. 98 | Custom { id: EmojiId, name: String }, 99 | /// A unicode emoji. 100 | Unicode(String), 101 | } 102 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | //! Mappings of objects received from the API, with optional helper methods for ease of use. 2 | //! 3 | //! Models can optionally have additional helper methods compiled, by enabling the `model` feature. 4 | //! 5 | //! Normally you can import models through the sub-modules: 6 | //! 7 | //! ```rust,no_run 8 | //! use serenity::model::channel::{ChannelType, GuildChannel, Message}; 9 | //! use serenity::model::id::{ChannelId, GuildId}; 10 | //! use serenity::model::user::User; 11 | //! ``` 12 | //! 13 | //! This can get a bit tedious - especially with a large number of imports - so this can be 14 | //! simplified by simply glob importing everything from the prelude: 15 | //! 16 | //! ```rust,no_run 17 | //! use serenity::model::prelude::*; 18 | //! ``` 19 | 20 | #[macro_use] 21 | mod utils; 22 | 23 | pub mod application; 24 | pub mod channel; 25 | pub mod colour; 26 | pub mod connection; 27 | pub mod error; 28 | pub mod event; 29 | pub mod gateway; 30 | pub mod guild; 31 | pub mod id; 32 | pub mod invite; 33 | pub mod mention; 34 | pub mod misc; 35 | pub mod monetization; 36 | pub mod permissions; 37 | pub mod sticker; 38 | pub mod timestamp; 39 | pub mod user; 40 | pub mod voice; 41 | pub mod webhook; 42 | 43 | #[cfg(feature = "voice_model")] 44 | pub use serenity_voice_model as voice_gateway; 45 | 46 | pub use self::colour::{Color, Colour}; 47 | pub use self::error::Error as ModelError; 48 | pub use self::permissions::Permissions; 49 | pub use self::timestamp::Timestamp; 50 | 51 | /// The model prelude re-exports all types in the model sub-modules. 52 | /// 53 | /// This allows for quick and easy access to all of the model types. 54 | /// 55 | /// # Examples 56 | /// 57 | /// Import all model types into scope: 58 | /// 59 | /// ```rust,no_run 60 | /// use serenity::model::prelude::*; 61 | /// ``` 62 | pub mod prelude { 63 | pub(crate) use std::collections::HashMap; 64 | 65 | pub(crate) use serde::de::Visitor; 66 | pub(crate) use serde::{Deserialize, Deserializer}; 67 | 68 | pub use super::guild::automod::EventType as AutomodEventType; 69 | #[doc(hidden)] 70 | pub use super::guild::automod::{ 71 | Action, 72 | ActionExecution, 73 | ActionType, 74 | KeywordPresetType, 75 | Rule, 76 | Trigger, 77 | TriggerMetadata, 78 | TriggerType, 79 | }; 80 | #[doc(hidden)] 81 | pub use super::{ 82 | application::*, 83 | channel::*, 84 | colour::*, 85 | connection::*, 86 | event::*, 87 | gateway::*, 88 | guild::audit_log::*, 89 | guild::*, 90 | id::*, 91 | invite::*, 92 | mention::*, 93 | misc::*, 94 | monetization::*, 95 | permissions::*, 96 | sticker::*, 97 | user::*, 98 | voice::*, 99 | webhook::*, 100 | ModelError, 101 | Timestamp, 102 | }; 103 | pub(crate) use crate::internal::prelude::*; 104 | } 105 | -------------------------------------------------------------------------------- /src/model/voice.rs: -------------------------------------------------------------------------------- 1 | //! Representations of voice information. 2 | 3 | use serde::de::{Deserialize, Deserializer}; 4 | use serde::Serialize; 5 | 6 | use crate::model::guild::Member; 7 | use crate::model::id::{ChannelId, GuildId, UserId}; 8 | use crate::model::Timestamp; 9 | 10 | /// Information about an available voice region. 11 | /// 12 | /// [Discord docs](https://discord.com/developers/docs/resources/voice#voice-region-object). 13 | #[derive(Clone, Debug, Deserialize, Serialize)] 14 | #[non_exhaustive] 15 | pub struct VoiceRegion { 16 | /// Whether it is a custom voice region, which is used for events. 17 | pub custom: bool, 18 | /// Whether it is a deprecated voice region, which you should avoid using. 19 | pub deprecated: bool, 20 | /// The internal Id of the voice region. 21 | pub id: String, 22 | /// A recognizable name of the location of the voice region. 23 | pub name: String, 24 | /// Whether the voice region is optimal for use by the current user. 25 | pub optimal: bool, 26 | } 27 | 28 | /// A user's state within a voice channel. 29 | /// 30 | /// [Discord docs](https://discord.com/developers/docs/resources/voice#voice-state-object). 31 | #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] 32 | #[derive(Clone, Debug, Deserialize, Serialize)] 33 | #[serde(remote = "Self")] 34 | #[non_exhaustive] 35 | pub struct VoiceState { 36 | pub channel_id: Option, 37 | pub deaf: bool, 38 | pub guild_id: Option, 39 | pub member: Option, 40 | pub mute: bool, 41 | pub self_deaf: bool, 42 | pub self_mute: bool, 43 | pub self_stream: Option, 44 | pub self_video: bool, 45 | pub session_id: String, 46 | pub suppress: bool, 47 | pub user_id: UserId, 48 | /// When unsuppressed, non-bot users will have this set to the current time. Bot users will be 49 | /// set to [`None`]. When suppressed, the user will have their 50 | /// [`Self::request_to_speak_timestamp`] removed. 51 | pub request_to_speak_timestamp: Option, 52 | } 53 | 54 | // Manual impl needed to insert guild_id into Member 55 | impl<'de> Deserialize<'de> for VoiceState { 56 | fn deserialize>(deserializer: D) -> Result { 57 | // calls #[serde(remote)]-generated inherent method 58 | let mut state = Self::deserialize(deserializer)?; 59 | if let (Some(guild_id), Some(member)) = (state.guild_id, state.member.as_mut()) { 60 | member.guild_id = guild_id; 61 | } 62 | Ok(state) 63 | } 64 | } 65 | 66 | impl Serialize for VoiceState { 67 | fn serialize(&self, serializer: S) -> Result { 68 | // calls #[serde(remote)]-generated inherent method 69 | Self::serialize(self, serializer) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! A set of exports which can be helpful to use. 2 | //! 3 | //! Note that the `SerenityError` re-export is equivalent to [`serenity::Error`], although is 4 | //! re-exported as a separate name to remove likely ambiguity with other crate error enums. 5 | //! 6 | //! # Examples 7 | //! 8 | //! Import all of the exports: 9 | //! 10 | //! ```rust 11 | //! use serenity::prelude::*; 12 | //! ``` 13 | //! 14 | //! [`serenity::Error`]: crate::Error 15 | 16 | pub use tokio::sync::{Mutex, RwLock}; 17 | #[cfg(feature = "client")] 18 | pub use typemap_rev::{TypeMap, TypeMapKey}; 19 | 20 | #[cfg(feature = "client")] 21 | pub use crate::client::Context; 22 | #[cfg(all(feature = "client", feature = "gateway"))] 23 | pub use crate::client::{Client, ClientError, EventHandler, RawEventHandler}; 24 | pub use crate::error::Error as SerenityError; 25 | #[cfg(feature = "gateway")] 26 | pub use crate::gateway::GatewayError; 27 | #[cfg(feature = "http")] 28 | pub use crate::http::CacheHttp; 29 | #[cfg(feature = "http")] 30 | pub use crate::http::HttpError; 31 | pub use crate::model::mention::Mentionable; 32 | #[cfg(feature = "model")] 33 | pub use crate::model::{gateway::GatewayIntents, ModelError}; 34 | -------------------------------------------------------------------------------- /src/utils/argument_convert/_template.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use super::ArgumentConvert; 3 | use crate::{model::prelude::*, prelude::*}; 4 | 5 | /// Error that can be returned from [`PLACEHOLDER::convert`]. 6 | #[non_exhaustive] 7 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 8 | pub enum PLACEHOLDERParseError { 9 | } 10 | 11 | impl std::error::Error for PLACEHOLDERParseError {} 12 | 13 | impl fmt::Display for PLACEHOLDERParseError { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | match self { 16 | } 17 | } 18 | } 19 | 20 | /// Look up a [`PLACEHOLDER`] by a string case-insensitively. 21 | /// 22 | /// Requires the cache feature to be enabled. 23 | /// 24 | /// The lookup strategy is as follows (in order): 25 | /// 1. Lookup by PLACEHOLDER 26 | /// 2. [Lookup by PLACEHOLDER](`crate::utils::parse_PLACEHOLDER`). 27 | #[async_trait::async_trait] 28 | impl ArgumentConvert for PLACEHOLDER { 29 | type Err = PLACEHOLDERParseError; 30 | 31 | async fn convert( 32 | ctx: impl CacheHttp, 33 | guild_id: Option, 34 | _channel_id: Option, 35 | s: &str, 36 | ) -> Result { 37 | let lookup_by_PLACEHOLDER = || PLACEHOLDER; 38 | 39 | lookup_by_PLACEHOLDER() 40 | .or_else(lookup_by_PLACEHOLDER) 41 | .or_else(lookup_by_PLACEHOLDER) 42 | .cloned() 43 | .ok_or(PLACEHOLDERParseError::NotFoundOrMalformed) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/argument_convert/emoji.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use super::ArgumentConvert; 4 | use crate::model::prelude::*; 5 | use crate::prelude::*; 6 | 7 | /// Error that can be returned from [`Emoji::convert`]. 8 | #[non_exhaustive] 9 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 10 | pub enum EmojiParseError { 11 | /// Parser was invoked outside a guild. 12 | OutsideGuild, 13 | /// Guild was not in cache, or guild HTTP request failed. 14 | FailedToRetrieveGuild, 15 | /// The provided emoji string failed to parse, or the parsed result cannot be found in the 16 | /// guild roles. 17 | NotFoundOrMalformed, 18 | } 19 | 20 | impl std::error::Error for EmojiParseError {} 21 | 22 | impl fmt::Display for EmojiParseError { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | match self { 25 | Self::OutsideGuild => f.write_str("Tried to find emoji outside a guild"), 26 | Self::FailedToRetrieveGuild => f.write_str("Could not retrieve guild data"), 27 | Self::NotFoundOrMalformed => f.write_str("Emoji not found or unknown format"), 28 | } 29 | } 30 | } 31 | 32 | /// Look up a [`Emoji`]. 33 | /// 34 | /// Requires the cache feature to be enabled. 35 | /// 36 | /// The lookup strategy is as follows (in order): 37 | /// 1. Lookup by ID. 38 | /// 2. [Lookup by extracting ID from the emoji](`crate::utils::parse_emoji`). 39 | /// 3. Lookup by name. 40 | #[async_trait::async_trait] 41 | impl ArgumentConvert for Emoji { 42 | type Err = EmojiParseError; 43 | 44 | async fn convert( 45 | ctx: impl CacheHttp, 46 | guild_id: Option, 47 | _channel_id: Option, 48 | s: &str, 49 | ) -> Result { 50 | // Get Guild or PartialGuild 51 | let guild_id = guild_id.ok_or(EmojiParseError::OutsideGuild)?; 52 | let guild = guild_id 53 | .to_partial_guild(&ctx) 54 | .await 55 | .map_err(|_| EmojiParseError::FailedToRetrieveGuild)?; 56 | 57 | let direct_id = s.parse().ok(); 58 | let id_from_mention = crate::utils::parse_emoji(s).map(|e| e.id); 59 | 60 | if let Some(emoji_id) = direct_id.or(id_from_mention) { 61 | if let Some(emoji) = guild.emojis.get(&emoji_id).cloned() { 62 | return Ok(emoji); 63 | } 64 | } 65 | 66 | if let Some(emoji) = 67 | guild.emojis.values().find(|emoji| emoji.name.eq_ignore_ascii_case(s)).cloned() 68 | { 69 | return Ok(emoji); 70 | } 71 | 72 | Err(EmojiParseError::NotFoundOrMalformed) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/argument_convert/guild.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use super::ArgumentConvert; 4 | use crate::model::prelude::*; 5 | use crate::prelude::*; 6 | 7 | /// Error that can be returned from [`Guild::convert`]. 8 | #[non_exhaustive] 9 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 10 | pub enum GuildParseError { 11 | /// The provided guild string failed to parse, or the parsed result cannot be found in the 12 | /// cache. 13 | NotFoundOrMalformed, 14 | /// No cache, so no guild search could be done. 15 | NoCache, 16 | } 17 | 18 | impl std::error::Error for GuildParseError {} 19 | 20 | impl fmt::Display for GuildParseError { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | match self { 23 | Self::NotFoundOrMalformed => f.write_str("Guild not found or unknown format"), 24 | Self::NoCache => f.write_str("No cached list of guilds was provided"), 25 | } 26 | } 27 | } 28 | 29 | /// Look up a Guild, either by ID or by a string case-insensitively. 30 | /// 31 | /// Requires the cache feature to be enabled. 32 | #[async_trait::async_trait] 33 | impl ArgumentConvert for Guild { 34 | type Err = GuildParseError; 35 | 36 | async fn convert( 37 | ctx: impl CacheHttp, 38 | _guild_id: Option, 39 | _channel_id: Option, 40 | s: &str, 41 | ) -> Result { 42 | let guilds = &ctx.cache().ok_or(GuildParseError::NoCache)?.guilds; 43 | 44 | let lookup_by_id = || guilds.get(&s.parse().ok()?).map(|g| g.clone()); 45 | 46 | let lookup_by_name = || { 47 | guilds.iter().find_map(|m| { 48 | let guild = m.value(); 49 | guild.name.eq_ignore_ascii_case(s).then(|| guild.clone()) 50 | }) 51 | }; 52 | 53 | lookup_by_id().or_else(lookup_by_name).ok_or(GuildParseError::NotFoundOrMalformed) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/argument_convert/member.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use super::ArgumentConvert; 4 | use crate::model::prelude::*; 5 | use crate::prelude::*; 6 | 7 | /// Error that can be returned from [`Member::convert`]. 8 | #[non_exhaustive] 9 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 10 | pub enum MemberParseError { 11 | /// Parser was invoked outside a guild. 12 | OutsideGuild, 13 | /// The guild in which the parser was invoked is not in cache. 14 | GuildNotInCache, 15 | /// The provided member string failed to parse, or the parsed result cannot be found in the 16 | /// guild cache data. 17 | NotFoundOrMalformed, 18 | } 19 | 20 | impl std::error::Error for MemberParseError {} 21 | 22 | impl fmt::Display for MemberParseError { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | match self { 25 | Self::OutsideGuild => f.write_str("Tried to find member outside a guild"), 26 | Self::GuildNotInCache => f.write_str("Guild is not in cache"), 27 | Self::NotFoundOrMalformed => f.write_str("Member not found or unknown format"), 28 | } 29 | } 30 | } 31 | 32 | /// Look up a guild member by a string case-insensitively. 33 | /// 34 | /// Requires the cache feature to be enabled. 35 | /// 36 | /// The lookup strategy is as follows (in order): 37 | /// 1. Lookup by ID. 38 | /// 2. [Lookup by mention](`crate::utils::parse_username`). 39 | /// 3. [Lookup by name#discrim](`crate::utils::parse_user_tag`). 40 | /// 4. Lookup by name 41 | /// 5. Lookup by nickname 42 | #[async_trait::async_trait] 43 | impl ArgumentConvert for Member { 44 | type Err = MemberParseError; 45 | 46 | async fn convert( 47 | ctx: impl CacheHttp, 48 | guild_id: Option, 49 | _channel_id: Option, 50 | s: &str, 51 | ) -> Result { 52 | let guild_id = guild_id.ok_or(MemberParseError::OutsideGuild)?; 53 | 54 | // DON'T use guild.members: it's only populated when guild presences intent is enabled! 55 | 56 | // If string is a raw user ID or a mention 57 | if let Some(user_id) = s.parse().ok().or_else(|| crate::utils::parse_user_mention(s)) { 58 | if let Ok(member) = guild_id.member(&ctx, user_id).await { 59 | return Ok(member); 60 | } 61 | } 62 | 63 | // Following code is inspired by discord.py's MemberConvert::query_member_named 64 | 65 | // If string is a username+discriminator 66 | if let Some((name, discrim)) = crate::utils::parse_user_tag(s) { 67 | if let Ok(member_results) = guild_id.search_members(ctx.http(), name, Some(100)).await { 68 | if let Some(member) = member_results.into_iter().find(|m| { 69 | m.user.name.eq_ignore_ascii_case(name) && m.user.discriminator == discrim 70 | }) { 71 | return Ok(member); 72 | } 73 | } 74 | } 75 | 76 | // If string is username or nickname 77 | if let Ok(member_results) = guild_id.search_members(ctx.http(), s, Some(100)).await { 78 | if let Some(member) = member_results.into_iter().find(|m| { 79 | m.user.name.eq_ignore_ascii_case(s) 80 | || m.nick.as_ref().is_some_and(|nick| nick.eq_ignore_ascii_case(s)) 81 | }) { 82 | return Ok(member); 83 | } 84 | } 85 | 86 | Err(MemberParseError::NotFoundOrMalformed) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/argument_convert/message.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use super::ArgumentConvert; 4 | use crate::model::prelude::*; 5 | use crate::prelude::*; 6 | 7 | /// Error that can be returned from [`Message::convert`]. 8 | #[non_exhaustive] 9 | #[derive(Debug)] 10 | pub enum MessageParseError { 11 | /// When the provided string does not adhere to any known guild message format 12 | Malformed, 13 | /// When message data retrieval via HTTP failed 14 | Http(SerenityError), 15 | /// When the `gateway` feature is disabled and the required information was not in cache. 16 | HttpNotAvailable, 17 | } 18 | 19 | impl std::error::Error for MessageParseError { 20 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 21 | match self { 22 | Self::Http(e) => Some(e), 23 | Self::HttpNotAvailable | Self::Malformed => None, 24 | } 25 | } 26 | } 27 | 28 | impl fmt::Display for MessageParseError { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | match self { 31 | Self::Malformed => { 32 | f.write_str("Provided string did not adhere to any known guild message format") 33 | }, 34 | Self::Http(_) => f.write_str("Failed to request message data via HTTP"), 35 | Self::HttpNotAvailable => f.write_str( 36 | "Gateway feature is disabled and the required information was not in cache", 37 | ), 38 | } 39 | } 40 | } 41 | 42 | /// Look up a message by a string. 43 | /// 44 | /// The lookup strategy is as follows (in order): 45 | /// 1. [Lookup by "{channel ID}-{message ID}"](`crate::utils::parse_message_id_pair`) (retrieved by 46 | /// shift-clicking on "Copy ID") 47 | /// 2. Lookup by message ID (the message must be in the context channel) 48 | /// 3. [Lookup by message URL](`crate::utils::parse_message_url`) 49 | #[async_trait::async_trait] 50 | impl ArgumentConvert for Message { 51 | type Err = MessageParseError; 52 | 53 | async fn convert( 54 | ctx: impl CacheHttp, 55 | _guild_id: Option, 56 | channel_id: Option, 57 | s: &str, 58 | ) -> Result { 59 | let extract_from_message_id = || Some((channel_id?, s.parse().ok()?)); 60 | 61 | let extract_from_message_url = || { 62 | let (_guild_id, channel_id, message_id) = crate::utils::parse_message_url(s)?; 63 | Some((channel_id, message_id)) 64 | }; 65 | 66 | let (channel_id, message_id) = crate::utils::parse_message_id_pair(s) 67 | .or_else(extract_from_message_id) 68 | .or_else(extract_from_message_url) 69 | .ok_or(MessageParseError::Malformed)?; 70 | 71 | channel_id.message(ctx, message_id).await.map_err(MessageParseError::Http) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/argument_convert/role.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use super::ArgumentConvert; 4 | use crate::model::prelude::*; 5 | use crate::prelude::*; 6 | 7 | /// Error that can be returned from [`Role::convert`]. 8 | #[non_exhaustive] 9 | #[derive(Debug)] 10 | pub enum RoleParseError { 11 | /// When the operation was invoked outside a guild. 12 | NotInGuild, 13 | /// When the guild's roles were not found in cache. 14 | NotInCache, 15 | /// HTTP error while retrieving guild roles. 16 | Http(SerenityError), 17 | /// The provided channel string failed to parse, or the parsed result cannot be found in the 18 | /// cache. 19 | NotFoundOrMalformed, 20 | } 21 | 22 | impl std::error::Error for RoleParseError { 23 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 24 | match self { 25 | Self::Http(e) => Some(e), 26 | Self::NotFoundOrMalformed | Self::NotInGuild | Self::NotInCache => None, 27 | } 28 | } 29 | } 30 | 31 | impl fmt::Display for RoleParseError { 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | match self { 34 | Self::NotInGuild => f.write_str("Must invoke this operation in a guild"), 35 | Self::NotInCache => f.write_str("Guild's roles were not found in cache"), 36 | Self::Http(_) => f.write_str("Failed to retrieve roles via HTTP"), 37 | Self::NotFoundOrMalformed => f.write_str("Role not found or unknown format"), 38 | } 39 | } 40 | } 41 | 42 | /// Look up a [`Role`] by a string case-insensitively. 43 | /// 44 | /// Requires the cache feature to be enabled. 45 | /// 46 | /// The lookup strategy is as follows (in order): 47 | /// 1. Lookup by ID 48 | /// 2. [Lookup by mention](`crate::utils::parse_role`). 49 | /// 3. Lookup by name (case-insensitive) 50 | #[async_trait::async_trait] 51 | impl ArgumentConvert for Role { 52 | type Err = RoleParseError; 53 | 54 | async fn convert( 55 | ctx: impl CacheHttp, 56 | guild_id: Option, 57 | _channel_id: Option, 58 | s: &str, 59 | ) -> Result { 60 | let guild_id = guild_id.ok_or(RoleParseError::NotInGuild)?; 61 | 62 | #[cfg(feature = "cache")] 63 | let guild; 64 | 65 | #[cfg(feature = "cache")] 66 | let roles = { 67 | let cache = ctx.cache().ok_or(RoleParseError::NotInCache)?; 68 | guild = cache.guild(guild_id).ok_or(RoleParseError::NotInCache)?; 69 | &guild.roles 70 | }; 71 | 72 | #[cfg(not(feature = "cache"))] 73 | let roles = ctx.http().get_guild_roles(guild_id).await.map_err(RoleParseError::Http)?; 74 | 75 | if let Some(role_id) = s.parse().ok().or_else(|| crate::utils::parse_role_mention(s)) { 76 | #[cfg(feature = "cache")] 77 | if let Some(role) = roles.get(&role_id) { 78 | return Ok(role.clone()); 79 | } 80 | #[cfg(not(feature = "cache"))] 81 | if let Some(role) = roles.iter().find(|role| role.id == role_id) { 82 | return Ok(role.clone()); 83 | } 84 | } 85 | 86 | #[cfg(feature = "cache")] 87 | if let Some(role) = roles.values().find(|role| role.name.eq_ignore_ascii_case(s)) { 88 | return Ok(role.clone()); 89 | } 90 | #[cfg(not(feature = "cache"))] 91 | if let Some(role) = roles.into_iter().find(|role| role.name.eq_ignore_ascii_case(s)) { 92 | return Ok(role); 93 | } 94 | 95 | Err(RoleParseError::NotFoundOrMalformed) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/utils/argument_convert/user.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use super::ArgumentConvert; 4 | use crate::model::prelude::*; 5 | use crate::prelude::*; 6 | 7 | /// Error that can be returned from [`User::convert`]. 8 | #[non_exhaustive] 9 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 10 | pub enum UserParseError { 11 | /// The provided user string failed to parse, or the parsed result cannot be found in the guild 12 | /// cache data. 13 | NotFoundOrMalformed, 14 | } 15 | 16 | impl std::error::Error for UserParseError {} 17 | 18 | impl fmt::Display for UserParseError { 19 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | match self { 21 | Self::NotFoundOrMalformed => f.write_str("User not found or unknown format"), 22 | } 23 | } 24 | } 25 | 26 | #[cfg(feature = "cache")] 27 | fn lookup_by_global_cache(ctx: impl CacheHttp, s: &str) -> Option { 28 | let users = &ctx.cache()?.users; 29 | 30 | let lookup_by_id = || users.get(&s.parse().ok()?).map(|u| u.clone()); 31 | 32 | let lookup_by_mention = || users.get(&crate::utils::parse_user_mention(s)?).map(|u| u.clone()); 33 | 34 | let lookup_by_name_and_discrim = || { 35 | let (name, discrim) = crate::utils::parse_user_tag(s)?; 36 | users.iter().find_map(|m| { 37 | let user = m.value(); 38 | (user.discriminator == discrim && user.name.eq_ignore_ascii_case(name)) 39 | .then(|| user.clone()) 40 | }) 41 | }; 42 | 43 | let lookup_by_name = || { 44 | users.iter().find_map(|m| { 45 | let user = m.value(); 46 | (user.name == s).then(|| user.clone()) 47 | }) 48 | }; 49 | 50 | lookup_by_id() 51 | .or_else(lookup_by_mention) 52 | .or_else(lookup_by_name_and_discrim) 53 | .or_else(lookup_by_name) 54 | } 55 | 56 | /// Look up a user by a string case-insensitively. 57 | /// 58 | /// Requires the cache feature to be enabled. If a user is not in cache, they will not be found! 59 | /// 60 | /// The lookup strategy is as follows (in order): 61 | /// 1. Lookup by ID. 62 | /// 2. [Lookup by mention](`crate::utils::parse_username`). 63 | /// 3. [Lookup by name#discrim](`crate::utils::parse_user_tag`). 64 | /// 4. Lookup by name 65 | #[async_trait::async_trait] 66 | impl ArgumentConvert for User { 67 | type Err = UserParseError; 68 | 69 | async fn convert( 70 | ctx: impl CacheHttp, 71 | guild_id: Option, 72 | channel_id: Option, 73 | s: &str, 74 | ) -> Result { 75 | // Try to look up in global user cache via a variety of methods 76 | #[cfg(feature = "cache")] 77 | if let Some(user) = lookup_by_global_cache(&ctx, s) { 78 | return Ok(user); 79 | } 80 | 81 | // If not successful, convert as a Member which uses HTTP endpoints instead of cache 82 | if let Ok(member) = Member::convert(&ctx, guild_id, channel_id, s).await { 83 | return Ok(member.user); 84 | } 85 | 86 | // If string is a raw user ID or a mention 87 | if let Some(user_id) = s.parse().ok().or_else(|| crate::utils::parse_user_mention(s)) { 88 | // Now, we can still try UserId::to_user because it works for all users from all guilds 89 | // the bot is joined 90 | if let Ok(user) = user_id.to_user(&ctx).await { 91 | return Ok(user); 92 | } 93 | } 94 | 95 | Err(UserParseError::NotFoundOrMalformed) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/utils/token.rs: -------------------------------------------------------------------------------- 1 | //! Utilities to parse and validate Discord tokens. 2 | 3 | use std::{fmt, str}; 4 | 5 | /// Validates that a token is likely in a valid format. 6 | /// 7 | /// This performs the following checks on a given token: 8 | /// - Is not empty; 9 | /// - Contains 3 parts (split by the period char `'.'`); 10 | /// - The second part of the token is at least 6 characters long; 11 | /// 12 | /// # Examples 13 | /// 14 | /// Validate that a token is valid and that a number of malformed tokens are actually invalid: 15 | /// 16 | /// ``` 17 | /// use serenity::utils::token::validate; 18 | /// 19 | /// // ensure a valid token is in fact a valid format: 20 | /// assert!(validate("Mjg4NzYwMjQxMzYzODc3ODg4.C_ikow.j3VupLBuE1QWZng3TMGH0z_UAwg").is_ok()); 21 | /// 22 | /// assert!(validate("Mjg4NzYwMjQxMzYzODc3ODg4").is_err()); 23 | /// assert!(validate("").is_err()); 24 | /// ``` 25 | /// 26 | /// # Errors 27 | /// 28 | /// Returns a [`InvalidToken`] when one of the above checks fail. The type of failure is not 29 | /// specified. 30 | pub fn validate(token: impl AsRef) -> Result<(), InvalidToken> { 31 | // Tokens can be preceded by "Bot " (that's how the Discord API expects them) 32 | let mut parts = token.as_ref().trim_start_matches("Bot ").split('.'); 33 | 34 | let is_valid = parts.next().is_some_and(|p| !p.is_empty()) 35 | && parts.next().is_some_and(|p| !p.is_empty()) 36 | && parts.next().is_some_and(|p| !p.is_empty()) 37 | && parts.next().is_none(); 38 | 39 | if is_valid { 40 | Ok(()) 41 | } else { 42 | Err(InvalidToken) 43 | } 44 | } 45 | 46 | /// Error that can be return by [`validate`]. 47 | #[derive(Debug)] 48 | pub struct InvalidToken; 49 | 50 | impl std::error::Error for InvalidToken {} 51 | 52 | impl fmt::Display for InvalidToken { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | f.write_str("The provided token was invalid") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/test_reaction.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use serenity::model::channel::ReactionType; 4 | use serenity::model::id::EmojiId; 5 | 6 | #[test] 7 | fn str_to_reaction_type() { 8 | let emoji_str = "<:customemoji:600404340292059257>"; 9 | let reaction = ReactionType::try_from(emoji_str).unwrap(); 10 | let reaction2 = ReactionType::Custom { 11 | animated: false, 12 | id: EmojiId::new(600404340292059257), 13 | name: Some("customemoji".to_string()), 14 | }; 15 | assert_eq!(reaction, reaction2); 16 | } 17 | 18 | #[test] 19 | fn str_to_reaction_type_animated() { 20 | let emoji_str = ""; 21 | let reaction = ReactionType::try_from(emoji_str).unwrap(); 22 | let reaction2 = ReactionType::Custom { 23 | animated: true, 24 | id: EmojiId::new(600409340292059257), 25 | name: Some("customemoji2".to_string()), 26 | }; 27 | assert_eq!(reaction, reaction2); 28 | } 29 | 30 | #[test] 31 | fn string_to_reaction_type() { 32 | let emoji_string = "<:customemoji:600404340292059257>".to_string(); 33 | let reaction = ReactionType::try_from(emoji_string).unwrap(); 34 | let reaction2 = ReactionType::Custom { 35 | animated: false, 36 | id: EmojiId::new(600404340292059257), 37 | name: Some("customemoji".to_string()), 38 | }; 39 | assert_eq!(reaction, reaction2); 40 | } 41 | 42 | #[test] 43 | fn string_to_reaction_type_empty() { 44 | let emoji_string = "".to_string(); 45 | ReactionType::try_from(emoji_string).unwrap_err(); 46 | } 47 | 48 | #[test] 49 | fn str_to_reaction_type_empty() { 50 | let emoji_str = ""; 51 | ReactionType::try_from(emoji_str).unwrap_err(); 52 | } 53 | 54 | #[test] 55 | fn str_to_reaction_type_mangled() { 56 | let emoji_str = ""; 57 | ReactionType::try_from(emoji_str).unwrap_err(); 58 | } 59 | 60 | #[test] 61 | fn str_to_reaction_type_mangled_2() { 62 | let emoji_str = "Trail"; 63 | ReactionType::try_from(emoji_str).unwrap_err(); 64 | } 65 | 66 | #[test] 67 | fn str_to_reaction_type_mangled_3() { 68 | let emoji_str = ""; 69 | ReactionType::try_from(emoji_str).unwrap_err(); 70 | } 71 | 72 | #[test] 73 | fn str_to_reaction_type_mangled_4() { 74 | let emoji_str = "<:somestuff:1234"; 75 | ReactionType::try_from(emoji_str).unwrap_err(); 76 | } 77 | 78 | #[test] 79 | fn str_fromstr() { 80 | let emoji_str = "<:somestuff:1234"; 81 | ReactionType::from_str(emoji_str).unwrap_err(); 82 | } 83 | 84 | #[test] 85 | fn json_to_reaction_type() { 86 | let s = r#"{"name": "foo", "id": "1"}"#; 87 | let value = serde_json::from_str(s).unwrap(); 88 | assert!(matches!(value, ReactionType::Custom { .. })); 89 | if let ReactionType::Custom { 90 | name, .. 91 | } = value 92 | { 93 | assert_eq!(name.as_deref(), Some("foo")); 94 | } 95 | 96 | let s = r#"{"name": null, "id": "1"}"#; 97 | let value = serde_json::from_str(s).unwrap(); 98 | assert!(matches!(value, ReactionType::Custom { .. })); 99 | 100 | let s = r#"{"id": "1"}"#; 101 | let value = serde_json::from_str(s).unwrap(); 102 | assert!(matches!(value, ReactionType::Custom { .. })); 103 | 104 | let s = r#"{"name": "foo"}"#; 105 | let value = serde_json::from_str(s).unwrap(); 106 | assert!(matches!(value, ReactionType::Unicode(_))); 107 | if let ReactionType::Unicode(value) = value { 108 | assert_eq!(value, "foo"); 109 | } 110 | 111 | let s = r#"{"name": null}"#; 112 | assert!(serde_json::from_str::(s).is_err()); 113 | } 114 | -------------------------------------------------------------------------------- /voice-model/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serenity-voice-model" 3 | version = "0.2.0" 4 | authors = ["Alex M. M. "] 5 | description = "A Rust library for (de)serializing Discord Voice API gateway messages." 6 | # readme = "README.md" 7 | include = ["src/**/*.rs", "Cargo.toml"] 8 | 9 | documentation.workspace = true 10 | homepage.workspace = true 11 | repository.workspace = true 12 | keywords.workspace = true 13 | license.workspace = true 14 | edition.workspace = true 15 | rust-version.workspace = true 16 | 17 | [dependencies] 18 | bitflags = "2.4" 19 | num-traits = "0.2" 20 | serde_repr = "0.1.5" 21 | 22 | [dependencies.serde] 23 | version = "1" 24 | features = ["derive"] 25 | 26 | [dependencies.serde_json] 27 | features = ["raw_value"] 28 | version = "1" 29 | 30 | [dev-dependencies] 31 | criterion = "0.5" 32 | serde_test = "1" 33 | 34 | [[bench]] 35 | name = "deserialisation" 36 | path = "benches/de.rs" 37 | harness = false 38 | -------------------------------------------------------------------------------- /voice-model/benches/de.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use serenity_voice_model::Event; 3 | 4 | pub fn json_deser(c: &mut Criterion) { 5 | let json_data = r#"{ 6 | "op": 2, 7 | "d": { 8 | "ssrc": 1, 9 | "ip": "127.0.0.1", 10 | "port": 1234, 11 | "modes": ["xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite"], 12 | "heartbeat_interval": 1 13 | } 14 | }"#; 15 | 16 | let wonky_json_data = r#"{ 17 | "d": { 18 | "ssrc": 1, 19 | "ip": "127.0.0.1", 20 | "port": 1234, 21 | "modes": ["xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite"], 22 | "heartbeat_interval": 1 23 | }, 24 | "op": 2 25 | }"#; 26 | 27 | c.bench_function("Ready event", |b| { 28 | b.iter(|| serde_json::from_str::(black_box(json_data))) 29 | }); 30 | 31 | c.bench_function("Ready event (bad order)", |b| { 32 | b.iter(|| serde_json::from_str::(black_box(wonky_json_data))) 33 | }); 34 | } 35 | 36 | criterion_group!(benches, json_deser); 37 | criterion_main!(benches); 38 | -------------------------------------------------------------------------------- /voice-model/rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | match_block_trailing_comma = true 3 | newline_style = "Unix" 4 | use_field_init_shorthand = true 5 | use_small_heuristics = "Max" 6 | use_try_shorthand = true 7 | 8 | # nightly/unstable features 9 | format_code_in_doc_comments = true 10 | group_imports = "StdExternalCrate" 11 | imports_granularity = "Module" 12 | imports_layout = "HorizontalVertical" 13 | match_arm_blocks = false 14 | normalize_comments = true 15 | overflow_delimited_expr = true 16 | struct_lit_single_line = false 17 | -------------------------------------------------------------------------------- /voice-model/src/close_code.rs: -------------------------------------------------------------------------------- 1 | /// Discord Voice Gateway Websocket close codes. 2 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 3 | pub enum CloseCode { 4 | /// Invalid Voice OP Code. 5 | UnknownOpcode = 4001, 6 | /// Invalid identification payload sent. 7 | InvalidPayload = 4002, 8 | /// A payload was sent prior to identifying. 9 | NotAuthenticated = 4003, 10 | /// The account token sent with the identify payload was incorrect. 11 | AuthenticationFailed = 4004, 12 | /// More than one identify payload was sent. 13 | AlreadyAuthenticated = 4005, 14 | /// The session is no longer valid. 15 | SessionInvalid = 4006, 16 | /// A session timed out. 17 | SessionTimeout = 4009, 18 | /// The server for the last connection attempt could not be found. 19 | ServerNotFound = 4011, 20 | /// Discord did not recognise the voice protocol chosen. 21 | UnknownProtocol = 4012, 22 | /// Disconnected, either due to channel closure/removal or kicking. 23 | /// 24 | /// Should not reconnect. 25 | Disconnected = 4014, 26 | /// Connected voice server crashed. 27 | /// 28 | /// Should resume. 29 | VoiceServerCrash = 4015, 30 | /// Discord didn't recognise the encryption scheme. 31 | UnknownEncryptionMode = 4016, 32 | } 33 | 34 | impl CloseCode { 35 | /// Indicates whether a voice client should attempt to reconnect in response to this close 36 | /// code. 37 | /// 38 | /// Otherwise, the connection should be closed. 39 | pub fn should_resume(&self) -> bool { 40 | matches!(self, CloseCode::VoiceServerCrash | CloseCode::SessionTimeout) 41 | } 42 | } 43 | 44 | impl num_traits::cast::FromPrimitive for CloseCode { 45 | fn from_u64(n: u64) -> Option { 46 | Some(match n { 47 | 4001 => Self::UnknownOpcode, 48 | 4002 => Self::InvalidPayload, 49 | 4003 => Self::NotAuthenticated, 50 | 4004 => Self::AuthenticationFailed, 51 | 4005 => Self::AlreadyAuthenticated, 52 | 4006 => Self::SessionInvalid, 53 | 4009 => Self::SessionTimeout, 54 | 4011 => Self::ServerNotFound, 55 | 4012 => Self::UnknownProtocol, 56 | 4014 => Self::Disconnected, 57 | 4015 => Self::VoiceServerCrash, 58 | 4016 => Self::UnknownEncryptionMode, 59 | _ => return None, 60 | }) 61 | } 62 | 63 | fn from_i64(n: i64) -> Option { 64 | Self::from_u64(n as u64) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /voice-model/src/constants.rs: -------------------------------------------------------------------------------- 1 | //! A set of constants used by the library. 2 | 3 | /// Gateway version of the Voice API which this library encodes. 4 | pub const GATEWAY_VERSION: u8 = 4; 5 | -------------------------------------------------------------------------------- /voice-model/src/event/from.rs: -------------------------------------------------------------------------------- 1 | use super::Event; 2 | use crate::payload::*; 3 | 4 | impl From for Event { 5 | fn from(i: Identify) -> Self { 6 | Event::Identify(i) 7 | } 8 | } 9 | 10 | impl From for Event { 11 | fn from(i: SelectProtocol) -> Self { 12 | Event::SelectProtocol(i) 13 | } 14 | } 15 | 16 | impl From for Event { 17 | fn from(i: Ready) -> Self { 18 | Event::Ready(i) 19 | } 20 | } 21 | 22 | impl From for Event { 23 | fn from(i: Heartbeat) -> Self { 24 | Event::Heartbeat(i) 25 | } 26 | } 27 | 28 | impl From for Event { 29 | fn from(i: SessionDescription) -> Self { 30 | Event::SessionDescription(i) 31 | } 32 | } 33 | 34 | impl From for Event { 35 | fn from(i: Speaking) -> Self { 36 | Event::Speaking(i) 37 | } 38 | } 39 | 40 | impl From for Event { 41 | fn from(i: HeartbeatAck) -> Self { 42 | Event::HeartbeatAck(i) 43 | } 44 | } 45 | 46 | impl From for Event { 47 | fn from(i: Resume) -> Self { 48 | Event::Resume(i) 49 | } 50 | } 51 | 52 | impl From for Event { 53 | fn from(i: Hello) -> Self { 54 | Event::Hello(i) 55 | } 56 | } 57 | 58 | impl From for Event { 59 | fn from(i: ClientConnect) -> Self { 60 | Event::ClientConnect(i) 61 | } 62 | } 63 | 64 | impl From for Event { 65 | fn from(i: ClientDisconnect) -> Self { 66 | Event::ClientDisconnect(i) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /voice-model/src/id.rs: -------------------------------------------------------------------------------- 1 | //! A collection of newtypes defining type-strong IDs. 2 | use std::fmt; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::util::json_safe_u64; 7 | 8 | #[derive( 9 | Clone, Copy, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, 10 | )] 11 | pub struct GuildId(#[serde(with = "json_safe_u64")] pub u64); 12 | 13 | impl fmt::Display for GuildId { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | fmt::Display::fmt(&self.0, f) 16 | } 17 | } 18 | 19 | #[derive( 20 | Clone, Copy, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, 21 | )] 22 | pub struct UserId(#[serde(with = "json_safe_u64")] pub u64); 23 | 24 | impl fmt::Display for UserId { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | fmt::Display::fmt(&self.0, f) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /voice-model/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Mappings of objects received from Discord's voice gateway API, with implementations for 2 | //! (de)serialisation. 3 | #![deny(rustdoc::broken_intra_doc_links)] 4 | 5 | mod close_code; 6 | pub mod constants; 7 | mod event; 8 | pub mod id; 9 | mod opcode; 10 | pub mod payload; 11 | mod protocol_data; 12 | mod speaking_state; 13 | mod util; 14 | 15 | pub use num_traits::FromPrimitive; 16 | 17 | pub use self::close_code::CloseCode; 18 | pub use self::event::Event; 19 | pub use self::opcode::Opcode; 20 | pub use self::protocol_data::ProtocolData; 21 | pub use self::speaking_state::SpeakingState; 22 | -------------------------------------------------------------------------------- /voice-model/src/opcode.rs: -------------------------------------------------------------------------------- 1 | use serde_repr::{Deserialize_repr, Serialize_repr}; 2 | 3 | /// An enum representing the [voice opcodes]. 4 | /// 5 | /// [voice opcodes]: https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice 6 | #[derive( 7 | Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize_repr, Serialize_repr, 8 | )] 9 | #[non_exhaustive] 10 | #[repr(u8)] 11 | pub enum Opcode { 12 | /// Used to begin a voice websocket connection. 13 | Identify = 0, 14 | /// Used to select the voice protocol. 15 | SelectProtocol = 1, 16 | /// Used to complete the websocket handshake. 17 | Ready = 2, 18 | /// Used to keep the websocket connection alive. 19 | Heartbeat = 3, 20 | /// Server's confirmation of a negotiated encryption scheme. 21 | SessionDescription = 4, 22 | /// Used to indicate which users are speaking, or to inform Discord that the client is now speaking. 23 | Speaking = 5, 24 | /// Heartbeat ACK, received by the client to show the server's receipt of a heartbeat. 25 | HeartbeatAck = 6, 26 | /// Sent after a disconnect to attempt to resume a session. 27 | Resume = 7, 28 | /// Used to determine how often the client must send a heartbeat. 29 | Hello = 8, 30 | /// Sent by the server if a session could successfully be resumed. 31 | Resumed = 9, 32 | /// Message indicating that another user has connected to the voice channel. 33 | ClientConnect = 12, 34 | /// Message indicating that another user has disconnected from the voice channel. 35 | ClientDisconnect = 13, 36 | } 37 | -------------------------------------------------------------------------------- /voice-model/src/protocol_data.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// The client's response to a connection offer. 6 | #[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] 7 | pub struct ProtocolData { 8 | /// IP address of the client as seen by the server (*e.g.*, after using [IP Discovery] for NAT 9 | /// hole-punching). 10 | /// 11 | /// [IP Discovery]: https://docs.rs/discortp/discord/struct.IpDiscovery.html 12 | pub address: IpAddr, 13 | /// The client's chosen encryption mode (from those offered by the server). 14 | pub mode: String, 15 | /// UDP source port of the client as seen by the server, as above. 16 | pub port: u16, 17 | } 18 | -------------------------------------------------------------------------------- /voice-model/src/speaking_state.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | use serde::de::Deserializer; 3 | use serde::ser::Serializer; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | bitflags! { 7 | /// Flag set describing how a speaker is sending audio. 8 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 9 | pub struct SpeakingState: u8 { 10 | /// Normal transmission of voice audio. 11 | const MICROPHONE = 1; 12 | /// Transmission of context audio for video, no speaking indicator. 13 | const SOUNDSHARE = 1 << 1; 14 | /// Priority speaker, lowering audio of other speakers. 15 | const PRIORITY = 1 << 2; 16 | } 17 | } 18 | 19 | impl SpeakingState { 20 | pub fn microphone(self) -> bool { 21 | self.contains(Self::MICROPHONE) 22 | } 23 | 24 | pub fn soundshare(self) -> bool { 25 | self.contains(Self::SOUNDSHARE) 26 | } 27 | 28 | pub fn priority(self) -> bool { 29 | self.contains(Self::PRIORITY) 30 | } 31 | } 32 | 33 | // Manual impl needed because object is sent as a flags integer 34 | // (could maybe just put `#[serde(transparent)]` on the type?) 35 | impl<'de> Deserialize<'de> for SpeakingState { 36 | fn deserialize>(deserializer: D) -> Result { 37 | Ok(Self::from_bits_truncate(u8::deserialize(deserializer)?)) 38 | } 39 | } 40 | 41 | impl Serialize for SpeakingState { 42 | fn serialize(&self, serializer: S) -> Result 43 | where 44 | S: Serializer, 45 | { 46 | serializer.serialize_u8(self.bits()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /voice-model/src/util.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod json_safe_u64 { 2 | use core::fmt::{Formatter, Result as FmtResult}; 3 | 4 | use serde::de::{Deserializer, Error, Visitor}; 5 | use serde::ser::Serializer; 6 | 7 | struct U64Visitor; 8 | 9 | impl Visitor<'_> for U64Visitor { 10 | type Value = u64; 11 | 12 | fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { 13 | formatter.write_str("a u64 represented by a string or number") 14 | } 15 | 16 | fn visit_u64(self, value: u64) -> Result { 17 | Ok(value) 18 | } 19 | 20 | fn visit_str(self, value: &str) -> Result { 21 | value.parse::().map_err(E::custom) 22 | } 23 | } 24 | 25 | pub fn deserialize<'de, D>(deserializer: D) -> Result 26 | where 27 | D: Deserializer<'de>, 28 | { 29 | deserializer.deserialize_any(U64Visitor) 30 | } 31 | 32 | pub fn serialize(value: &u64, serializer: S) -> Result 33 | where 34 | S: Serializer, 35 | { 36 | serializer.collect_str(value) 37 | } 38 | } 39 | --------------------------------------------------------------------------------