├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── Cargo.toml ├── README.md ├── argument-choices.rs ├── autocompleting-arguments.rs ├── basic-bot.rs ├── custom-types.rs ├── shared-data.rs ├── using-checks.rs ├── using-dedicated-tasks.rs ├── using-groups.rs ├── using-hooks.rs └── using-modals.rs ├── vesper-macros ├── Cargo.toml ├── LICENSE ├── README.md └── src │ ├── after.rs │ ├── autocomplete.rs │ ├── before.rs │ ├── check.rs │ ├── command │ ├── argument.rs │ ├── details.rs │ └── mod.rs │ ├── error_handler.rs │ ├── extractors │ ├── closure.rs │ ├── either.rs │ ├── function_closure.rs │ ├── function_path.rs │ ├── ident.rs │ ├── list.rs │ ├── map.rs │ ├── mod.rs │ └── tuple.rs │ ├── hook.rs │ ├── lib.rs │ ├── modal.rs │ ├── optional.rs │ ├── parse.rs │ └── util.rs └── vesper ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── argument.rs ├── builder.rs ├── command.rs ├── context.rs ├── error.rs ├── extract.rs ├── framework.rs ├── group.rs ├── hook.rs ├── iter.rs ├── lib.rs ├── localizations.rs ├── modal.rs ├── parse.rs ├── parse_impl.rs ├── parsers.rs ├── range.rs └── wait.rs /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | 4 | target 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Log of changes for ``zephyrus`` crate, changes in between versions will be documented here. 4 | 5 | --- 6 | 7 | ## 0.5.0 - 2022-11-23 8 | 9 | #### Changes: 10 | - Updated twilight to version 0.14 ([Carson M] at [#3]) 11 | 12 | 13 | ## 0.6.0 -- 2022-12-28 14 | 15 | #### Changes: 16 | - Framework can now have custom return types from commands 17 | - Created usage examples 18 | 19 | ## 0.7.0 -- 2023-01-05 20 | 21 | #### Changes: 22 | - Changed Parse trait signature 23 | - Implemented parse for Id 24 | - Now a mutable reference for an interaction can be obtained using SlashContext::interaction_mut() 25 | - Implemented Parse for Attachment, User and Role 26 | 27 | ## 0.8.0 -- 2023-02-06 28 | 29 | #### Changes: 30 | - Created Modal trait 31 | - Now context can send modals directly 32 | - Created Modal derive macro to create modals directly 33 | 34 | ## 0.9.0 -- 2023-02-24 35 | 36 | #### Changes: 37 | - Updated twilight dependencies to 0.15 38 | - Now modal derive requires to specify attributes inside a modal one: #[modal(...)] 39 | - Now parse derive requires to specify attributes inside a parse one: #[parse(...)] 40 | 41 | ## 0.10.0 -- 2023-07-31 42 | 43 | #### Changes: 44 | - Moved to `darling` crate to create macros 45 | - Added support for `chat` and `message` commands 46 | - Added `#[only_guilds]` and `#[nsfw]` attribute for commands 47 | - Deprecated `SlashContext#acknowledge` in favor of `SlashContext#defer` 48 | - Added `Framework#twilight_commands` to get a serializable representation of registered commands ([Carter] at [#9] & [#10]) 49 | 50 | ## 0.11.0 -- 2023/09/21 51 | - Use typed errors 52 | - Added localizations for command both commands and it's attributes 53 | - Allow a `#[skip]` attribute for chat command arguments 54 | 55 | ## 0.12.0 -- 2023/12/28 56 | - Fixed an error when dereferencing a misaligned pointer on `Range` type ([iciivy] at [#13]) 57 | - Allow providing localizations using closures and function pointers 58 | 59 | ## 0.13.0 -- 2024/2/5 60 | - Omit emitting argument parsing code on commands without arguments ([Carson M] at [#16]) 61 | - Disabled ``twilight-http`` default features ([Carson M] at [#17]) 62 | - Add Channel & Thread related parsers 63 | 64 | 65 | [Carson M]: https://github.com/decahedron1 66 | [Carter]: https://github.com/Fyko 67 | [iciivy]: https://github.com/iciivy 68 | 69 | 70 | [#3]: https://github.com/AlvaroMS25/zephyrus/pull/3 71 | [#9]: https://github.com/AlvaroMS25/zephyrus/pull/9 72 | [#10]: https://github.com/AlvaroMS25/zephyrus/pull/10 73 | [#13]: https://github.com/AlvaroMS25/vesper/pull/13 74 | [#16]: https://github.com/AlvaroMS25/vesper/pull/16 75 | [#17]: https://github.com/AlvaroMS25/vesper/pull/17 76 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "examples", 4 | "vesper", 5 | "vesper-macros" 6 | ] 7 | resolver = "2" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alvaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zephyrus-examples" 3 | edition = "2021" 4 | version = "0.0.0" 5 | publish = false 6 | 7 | [dependencies] 8 | futures-util = "0.3" 9 | rand = "0.8" 10 | tokio = { version = "1", features = ["full"]} 11 | twilight-http = "0.15" 12 | twilight-model = "0.15" 13 | twilight-gateway = "0.15.0" 14 | vesper = { path = "../vesper" } 15 | 16 | [[example]] 17 | name = "basic-bot" 18 | path = "basic-bot.rs" 19 | 20 | [[example]] 21 | name = "using-groups" 22 | path = "using-groups.rs" 23 | 24 | [[example]] 25 | name = "shared-data" 26 | path = "shared-data.rs" 27 | 28 | [[example]] 29 | name = "using-hooks" 30 | path = "using-hooks.rs" 31 | 32 | [[example]] 33 | name = "using-checks" 34 | path = "using-checks.rs" 35 | 36 | [[example]] 37 | name = "autocompleting-arguments" 38 | path = "autocompleting-arguments.rs" 39 | 40 | [[example]] 41 | name = "argument-choices" 42 | path = "argument-choices.rs" 43 | 44 | [[example]] 45 | name = "using-dedicated-tasks" 46 | path = "using-dedicated-tasks.rs" 47 | 48 | [[example]] 49 | name = "custom-types" 50 | path = "custom-types.rs" 51 | 52 | [[example]] 53 | name = "using-modals" 54 | path = "using-modals.rs" 55 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ### Usage examples for zephyrus. 2 | 3 | This directory contains several examples on how to use the ``zephyrus`` crate. 4 | 5 | The examples can be run by using the following cargo command: 6 | ``` 7 | cargo run --example 8 | ``` 9 | 10 | *Please note that most of the examples are made without spawning dedicated tasks, this is to simplify the examples 11 | and to avoid having to always have to wrap the framework inside an `Arc`. However, it is preferred to process each 12 | interaction inside a dedicated task to avoid having to wait until a command finishes its execution to execute the next 13 | one* 14 | 15 | To add an example to this directory, feel free to open a Pull request. 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/argument-choices.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use futures_util::StreamExt; 4 | use twilight_gateway::{stream::{self, ShardEventStream}, Config}; 5 | use twilight_http::Client; 6 | use twilight_model::gateway::event::Event; 7 | use twilight_model::gateway::Intents; 8 | use twilight_model::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}; 9 | use twilight_model::id::Id; 10 | use vesper::prelude::*; 11 | 12 | #[tokio::main] 13 | async fn main() { 14 | let token = env::var("DISCORD_TOKEN").unwrap(); 15 | let application_id = env::var("DISCORD_APPLICATION_ID").unwrap().parse::().unwrap(); 16 | 17 | let http_client = Arc::new(Client::new(token.clone())); 18 | 19 | let config = Config::new(token.clone(), Intents::empty()); 20 | let mut shards = stream::create_recommended( 21 | &http_client, 22 | config, 23 | |_, builder| builder.build() 24 | ).await.unwrap().collect::>(); 25 | 26 | let mut stream = ShardEventStream::new(shards.iter_mut()); 27 | 28 | let framework = Framework::builder(http_client, Id::new(application_id), ()) 29 | .command(multiple_selection) 30 | .build(); 31 | 32 | while let Some((_, event)) = stream.next().await { 33 | match event { 34 | Err(error) => { 35 | if error.is_fatal() { 36 | eprintln!("Gateway connection fatally closed, error: {error:?}"); 37 | break; 38 | } 39 | }, 40 | Ok(event) => match event { 41 | Event::Ready(_) => { 42 | // We have to register the commands for them to show in discord. 43 | framework.register_global_commands().await.unwrap(); 44 | }, 45 | Event::InteractionCreate(interaction) => { 46 | framework.process(interaction.0).await; 47 | }, 48 | _ => () 49 | } 50 | } 51 | } 52 | } 53 | 54 | #[derive(Debug, Parse)] 55 | enum ArgumentOption { 56 | Something, 57 | AnotherOption, 58 | #[parse(rename = "Some other item")] Other // This will be shown as "Some other item" 59 | } 60 | 61 | #[command] 62 | #[description = "Says hello"] 63 | async fn multiple_selection( 64 | ctx: &SlashContext<()>, 65 | #[description = "The option to select"] option: ArgumentOption 66 | ) -> DefaultCommandResult { 67 | ctx.interaction_client.create_response( 68 | ctx.interaction.id, 69 | &ctx.interaction.token, 70 | &InteractionResponse { 71 | kind: InteractionResponseType::ChannelMessageWithSource, 72 | data: Some(InteractionResponseData { 73 | content: Some(format!("You selected: {option:?}")), 74 | ..Default::default() 75 | }) 76 | } 77 | ).await?; 78 | 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /examples/autocompleting-arguments.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use futures_util::StreamExt; 4 | use twilight_gateway::{stream::{self, ShardEventStream}, Config}; 5 | use twilight_http::Client; 6 | use twilight_model::application::command::{CommandOptionChoice, CommandOptionChoiceValue}; 7 | use twilight_model::gateway::event::Event; 8 | use twilight_model::gateway::Intents; 9 | use twilight_model::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}; 10 | use twilight_model::id::Id; 11 | use vesper::prelude::*; 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | let token = env::var("DISCORD_TOKEN").unwrap(); 16 | let application_id = env::var("DISCORD_APPLICATION_ID").unwrap().parse::().unwrap(); 17 | 18 | let http_client = Arc::new(Client::new(token.clone())); 19 | 20 | let config = Config::new(token.clone(), Intents::empty()); 21 | let mut shards = stream::create_recommended( 22 | &http_client, 23 | config, 24 | |_, builder| builder.build() 25 | ).await.unwrap().collect::>(); 26 | 27 | let mut stream = ShardEventStream::new(shards.iter_mut()); 28 | 29 | let framework = Framework::builder(http_client, Id::new(application_id), ()) 30 | .command(random_number) 31 | .build(); 32 | 33 | while let Some((_, event)) = stream.next().await { 34 | match event { 35 | Err(error) => { 36 | if error.is_fatal() { 37 | eprintln!("Gateway connection fatally closed, error: {error:?}"); 38 | break; 39 | } 40 | }, 41 | Ok(event) => match event { 42 | Event::Ready(_) => { 43 | // We have to register the commands for them to show in discord. 44 | framework.register_global_commands().await.unwrap(); 45 | }, 46 | Event::InteractionCreate(interaction) => { 47 | framework.process(interaction.0).await; 48 | }, 49 | _ => () 50 | } 51 | } 52 | } 53 | } 54 | 55 | #[autocomplete] 56 | async fn generate_random(_ctx: AutocompleteContext<()>) -> Option { 57 | Some(InteractionResponseData { 58 | choices: Some((0..5) 59 | .map(|_| rand::random::()) 60 | .map(|item| { 61 | CommandOptionChoice { 62 | name: item.to_string(), 63 | name_localizations: None, 64 | value: CommandOptionChoiceValue::Integer(item as i64) 65 | } 66 | }) 67 | .collect()), 68 | ..Default::default() 69 | }) 70 | } 71 | 72 | #[command] 73 | #[description = "Uses autocompletion to provide a random list of numbers"] 74 | async fn random_number( 75 | ctx: &SlashContext<()>, 76 | #[autocomplete(generate_random)] #[description = "A number to repeat"] num: u8 77 | ) -> DefaultCommandResult 78 | { 79 | ctx.interaction_client.create_response( 80 | ctx.interaction.id, 81 | &ctx.interaction.token, 82 | &InteractionResponse { 83 | kind: InteractionResponseType::ChannelMessageWithSource, 84 | data: Some(InteractionResponseData { 85 | content: Some(format!("The number is {num}")), 86 | ..Default::default() 87 | }) 88 | } 89 | ).await?; 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /examples/basic-bot.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use futures_util::StreamExt; 4 | use twilight_gateway::{stream::{self, ShardEventStream}, Config}; 5 | use twilight_http::Client; 6 | use twilight_model::gateway::event::Event; 7 | use twilight_model::gateway::Intents; 8 | use twilight_model::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}; 9 | use twilight_model::id::Id; 10 | use vesper::prelude::*; 11 | 12 | #[tokio::main] 13 | async fn main() { 14 | let token = env::var("DISCORD_TOKEN").unwrap(); 15 | let application_id = env::var("DISCORD_APPLICATION_ID").unwrap().parse::().unwrap(); 16 | 17 | let http_client = Arc::new(Client::new(token.clone())); 18 | 19 | let config = Config::new(token.clone(), Intents::empty()); 20 | let mut shards = stream::create_recommended( 21 | &http_client, 22 | config, 23 | |_, builder| builder.build() 24 | ).await.unwrap().collect::>(); 25 | 26 | let mut stream = ShardEventStream::new(shards.iter_mut()); 27 | 28 | let framework = Framework::builder(http_client, Id::new(application_id), ()) 29 | .command(hello) 30 | .build(); 31 | 32 | while let Some((_, event)) = stream.next().await { 33 | match event { 34 | Err(error) => { 35 | if error.is_fatal() { 36 | eprintln!("Gateway connection fatally closed, error: {error:?}"); 37 | break; 38 | } 39 | }, 40 | Ok(event) => match event { 41 | Event::Ready(_) => { 42 | // We have to register the commands for them to show in discord. 43 | framework.register_global_commands().await.unwrap(); 44 | }, 45 | Event::InteractionCreate(interaction) => { 46 | framework.process(interaction.0).await; 47 | }, 48 | _ => () 49 | } 50 | } 51 | } 52 | } 53 | 54 | #[command] 55 | #[description = "Says hello"] 56 | async fn hello(ctx: &SlashContext<()>) -> DefaultCommandResult { 57 | ctx.interaction_client.create_response( 58 | ctx.interaction.id, 59 | &ctx.interaction.token, 60 | &InteractionResponse { 61 | kind: InteractionResponseType::ChannelMessageWithSource, 62 | data: Some(InteractionResponseData { 63 | content: Some(String::from("Hello!")), 64 | ..Default::default() 65 | }) 66 | } 67 | ).await?; 68 | 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /examples/custom-types.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use std::time::{Duration, Instant}; 4 | use futures_util::StreamExt; 5 | use twilight_gateway::{stream::{self, ShardEventStream}, Config}; 6 | use twilight_http::Client; 7 | use twilight_model::gateway::event::Event; 8 | use twilight_model::gateway::Intents; 9 | use twilight_model::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}; 10 | use twilight_model::id::Id; 11 | use vesper::framework::DefaultError; 12 | use vesper::prelude::*; 13 | 14 | // The framework accepts custom error types, however, the custom error must implement 15 | // `From` 16 | pub enum MyError { 17 | Parse(ParseError), 18 | Http(twilight_http::Error), 19 | Other(DefaultError) 20 | } 21 | 22 | impl From for MyError { 23 | fn from(value: ParseError) -> Self { 24 | Self::Parse(value) 25 | } 26 | } 27 | 28 | impl From for MyError { 29 | fn from(value: twilight_http::Error) -> Self { 30 | Self::Http(value) 31 | } 32 | } 33 | 34 | impl From for MyError { 35 | fn from(value: DefaultError) -> Self { 36 | Self::Other(value) 37 | } 38 | } 39 | 40 | // Let's measure the time a command needs to respond to an interaction, to do this let's specify 41 | // a custom `Ok` return type for our commands. 42 | pub struct ElapsedTime(Duration); 43 | 44 | #[tokio::main] 45 | async fn main() { 46 | let token = env::var("DISCORD_TOKEN").unwrap(); 47 | let application_id = env::var("DISCORD_APPLICATION_ID").unwrap().parse::().unwrap(); 48 | 49 | let http_client = Arc::new(Client::new(token.clone())); 50 | 51 | let config = Config::new(token.clone(), Intents::empty()); 52 | let mut shards = stream::create_recommended( 53 | &http_client, 54 | config, 55 | |_, builder| builder.build() 56 | ).await.unwrap().collect::>(); 57 | 58 | let mut stream = ShardEventStream::new(shards.iter_mut()); 59 | 60 | // The framework supports setting a custom return type for commands and some hooks, to specify 61 | // them just pass them as generic types. 62 | let framework = Framework::<_, ElapsedTime, MyError>::builder(http_client, Id::new(application_id), ()) 63 | .command(timer) 64 | .after(after_hook) 65 | .build(); 66 | 67 | while let Some((_, event)) = stream.next().await { 68 | match event { 69 | Err(error) => { 70 | if error.is_fatal() { 71 | eprintln!("Gateway connection fatally closed, error: {error:?}"); 72 | break; 73 | } 74 | }, 75 | Ok(event) => match event { 76 | Event::Ready(_) => { 77 | // We have to register the commands for them to show in discord. 78 | framework.register_global_commands().await.unwrap(); 79 | }, 80 | Event::InteractionCreate(interaction) => { 81 | framework.process(interaction.0).await; 82 | }, 83 | _ => () 84 | } 85 | } 86 | } 87 | } 88 | 89 | #[after] 90 | async fn after_hook( 91 | _: &SlashContext<()>, 92 | command_name: &str, 93 | result: Option> 94 | ) { 95 | // We don't have a custom error handler, so result will be always `Some` 96 | let result = result.unwrap(); 97 | 98 | match result { 99 | Ok(elapsed) => { 100 | println!("Command {} took {} ms to execute", command_name, elapsed.0.as_millis()) 101 | }, 102 | Err(e) => match e { 103 | MyError::Parse(p) => println!("An error occurred when parsing a command {}", p), 104 | MyError::Http(e) => println!("An HTTP error occurred: {}", e), 105 | MyError::Other(other) => println!("An error occurred {}", other) 106 | } 107 | }; 108 | } 109 | 110 | #[command] 111 | #[description = "Measures the time needed to respond to an interaction"] 112 | async fn timer(ctx: &SlashContext<()>) -> Result { 113 | let start = Instant::now(); 114 | 115 | ctx.interaction_client.create_response( 116 | ctx.interaction.id, 117 | &ctx.interaction.token, 118 | &InteractionResponse { 119 | kind: InteractionResponseType::ChannelMessageWithSource, 120 | data: Some(InteractionResponseData { 121 | content: Some(String::from("")), 122 | ..Default::default() 123 | }) 124 | } 125 | ).await?; 126 | 127 | Ok(ElapsedTime(Instant::now() - start)) 128 | } 129 | -------------------------------------------------------------------------------- /examples/shared-data.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use std::sync::atomic::{AtomicUsize, Ordering}; 4 | use futures_util::StreamExt; 5 | use twilight_gateway::{stream::{self, ShardEventStream}, Config}; 6 | use twilight_http::Client; 7 | use twilight_model::gateway::event::Event; 8 | use twilight_model::gateway::Intents; 9 | use twilight_model::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}; 10 | use twilight_model::id::Id; 11 | use vesper::prelude::*; 12 | 13 | pub struct Shared { 14 | count: AtomicUsize 15 | } 16 | 17 | #[tokio::main] 18 | async fn main() { 19 | let token = env::var("DISCORD_TOKEN").unwrap(); 20 | let application_id = env::var("DISCORD_APPLICATION_ID").unwrap().parse::().unwrap(); 21 | 22 | let http_client = Arc::new(Client::new(token.clone())); 23 | 24 | let config = Config::new(token.clone(), Intents::empty()); 25 | let mut shards = stream::create_recommended( 26 | &http_client, 27 | config, 28 | |_, builder| builder.build() 29 | ).await.unwrap().collect::>(); 30 | 31 | let mut stream = ShardEventStream::new(shards.iter_mut()); 32 | 33 | let shared = Shared { 34 | count: AtomicUsize::new(0) 35 | }; 36 | 37 | // The builder function accepts data as the third argument, this data will then be passed to 38 | // every command and hook. 39 | let framework = Framework::builder(http_client, Id::new(application_id), shared) 40 | .group(|g| { 41 | g.name("count") 42 | .description("Shared state count related commands") 43 | .command(show) 44 | .command(increment_one) 45 | .command(increment_many) 46 | }) 47 | .build(); 48 | 49 | while let Some((_, event)) = stream.next().await { 50 | match event { 51 | Err(error) => { 52 | if error.is_fatal() { 53 | eprintln!("Gateway connection fatally closed, error: {error:?}"); 54 | break; 55 | } 56 | }, 57 | Ok(event) => match event { 58 | Event::Ready(_) => { 59 | // We have to register the commands for them to show in discord. 60 | framework.register_global_commands().await.unwrap(); 61 | }, 62 | Event::InteractionCreate(interaction) => { 63 | framework.process(interaction.0).await; 64 | }, 65 | _ => () 66 | } 67 | } 68 | } 69 | } 70 | 71 | #[command] 72 | #[description = "Shows the current count value"] 73 | async fn show(ctx: &SlashContext) -> DefaultCommandResult { 74 | let current = ctx.data.count.load(Ordering::Relaxed); 75 | 76 | ctx.interaction_client.create_response( 77 | ctx.interaction.id, 78 | &ctx.interaction.token, 79 | &InteractionResponse { 80 | kind: InteractionResponseType::ChannelMessageWithSource, 81 | data: Some(InteractionResponseData { 82 | content: Some(format!("The count number is {current}")), 83 | ..Default::default() 84 | }) 85 | } 86 | ).await?; 87 | 88 | Ok(()) 89 | } 90 | 91 | #[command] 92 | #[description = "Increments the count by one"] 93 | async fn increment_one(ctx: &SlashContext) -> DefaultCommandResult { 94 | ctx.data.count.fetch_add(1, Ordering::Relaxed); 95 | 96 | ctx.interaction_client.create_response( 97 | ctx.interaction.id, 98 | &ctx.interaction.token, 99 | &InteractionResponse { 100 | kind: InteractionResponseType::ChannelMessageWithSource, 101 | data: Some(InteractionResponseData { 102 | content: Some(format!("The count number has been incremented by one")), 103 | ..Default::default() 104 | }) 105 | } 106 | ).await?; 107 | 108 | Ok(()) 109 | } 110 | 111 | #[command] 112 | #[description = "Increments the count by the specified number"] 113 | async fn increment_many( 114 | ctx: &SlashContext, 115 | #[description = "How many numbers to add to count"] many: usize 116 | ) -> DefaultCommandResult 117 | { 118 | ctx.data.count.fetch_add(many, Ordering::Relaxed); 119 | 120 | ctx.interaction_client.create_response( 121 | ctx.interaction.id, 122 | &ctx.interaction.token, 123 | &InteractionResponse { 124 | kind: InteractionResponseType::ChannelMessageWithSource, 125 | data: Some(InteractionResponseData { 126 | content: Some(format!("The count number has been incremented by {many}")), 127 | ..Default::default() 128 | }) 129 | } 130 | ).await?; 131 | 132 | Ok(()) 133 | } 134 | -------------------------------------------------------------------------------- /examples/using-checks.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use futures_util::StreamExt; 4 | use twilight_gateway::{stream::{self, ShardEventStream}, Config}; 5 | use twilight_http::Client; 6 | use twilight_model::gateway::event::Event; 7 | use twilight_model::gateway::Intents; 8 | use twilight_model::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}; 9 | use twilight_model::id::Id; 10 | use vesper::framework::DefaultError; 11 | use vesper::prelude::*; 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | let token = env::var("DISCORD_TOKEN").unwrap(); 16 | let application_id = env::var("DISCORD_APPLICATION_ID").unwrap().parse::().unwrap(); 17 | 18 | let http_client = Arc::new(Client::new(token.clone())); 19 | 20 | let config = Config::new(token.clone(), Intents::empty()); 21 | let mut shards = stream::create_recommended( 22 | &http_client, 23 | config, 24 | |_, builder| builder.build() 25 | ).await.unwrap().collect::>(); 26 | 27 | let mut stream = ShardEventStream::new(shards.iter_mut()); 28 | 29 | let framework = Framework::builder(http_client, Id::new(application_id), ()) 30 | .command(hello) 31 | .build(); 32 | 33 | while let Some((_, event)) = stream.next().await { 34 | match event { 35 | Err(error) => { 36 | if error.is_fatal() { 37 | eprintln!("Gateway connection fatally closed, error: {error:?}"); 38 | break; 39 | } 40 | }, 41 | Ok(event) => match event { 42 | Event::Ready(_) => { 43 | // We have to register the commands for them to show in discord. 44 | framework.register_global_commands().await.unwrap(); 45 | }, 46 | Event::InteractionCreate(interaction) => { 47 | framework.process(interaction.0).await; 48 | }, 49 | _ => () 50 | } 51 | } 52 | } 53 | } 54 | 55 | #[check] 56 | async fn only_guilds(ctx: &SlashContext<()>) -> Result { 57 | // Only pass the check if the command has been executed inside a guild 58 | Ok(ctx.interaction.guild_id.is_some()) 59 | } 60 | 61 | #[check] 62 | async fn other_check(_ctx: &SlashContext<()>) -> Result { 63 | Ok(true) 64 | } 65 | 66 | #[command] 67 | #[description = "Says hello"] 68 | // The `check` attribute accepts a comma separated list of checks, so we can add as many as we want. 69 | // If a single one returns `false`, the command won't execute. If a check has an error and the 70 | // command has a custom error handler, it will handle the error. Otherwise the `after` hook will 71 | // receive the error of the check. 72 | #[checks(only_guilds, other_check)] 73 | async fn hello(ctx: &SlashContext<()>) -> DefaultCommandResult { 74 | ctx.interaction_client.create_response( 75 | ctx.interaction.id, 76 | &ctx.interaction.token, 77 | &InteractionResponse { 78 | kind: InteractionResponseType::ChannelMessageWithSource, 79 | data: Some(InteractionResponseData { 80 | content: Some(String::from("Hello!")), 81 | ..Default::default() 82 | }) 83 | } 84 | ).await?; 85 | 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /examples/using-dedicated-tasks.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use futures_util::StreamExt; 4 | use twilight_gateway::{stream::{self, ShardEventStream}, Config}; 5 | use twilight_http::Client; 6 | use twilight_model::gateway::event::Event; 7 | use twilight_model::gateway::Intents; 8 | use twilight_model::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}; 9 | use twilight_model::id::Id; 10 | use vesper::prelude::*; 11 | 12 | #[tokio::main] 13 | async fn main() { 14 | let token = env::var("DISCORD_TOKEN").unwrap(); 15 | let application_id = env::var("DISCORD_APPLICATION_ID").unwrap().parse::().unwrap(); 16 | 17 | let http_client = Arc::new(Client::new(token.clone())); 18 | 19 | let config = Config::new(token.clone(), Intents::empty()); 20 | let mut shards = stream::create_recommended( 21 | &http_client, 22 | config, 23 | |_, builder| builder.build() 24 | ).await.unwrap().collect::>(); 25 | 26 | let mut stream = ShardEventStream::new(shards.iter_mut()); 27 | 28 | // Wrap the framework in an Arc, so we can share it across tasks. 29 | let framework = Arc::new(Framework::builder(http_client, Id::new(application_id), ()) 30 | .command(state) 31 | .build()); 32 | 33 | while let Some((_, event)) = stream.next().await { 34 | match event { 35 | Err(error) => { 36 | if error.is_fatal() { 37 | eprintln!("Gateway connection fatally closed, error: {error:?}"); 38 | break; 39 | } 40 | }, 41 | Ok(event) => match event { 42 | Event::Ready(_) => { 43 | // We have to register the commands for them to show in discord. 44 | framework.register_global_commands().await.unwrap(); 45 | }, 46 | Event::InteractionCreate(interaction) => { 47 | let framework_clone = Arc::clone(&framework); 48 | tokio::spawn(async move { 49 | framework_clone.process(interaction.0).await; 50 | }); 51 | }, 52 | _ => () 53 | } 54 | } 55 | } 56 | } 57 | 58 | #[command] 59 | #[description = "Says where the client is running"] 60 | async fn state(ctx: &SlashContext<()>) -> DefaultCommandResult { 61 | ctx.interaction_client.create_response( 62 | ctx.interaction.id, 63 | &ctx.interaction.token, 64 | &InteractionResponse { 65 | kind: InteractionResponseType::ChannelMessageWithSource, 66 | data: Some(InteractionResponseData { 67 | content: Some(String::from("Running inside a multithreaded tokio runtime!")), 68 | ..Default::default() 69 | }) 70 | } 71 | ).await?; 72 | 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /examples/using-groups.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use futures_util::StreamExt; 4 | use rand::Rng; 5 | use twilight_gateway::{stream::{self, ShardEventStream}, Config}; 6 | use twilight_http::Client; 7 | use twilight_model::gateway::event::Event; 8 | use twilight_model::gateway::Intents; 9 | use twilight_model::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}; 10 | use twilight_model::id::Id; 11 | use vesper::prelude::*; 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | let token = env::var("DISCORD_TOKEN").unwrap(); 16 | let application_id = env::var("DISCORD_APPLICATION_ID").unwrap().parse::().unwrap(); 17 | 18 | let http_client = Arc::new(Client::new(token.clone())); 19 | 20 | let config = Config::new(token.clone(), Intents::empty()); 21 | let mut shards = stream::create_recommended( 22 | &http_client, 23 | config, 24 | |_, builder| builder.build() 25 | ).await.unwrap().collect::>(); 26 | 27 | let mut stream = ShardEventStream::new(shards.iter_mut()); 28 | 29 | let framework = Framework::builder(http_client, Id::new(application_id), ()) 30 | .group(|group| { 31 | group.name("message") 32 | .description("Message related commands") 33 | // By setting a command here, we specify this is a command group, then multiple 34 | // commands can be registered using the `command` method multiple times. 35 | .command(repeat) 36 | }) 37 | .group(|group| { 38 | group.name("random") 39 | .description("Generates random things") 40 | // Here, by setting another group, we are specifying this is a subcommand group. 41 | .group(|subgroup| { 42 | subgroup.name("integer") 43 | .description("Generates random integers") 44 | .command(number_between) 45 | }) 46 | // We can specify multiple subcommand groups. 47 | .group(|subgroup| { 48 | subgroup.name("char") 49 | .description("Generates random characters") 50 | .command(random_char) 51 | }) 52 | }) 53 | .build(); 54 | 55 | while let Some((_, event)) = stream.next().await { 56 | match event { 57 | Err(error) => { 58 | if error.is_fatal() { 59 | eprintln!("Gateway connection fatally closed, error: {error:?}"); 60 | break; 61 | } 62 | }, 63 | Ok(event) => match event { 64 | Event::Ready(_) => { 65 | // We have to register the commands for them to show in discord. 66 | framework.register_global_commands().await.unwrap(); 67 | }, 68 | Event::InteractionCreate(interaction) => { 69 | framework.process(interaction.0).await; 70 | }, 71 | _ => () 72 | } 73 | } 74 | } 75 | } 76 | 77 | #[command] 78 | #[description = "Repeats your message"] 79 | async fn repeat( 80 | ctx: &SlashContext<()>, 81 | #[description = "Message to repeat"] message: String 82 | ) -> DefaultCommandResult 83 | { 84 | ctx.interaction_client.create_response( 85 | ctx.interaction.id, 86 | &ctx.interaction.token, 87 | &InteractionResponse { 88 | kind: InteractionResponseType::ChannelMessageWithSource, 89 | data: Some(InteractionResponseData { 90 | content: Some(message), 91 | ..Default::default() 92 | }) 93 | } 94 | ).await?; 95 | 96 | Ok(()) 97 | } 98 | 99 | #[command] 100 | #[description = "Responds with a random number between the specified range"] 101 | async fn number_between( 102 | ctx: &SlashContext<()>, 103 | #[description = "The starting number of the range"] start: Range, 104 | #[description = "The end number of the range"] end: Range 105 | ) -> DefaultCommandResult 106 | { 107 | // Range implements deref to the specified type, so it can be used like a normal number. 108 | let num = rand::thread_rng().gen_range(*start..=*end); 109 | 110 | ctx.interaction_client.create_response( 111 | ctx.interaction.id, 112 | &ctx.interaction.token, 113 | &InteractionResponse { 114 | kind: InteractionResponseType::ChannelMessageWithSource, 115 | data: Some(InteractionResponseData { 116 | content: Some(format!("Your number is {num}")), 117 | ..Default::default() 118 | }) 119 | } 120 | ).await?; 121 | 122 | Ok(()) 123 | } 124 | 125 | // By passing the name as an argument we are manually specifying the name of the command, 126 | // if nothing is provided, the function name is used as the command name. 127 | #[command("char")] 128 | #[description = "Generates a random char"] 129 | async fn random_char(ctx: &SlashContext<()>) -> DefaultCommandResult { 130 | let char = rand::random::(); 131 | 132 | ctx.interaction_client.create_response( 133 | ctx.interaction.id, 134 | &ctx.interaction.token, 135 | &InteractionResponse { 136 | kind: InteractionResponseType::ChannelMessageWithSource, 137 | data: Some(InteractionResponseData { 138 | content: Some(format!("Your char is {char}")), 139 | ..Default::default() 140 | }) 141 | } 142 | ).await?; 143 | 144 | Ok(()) 145 | } 146 | -------------------------------------------------------------------------------- /examples/using-hooks.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use futures_util::StreamExt; 4 | use twilight_gateway::{stream::{self, ShardEventStream}, Config}; 5 | use twilight_http::Client; 6 | use twilight_model::gateway::event::Event; 7 | use twilight_model::gateway::Intents; 8 | use twilight_model::http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}; 9 | use twilight_model::id::Id; 10 | use vesper::framework::DefaultError; 11 | use vesper::prelude::*; 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | let token = env::var("DISCORD_TOKEN").unwrap(); 16 | let application_id = env::var("DISCORD_APPLICATION_ID").unwrap().parse::().unwrap(); 17 | 18 | let http_client = Arc::new(Client::new(token.clone())); 19 | 20 | let config = Config::new(token.clone(), Intents::empty()); 21 | let mut shards = stream::create_recommended( 22 | &http_client, 23 | config, 24 | |_, builder| builder.build() 25 | ).await.unwrap().collect::>(); 26 | 27 | let mut stream = ShardEventStream::new(shards.iter_mut()); 28 | 29 | let framework = Framework::builder(http_client, Id::new(application_id), ()) 30 | .command(hello) 31 | .before(before_hook) 32 | .after(after_hook) 33 | .build(); 34 | 35 | while let Some((_, event)) = stream.next().await { 36 | match event { 37 | Err(error) => { 38 | if error.is_fatal() { 39 | eprintln!("Gateway connection fatally closed, error: {error:?}"); 40 | break; 41 | } 42 | }, 43 | Ok(event) => match event { 44 | Event::Ready(_) => { 45 | // We have to register the commands for them to show in discord. 46 | framework.register_global_commands().await.unwrap(); 47 | }, 48 | Event::InteractionCreate(interaction) => { 49 | framework.process(interaction.0).await; 50 | }, 51 | _ => () 52 | } 53 | } 54 | } 55 | } 56 | 57 | #[before] 58 | async fn before_hook(_ctx: &SlashContext<()>, command_name: &str) -> bool { 59 | println!("Before hook executed for command {command_name}"); 60 | // The return type of this function specifies if the actual command should run or not, if `false` 61 | // is returned, then the command won't execute. 62 | true 63 | } 64 | 65 | // The result field will be some only if the command returned no errors or if the command has 66 | // no custom error handler set. 67 | #[after] 68 | async fn after_hook(_ctx: &SlashContext<()>, command_name: &str, result: Option) { 69 | println!("{command_name} finished, returned value: {result:?}"); 70 | } 71 | 72 | #[command] 73 | #[description = "Says hello"] 74 | async fn hello(ctx: &SlashContext<()>) -> DefaultCommandResult { 75 | ctx.interaction_client.create_response( 76 | ctx.interaction.id, 77 | &ctx.interaction.token, 78 | &InteractionResponse { 79 | kind: InteractionResponseType::ChannelMessageWithSource, 80 | data: Some(InteractionResponseData { 81 | content: Some(String::from("Hello!")), 82 | ..Default::default() 83 | }) 84 | } 85 | ).await?; 86 | 87 | Ok(()) 88 | } 89 | 90 | #[error_handler] 91 | async fn handle_error(_ctx: &SlashContext<()>, result: DefaultError) { 92 | println!("Command had an error: {result:?}"); 93 | } 94 | 95 | #[command] 96 | #[description = "Tries to ban the bot itself and raises an error"] 97 | // As we registered here a custom error handler, the after hook will only only have `Some` in the 98 | // result argument if the command execution finishes without raising any exceptions, which in this 99 | // case is only if the command is executed outside of a guild. Otherwise the result argument 100 | // will be `None`, as the error will be consumed at the custom error handler. 101 | #[error_handler(handle_error)] 102 | async fn raises_error(ctx: &SlashContext<()>) -> DefaultCommandResult { 103 | ctx.defer(false).await?; 104 | if !ctx.interaction.is_guild() { 105 | ctx.interaction_client.update_response(&ctx.interaction.token) 106 | .content(Some("This command can only be used in guilds")).unwrap() 107 | .await?; 108 | 109 | return Ok(()) 110 | } 111 | 112 | // Trying to ban a bot by itself results in an error. 113 | ctx.http_client().ban(ctx.interaction.guild_id.unwrap(), Id::new(ctx.application_id.get())) 114 | .await?; 115 | 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /examples/using-modals.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use futures_util::StreamExt; 4 | use twilight_gateway::{stream::{self, ShardEventStream}, Config}; 5 | use twilight_http::Client; 6 | use twilight_model::gateway::event::Event; 7 | use twilight_model::gateway::Intents; 8 | use twilight_model::id::Id; 9 | use vesper::prelude::*; 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | let token = env::var("DISCORD_TOKEN").unwrap(); 14 | let application_id = env::var("DISCORD_APPLICATION_ID").unwrap().parse::().unwrap(); 15 | 16 | let http_client = Arc::new(Client::new(token.clone())); 17 | 18 | let config = Config::new(token.clone(), Intents::empty()); 19 | let mut shards = stream::create_recommended( 20 | &http_client, 21 | config, 22 | |_, builder| builder.build() 23 | ).await.unwrap().collect::>(); 24 | 25 | let mut stream = ShardEventStream::new(shards.iter_mut()); 26 | 27 | let framework = Framework::builder(http_client, Id::new(application_id), ()) 28 | .command(show_modal) 29 | .build(); 30 | 31 | while let Some((_, event)) = stream.next().await { 32 | match event { 33 | Err(error) => { 34 | if error.is_fatal() { 35 | eprintln!("Gateway connection fatally closed, error: {error:?}"); 36 | break; 37 | } 38 | }, 39 | Ok(event) => match event { 40 | Event::Ready(_) => { 41 | // We have to register the commands for them to show in discord. 42 | framework.register_global_commands().await.unwrap(); 43 | }, 44 | Event::InteractionCreate(interaction) => { 45 | framework.process(interaction.0).await; 46 | }, 47 | _ => () 48 | } 49 | } 50 | } 51 | } 52 | 53 | #[command] 54 | #[description = "Shows a modal"] 55 | async fn show_modal(ctx: &SlashContext<()>) -> DefaultCommandResult { 56 | let wait = ctx.create_modal::().await?; 57 | let output = wait.await?; 58 | println!("{output:?}"); 59 | Ok(()) 60 | } 61 | 62 | #[derive(Modal, Debug)] 63 | struct MyModal { 64 | #[modal(placeholder = "My placeholder")] 65 | something: String, 66 | #[modal(label = "Another field", paragraph)] 67 | field_2: Option 68 | } 69 | -------------------------------------------------------------------------------- /vesper-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vesper-macros" 3 | version = "0.13.0" 4 | authors = ["Alvaro <62391364+AlvaroMS25@users.noreply.github.com>"] 5 | edition = "2018" 6 | description = "Procedural macros used by Zephyrus" 7 | readme = "README.md" 8 | repository = "https://github.com/AlvaroMS25/vesper" 9 | license = "MIT" 10 | keywords = ["async", "twilight", "discord", "slash-command"] 11 | categories = ["asynchronous"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | quote = "1" 19 | syn = { version = "2", features = ["full", "derive", "extra-traits"] } 20 | proc-macro2 = ">=1.0.56" 21 | darling = "0.20" -------------------------------------------------------------------------------- /vesper-macros/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alvaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vesper-macros/src/after.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use syn::{parse2, spanned::Spanned, Error, ItemFn, Result, Path}; 3 | use crate::util; 4 | 5 | /// The implementation of after macro, this macro takes the given input, which must be another 6 | /// function and prepares it to be an after hook, wrapping it in a struct and providing a pointer 7 | /// to the actual function 8 | pub fn after(input: TokenStream2) -> Result { 9 | let fun = parse2::(input)?; 10 | let ItemFn { 11 | attrs, 12 | vis, 13 | mut sig, 14 | block, 15 | } = fun; 16 | 17 | match sig.inputs.len() { 18 | c if c != 3 => { 19 | // This hook is expected to have three arguments, a reference to an `SlashContext`, 20 | // a &str indicating the name of the command and the result of a command execution. 21 | return Err(Error::new(sig.inputs.span(), "Expected three arguments")); 22 | } 23 | _ => (), 24 | }; 25 | 26 | // The name of the original function 27 | let ident = sig.ident.clone(); 28 | // This is the name the given function will have after this macro's execution 29 | let fn_ident = quote::format_ident!("_{}", &ident); 30 | sig.ident = fn_ident.clone(); 31 | 32 | /* 33 | Check the return of the function, returning if it does not match, this function is required 34 | to return `()` 35 | */ 36 | util::check_return_type(&sig.output, quote::quote!(()))?; 37 | 38 | let result_type = util::get_path(&util::get_pat(sig.inputs.iter().nth(2).unwrap())?.ty, false)?; 39 | let returnable = util::get_returnable_trait(); 40 | let optional = parse2::(quote::quote!(::vesper::extract::Optional))?; 41 | 42 | let ty = util::get_context_type(&sig, true)?; 43 | // Get the hook macro so we can fit the function into a normal fn pointer 44 | let hook = util::get_hook_macro(); 45 | let path = quote::quote!(::vesper::hook::AfterHook); 46 | 47 | Ok(quote::quote! { 48 | pub fn #ident() 49 | -> #path< 50 | #ty, 51 | <<#result_type as #optional>::Inner as #returnable>::Ok, 52 | <<#result_type as #optional>::Inner as #returnable>::Err 53 | > { 54 | #path(#fn_ident) 55 | } 56 | 57 | #[#hook] 58 | #(#attrs)* 59 | #vis #sig #block 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /vesper-macros/src/autocomplete.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{TokenStream as TokenStream2}; 2 | use syn::{ 3 | parse2, spanned::Spanned, Error, ItemFn, Result 4 | }; 5 | use crate::util; 6 | 7 | pub fn autocomplete(input: TokenStream2) -> Result { 8 | let mut fun = parse2::(input)?; 9 | 10 | if fun.sig.inputs.len() != 1 { 11 | return Err(Error::new( 12 | fun.sig.inputs.span(), 13 | "Autocomplete hook must have as parameters an AutocompleteContext", 14 | )); 15 | } 16 | 17 | let data_type = util::get_context_type(&fun.sig, false)?; 18 | util::set_context_lifetime(&mut fun.sig)?; 19 | let hook = util::get_hook_macro(); 20 | let path = quote::quote!(::vesper::hook::AutocompleteHook); 21 | let ident = fun.sig.ident.clone(); 22 | let fn_ident = quote::format_ident!("_{}", ident); 23 | fun.sig.ident = fn_ident.clone(); 24 | 25 | Ok(quote::quote! { 26 | pub fn #ident() -> #path<#data_type> { 27 | #path(#fn_ident) 28 | } 29 | 30 | #[#hook] 31 | #fun 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /vesper-macros/src/before.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use syn::{parse2, spanned::Spanned, Error, ItemFn, Result}; 3 | use crate::util; 4 | 5 | /// The implementation of before macro, this macro takes the given input, which must be another 6 | /// function and prepares it to be an before hook, wrapping it in a struct and providing a pointer 7 | /// to the actual function 8 | pub fn before(input: TokenStream2) -> Result { 9 | let fun = parse2::(input)?; 10 | let ItemFn { 11 | attrs, 12 | vis, 13 | mut sig, 14 | block, 15 | } = fun; 16 | 17 | if sig.inputs.len() > 2 { 18 | // This hook is expected to have a `&SlashContext` and a `&str` parameter. 19 | return Err(Error::new( 20 | sig.inputs.span(), 21 | "Function parameter must only be &SlashContext and &str", 22 | )); 23 | } 24 | 25 | // The name of the original macro 26 | let ident = sig.ident.clone(); 27 | // The name the function will have after this macro's execution 28 | let fn_ident = quote::format_ident!("_{}", &ident); 29 | sig.ident = fn_ident.clone(); 30 | /* 31 | Check the return of the function, returning if it does not match, this function is required 32 | to return a `bool` indicating if the recognised command should be executed or not 33 | */ 34 | util::check_return_type(&sig.output, quote::quote!(bool))?; 35 | 36 | let ty = util::get_context_type(&sig, true)?; 37 | // Get the hook macro so we can fit the function into a normal fn pointer 38 | let hook = util::get_hook_macro(); 39 | let path = quote::quote!(::vesper::hook::BeforeHook); 40 | 41 | Ok(quote::quote! { 42 | pub fn #ident() -> #path<#ty> { 43 | #path(#fn_ident) 44 | } 45 | 46 | #[#hook] 47 | #(#attrs)* 48 | #vis #sig #block 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /vesper-macros/src/check.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use syn::{parse2, spanned::Spanned, Error, ItemFn, Result}; 3 | use crate::util; 4 | 5 | pub fn check(input: TokenStream2) -> Result { 6 | let fun = parse2::(input)?; 7 | let ItemFn { 8 | attrs, 9 | vis, 10 | mut sig, 11 | block, 12 | } = fun; 13 | 14 | if sig.inputs.len() > 1 { 15 | // This hook is expected to have a single `&SlashContext` parameter. 16 | return Err(Error::new( 17 | sig.inputs.span(), 18 | "Function parameter must only be &SlashContext", 19 | )); 20 | } 21 | 22 | // The name of the original macro 23 | let ident = sig.ident.clone(); 24 | // The name the function will have after this macro's execution 25 | let fn_ident = quote::format_ident!("_{}", &ident); 26 | sig.ident = fn_ident.clone(); 27 | 28 | let return_type = util::get_return_type(&sig)?; 29 | let returnable = util::get_returnable_trait(); 30 | 31 | let ty = util::get_context_type(&sig, true)?; 32 | // Get the hook macro so we can fit the function into a normal fn pointer 33 | let hook = util::get_hook_macro(); 34 | let path = quote::quote!(::vesper::hook::CheckHook); 35 | 36 | Ok(quote::quote! { 37 | pub fn #ident() -> #path<#ty, <#return_type as #returnable>::Err> { 38 | #path(#fn_ident) 39 | } 40 | 41 | #[#hook] 42 | #(#attrs)* 43 | #vis #sig #block 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /vesper-macros/src/command/argument.rs: -------------------------------------------------------------------------------- 1 | use crate::extractors::{Either, FixedList, FunctionPath, Map}; 2 | use crate::optional::Optional; 3 | use crate::util; 4 | use darling::FromMeta; 5 | use darling::export::NestedMeta; 6 | use proc_macro2::{Ident, TokenStream}; 7 | use quote::ToTokens; 8 | use syn::spanned::Spanned; 9 | use syn::{FnArg, Type, LitStr, Error}; 10 | use crate::extractors::function_closure::FunctionOrClosure; 11 | 12 | #[derive(FromMeta)] 13 | pub struct ArgumentAttributes { 14 | #[darling(default)] 15 | pub localized_names: Option>, 16 | #[darling(default)] 17 | pub localized_names_fn: Option>>, 18 | /// The description of this argument, this is a required field parsed with `#[description]` 19 | /// attribute. 20 | /// 21 | /// This macro can be used two ways: 22 | /// 23 | /// - List way: #[description("Some description")] 24 | /// 25 | /// - Named value way: #[description = "Some description"] 26 | /// 27 | /// e.g.: fn a(#[description = "some here"] arg: String), being the fields inside `description` 28 | /// this field 29 | #[darling(default)] 30 | pub description: Option>>, 31 | #[darling(default)] 32 | pub localized_descriptions: Option>, 33 | #[darling(default)] 34 | pub localized_descriptions_fn: Option>>, 35 | /// The renaming of this argument, if this option is not specified, the original name will be 36 | /// used to parse the argument and register the command in discord 37 | #[darling(rename = "rename")] 38 | pub renaming: Option>>, 39 | pub autocomplete: Optional>>, 40 | #[darling(default)] 41 | pub skip: bool 42 | } 43 | 44 | /// A command argument, and all its details, skipping the first one, which must be an `SlashContext` 45 | /// reference. 46 | pub struct Argument { 47 | /// The name of this argument at function definition. 48 | /// 49 | /// e.g.: fn a(arg: String), being `arg` this field. 50 | pub ident: Ident, 51 | /// The type of this argument. 52 | /// 53 | /// e.g.: fn a(arg: String), being `String` this field. 54 | pub ty: Box, 55 | /// Argument attributes, only present if the command is a `chat` command. 56 | pub attributes: Option, 57 | pub chat_command: bool 58 | } 59 | 60 | impl Argument { 61 | /// Creates a new [argument](self::Argument) and parses the required fields 62 | pub fn new( 63 | mut arg: FnArg, 64 | chat_command: bool 65 | ) -> darling::Result { 66 | let pat = util::get_pat_mut(&mut arg)?; 67 | let ident = util::get_ident(&pat.pat)?; 68 | let ty = pat.ty.clone(); 69 | 70 | let attributes = pat.attrs 71 | .drain(..) 72 | .map(|attribute| attribute.meta) 73 | .map(NestedMeta::Meta) 74 | .collect::>(); 75 | 76 | let this = Self { 77 | ident, 78 | ty, 79 | attributes: if chat_command { 80 | Some(ArgumentAttributes::from_list(attributes.as_slice())?) 81 | } else { 82 | None 83 | }, 84 | chat_command 85 | }; 86 | 87 | if chat_command 88 | && !this.attributes.as_ref().map(|a| a.skip).unwrap_or(false) 89 | && this.attributes.as_ref().unwrap() 90 | .description.as_ref().map(|d| d.inner().is_empty()).unwrap_or(true) 91 | { 92 | return Err(Error::new( 93 | arg.span(), 94 | "Missing `description`" 95 | )).map_err(From::from); 96 | } 97 | 98 | Ok(this) 99 | } 100 | } 101 | 102 | impl ToTokens for Argument { 103 | fn to_tokens(&self, tokens: &mut TokenStream) { 104 | if self.attributes.as_ref().map(|a| a.skip).unwrap_or(false) || !self.chat_command { 105 | return; 106 | } 107 | let attributes = self.attributes.as_ref().unwrap(); 108 | 109 | let des = attributes.description.as_ref().unwrap().inner(); 110 | let ty = &self.ty; 111 | let argument_path = quote::quote!(::vesper::argument::CommandArgument); 112 | 113 | let name = match &attributes.renaming { 114 | Some(rename) => rename.inner().clone(), 115 | None => self.ident.to_string(), 116 | }; 117 | 118 | let add_localized_names = attributes.localized_names.as_ref().map(|map| { 119 | let localized_names = map.pairs(); 120 | quote::quote!(.localized_names(vec![#(#localized_names),*])) 121 | }); 122 | 123 | let add_localized_names_fn = attributes.localized_names_fn.as_ref().map(|fun| { 124 | let fun = fun.inner(); 125 | quote::quote!(.localized_names_fn(#fun)) 126 | }); 127 | 128 | let add_localized_descriptions = attributes.localized_descriptions.as_ref().map(|map| { 129 | let localized_descriptions = map.pairs(); 130 | quote::quote!(.localized_descriptions(vec![#(#localized_descriptions),*])) 131 | }); 132 | 133 | let add_localized_descriptions_fn = attributes.localized_descriptions_fn.as_ref().map(|fun| { 134 | let fun = fun.inner(); 135 | quote::quote!(.localized_descriptions_fn(#fun)) 136 | }); 137 | 138 | let autocomplete = attributes.autocomplete.as_ref().map(|either| { 139 | let inner = either.inner(); 140 | quote::quote!(#inner()) 141 | }); 142 | 143 | tokens.extend(quote::quote! { 144 | .add_argument(#argument_path::new::<#ty>( 145 | #name, 146 | #des, 147 | #autocomplete 148 | ) 149 | #add_localized_names 150 | #add_localized_names_fn 151 | #add_localized_descriptions 152 | #add_localized_descriptions_fn 153 | ) 154 | }); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /vesper-macros/src/command/details.rs: -------------------------------------------------------------------------------- 1 | use darling::{FromMeta, export::NestedMeta}; 2 | use proc_macro2::TokenStream as TokenStream2; 3 | use quote::ToTokens; 4 | use syn::spanned::Spanned; 5 | use syn::{Token, Meta, parse2, Error, LitStr}; 6 | use syn::parse::Parse; 7 | use syn::{Attribute, Result}; 8 | use syn::punctuated::Punctuated; 9 | 10 | use crate::extractors::{Either, FixedList, FunctionPath, Ident, List, Map}; 11 | use crate::extractors::function_closure::FunctionOrClosure; 12 | 13 | #[derive(Default, FromMeta)] 14 | /// The details of a given command 15 | pub struct CommandDetails { 16 | #[darling(skip, default)] 17 | pub input_options: InputOptions, 18 | #[darling(default)] 19 | pub localized_names: Option>, 20 | #[darling(default)] 21 | pub localized_names_fn: Option>>, 22 | /// The description of this command 23 | pub description: Either>, 24 | #[darling(default)] 25 | pub localized_descriptions: Option>, 26 | #[darling(default)] 27 | pub localized_descriptions_fn: Option>>, 28 | #[darling(default)] 29 | pub required_permissions: Option>, 30 | #[darling(default)] 31 | pub checks: Either, Punctuated>, 32 | #[darling(default)] 33 | pub error_handler: Option>>, 34 | #[darling(default)] 35 | pub nsfw: bool, 36 | #[darling(default)] 37 | pub only_guilds: bool 38 | } 39 | 40 | impl CommandDetails { 41 | pub fn parse(input_options: InputOptions, attrs: &mut Vec) -> Result { 42 | let meta = attrs 43 | .drain(..) 44 | .map(|item| item.meta) 45 | .map(NestedMeta::Meta) 46 | .collect::>(); 47 | 48 | let mut this = Self::from_list(meta.as_slice())?; 49 | 50 | this.input_options = input_options; 51 | Ok(this) 52 | } 53 | } 54 | 55 | impl ToTokens for CommandDetails { 56 | fn to_tokens(&self, tokens: &mut TokenStream2) { 57 | let options = &self.input_options; 58 | tokens.extend(quote::quote!(#options)); 59 | 60 | if let Some(localized_names) = &self.localized_names { 61 | let localized_names = localized_names.pairs(); 62 | tokens.extend(quote::quote!(.localized_names(vec![#(#localized_names),*]))) 63 | } 64 | 65 | if let Some(fun) = &self.localized_names_fn { 66 | let fun = fun.inner(); 67 | tokens.extend(quote::quote!(.localized_names_fn(#fun))); 68 | } 69 | 70 | let d = self.description.inner(); 71 | tokens.extend(quote::quote!(.description(#d))); 72 | 73 | if let Some(localized_descriptions) = &self.localized_descriptions { 74 | let localized_descriptions = localized_descriptions.pairs(); 75 | tokens.extend(quote::quote!(.localized_descriptions(vec![#(#localized_descriptions),*]))); 76 | } 77 | 78 | if let Some(fun) = &self.localized_descriptions_fn { 79 | let fun = fun.inner(); 80 | tokens.extend(quote::quote!(.localized_descriptions_fn(#fun))); 81 | } 82 | 83 | if let Some(permissions) = &self.required_permissions { 84 | let mut permission_stream = TokenStream2::new(); 85 | 86 | for (index, permission) in permissions.iter().enumerate() { 87 | if index == 0 || permissions.len() == 1 { 88 | permission_stream 89 | .extend(quote::quote!(vesper::twilight_exports::Permissions::#permission)) 90 | } else { 91 | permission_stream.extend( 92 | quote::quote!( | vesper::twilight_exports::Permissions::#permission), 93 | ) 94 | } 95 | } 96 | 97 | tokens.extend(quote::quote!(.required_permissions(#permission_stream))); 98 | } 99 | 100 | let mut checks = Vec::new(); 101 | self.checks.map_1( 102 | &mut checks, 103 | |checks, a| checks.extend(a.iter().cloned()), 104 | |checks, b| checks.extend(b.iter().cloned()) 105 | ); 106 | 107 | tokens.extend(quote::quote! { 108 | .checks(vec![#(#checks()),*]) 109 | }); 110 | 111 | if let Some(error_handler) = &self.error_handler { 112 | let error_handler = error_handler.inner(); 113 | tokens.extend(quote::quote!(.error_handler(#error_handler()))); 114 | } 115 | 116 | let nsfw = self.nsfw; 117 | let only_guilds = self.only_guilds; 118 | 119 | tokens.extend(quote::quote!( 120 | .nsfw(#nsfw) 121 | .only_guilds(#only_guilds) 122 | )); 123 | } 124 | } 125 | 126 | #[derive(Default, FromMeta)] 127 | pub struct InputOptions { 128 | #[darling(default)] 129 | pub chat: bool, 130 | #[darling(default)] 131 | pub message: bool, 132 | #[darling(default)] 133 | pub user: bool, 134 | #[darling(default)] 135 | pub name: String 136 | } 137 | 138 | impl InputOptions { 139 | pub fn new(stream: TokenStream2, ident: &syn::Ident) -> Result { 140 | let stream_empty = stream.is_empty(); 141 | let stream_clone = stream.clone(); 142 | let span = stream.span(); 143 | let meta = match parse2::(stream) { 144 | Ok(m) => m.0, 145 | Err(_) if !stream_empty => { 146 | return Ok(Self { 147 | chat: true, 148 | name: parse2::(stream_clone)?.value(), 149 | ..Default::default() 150 | }) 151 | }, 152 | Err(e) => return Err(e) 153 | }; 154 | 155 | let meta = meta.into_iter() 156 | .map(NestedMeta::Meta) 157 | .collect::>(); 158 | 159 | let mut this = Self::from_list(&meta)?; 160 | 161 | if !(this.chat || this.message || this.user) { 162 | this.chat = true; 163 | } 164 | 165 | if this.name.is_empty() { 166 | this.name = ident.to_string(); 167 | } 168 | 169 | if !(this.chat ^ this.message ^ this.user) || (this.chat && this.message && this.user) { 170 | return Err(Error::new( 171 | span, 172 | "Only one of `chat`, `message` or `user` can be selected" 173 | )); 174 | } 175 | 176 | Ok(this) 177 | } 178 | } 179 | 180 | impl ToTokens for InputOptions { 181 | fn to_tokens(&self, tokens: &mut TokenStream2) { 182 | let name = &self.name; 183 | tokens.extend(quote::quote!(.name(#name))); 184 | 185 | if self.chat { 186 | return; 187 | } 188 | 189 | 190 | let kind = if self.user { 191 | quote::quote!(::vesper::twilight_exports::CommandType::User) 192 | } else if self.message { 193 | quote::quote!(::vesper::twilight_exports::CommandType::Message) 194 | } else { 195 | unreachable!() 196 | }; 197 | 198 | tokens.extend(quote::quote!(.kind(#kind))); 199 | } 200 | } 201 | 202 | pub struct MetaListParser(pub Punctuated); 203 | 204 | impl Parse for MetaListParser { 205 | fn parse(input: syn::parse::ParseStream) -> Result { 206 | Ok(Self(input.call(Punctuated::parse_terminated)?)) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /vesper-macros/src/command/mod.rs: -------------------------------------------------------------------------------- 1 | mod argument; 2 | mod details; 3 | 4 | use proc_macro2::{Ident, TokenStream as TokenStream2}; 5 | use syn::{parse2, spanned::Spanned, Block, Error, ItemFn, Result, Signature, Type}; 6 | use {argument::Argument, details::CommandDetails}; 7 | use crate::util; 8 | 9 | use self::details::InputOptions; 10 | 11 | /// The implementation of the command macro, this macro modifies the provided function body to allow 12 | /// parsing all function arguments and wraps it into a command struct, registering all command names, 13 | /// types and descriptions. 14 | pub fn command(macro_attrs: TokenStream2, input: TokenStream2) -> Result { 15 | let fun = parse2::(input)?; 16 | 17 | let ItemFn { 18 | mut attrs, 19 | vis, 20 | mut sig, 21 | mut block, 22 | } = fun; 23 | 24 | if sig.inputs.is_empty() { 25 | // The function must have at least one argument, which must be an `SlashContext` 26 | return Err(Error::new( 27 | sig.inputs.span(), 28 | "Expected at least SlashContext as a parameter", 29 | )); 30 | } 31 | 32 | let input_options = InputOptions::new(macro_attrs, &sig.ident)?; 33 | 34 | // The name of the function 35 | let ident = sig.ident.clone(); 36 | // The name the function will have after macro execution 37 | let fn_ident = quote::format_ident!("_{}", &sig.ident); 38 | sig.ident = fn_ident.clone(); 39 | 40 | let (context_ident, context_type) = get_context_type_and_ident(&sig)?; 41 | let output = util::get_return_type(&sig)?; 42 | let returnable = util::get_returnable_trait(); 43 | 44 | // Get the hook macro so we can fit the function into a normal fn pointer 45 | let extract_output = util::get_hook_macro(); 46 | let command_path = util::get_command_path(); 47 | 48 | let args = parse_arguments( 49 | &mut sig, 50 | &mut block, 51 | context_ident, 52 | input_options.chat 53 | )?; 54 | let opts = CommandDetails::parse(input_options, &mut attrs)?; 55 | 56 | Ok(quote::quote! { 57 | pub fn #ident() -> #command_path<#context_type, <#output as #returnable>::Ok, <#output as #returnable>::Err> { 58 | #command_path::new(#fn_ident) 59 | #opts 60 | #(#args)* 61 | } 62 | 63 | #[#extract_output] 64 | #(#attrs)* 65 | #vis #sig #block 66 | }) 67 | } 68 | 69 | /// Prepares the given function to parse the required arguments 70 | pub fn parse_arguments( 71 | sig: &mut Signature, 72 | block: &mut Block, 73 | ctx_ident: Ident, 74 | chat_command: bool 75 | ) -> Result> { 76 | let mut arguments = Vec::new(); 77 | while sig.inputs.len() > 1 { 78 | arguments.push(Argument::new( 79 | sig.inputs.pop().unwrap().into_value(), 80 | chat_command 81 | )?); 82 | } 83 | 84 | arguments.reverse(); 85 | 86 | let (names, types, renames) = ( 87 | arguments.iter().map(|s| &s.ident).collect::>(), 88 | arguments.iter().map(|s| &s.ty).collect::>(), 89 | arguments 90 | .iter() 91 | .map(|s| { 92 | if let Some(renaming) = s.attributes.as_ref().map(|a| a.renaming.as_ref()).flatten() { 93 | renaming.inner().clone() 94 | } else { 95 | s.ident.to_string() 96 | } 97 | }) 98 | .collect::>(), 99 | ); 100 | 101 | if !names.is_empty() { 102 | // The original block of the function 103 | let b = █ 104 | 105 | // Modify the block to parse arguments 106 | *block = parse2(quote::quote! {{ 107 | let (#(#names),*) = { 108 | let mut __options = ::vesper::iter::DataIterator::new(#ctx_ident); 109 | 110 | #(let #names = 111 | __options.named_parse::<#types>(#renames).await?;)* 112 | 113 | if __options.len() > 0 { 114 | return Err( 115 | ::vesper::prelude::ParseError::StructureMismatch("Too many arguments received".to_string()).into() 116 | ); 117 | } 118 | 119 | (#(#names),*) 120 | }; 121 | 122 | #b 123 | }})?; 124 | } 125 | 126 | Ok(arguments) 127 | } 128 | 129 | 130 | /// Gets the identifier and the type of the first argument of a function, which must be an 131 | /// `SlashContext` 132 | pub fn get_context_type_and_ident(sig: &Signature) -> Result<(Ident, Type)> { 133 | let arg = match sig.inputs.iter().next() { 134 | None => { 135 | return Err(Error::new( 136 | sig.inputs.span(), 137 | "Expected SlashContext as first parameter", 138 | )) 139 | } 140 | Some(c) => c, 141 | }; 142 | 143 | let ctx_ident = util::get_ident(&util::get_pat(arg)?.pat)?; 144 | 145 | let ty = util::get_bracketed_generic(arg, true, |ty| { 146 | if let Type::Infer(_) = ty { 147 | Err(Error::new( 148 | sig.inputs.span(), 149 | "SlashContext must have a known type", 150 | )) 151 | } else { 152 | Ok(ty.clone()) 153 | } 154 | })?; 155 | 156 | let ty = match ty { 157 | None => Err(Error::new(arg.span(), "SlashContext type must be set")), 158 | Some(ty) => Ok(ty), 159 | }?; 160 | 161 | Ok((ctx_ident, ty)) 162 | } 163 | -------------------------------------------------------------------------------- /vesper-macros/src/error_handler.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use syn::{parse2, spanned::Spanned, Error, ItemFn, Result}; 3 | use crate::util; 4 | 5 | pub fn error_handler(input: TokenStream2) -> Result { 6 | let fun = parse2::(input)?; 7 | let ItemFn { 8 | attrs, 9 | vis, 10 | mut sig, 11 | block, 12 | } = fun; 13 | 14 | match sig.inputs.len() { 15 | c if c != 2 => { 16 | // This hook is expected to have three arguments, a reference to an `SlashContext`, 17 | // a &str indicating the name of the command and the result of a command execution. 18 | return Err(Error::new(sig.inputs.span(), "Expected two arguments")); 19 | } 20 | _ => (), 21 | }; 22 | 23 | // The name of the original function 24 | let ident = sig.ident.clone(); 25 | // This is the name the given function will have after this macro's execution 26 | let fn_ident = quote::format_ident!("_{}", &ident); 27 | sig.ident = fn_ident.clone(); 28 | 29 | /* 30 | Check the return of the function, returning if it does not match, this function is required 31 | to return `()` 32 | */ 33 | util::check_return_type(&sig.output, quote::quote!(()))?; 34 | 35 | let error_type = util::get_path(&util::get_pat(sig.inputs.iter().nth(1).unwrap())?.ty, false)?; 36 | 37 | let ty = util::get_context_type(&sig, true)?; 38 | // Get the hook macro so we can fit the function into a normal fn pointer 39 | let hook = util::get_hook_macro(); 40 | let path = quote::quote!(::vesper::hook::ErrorHandlerHook); 41 | 42 | Ok(quote::quote! { 43 | pub fn #ident() -> #path<#ty, #error_type> { 44 | #path(#fn_ident) 45 | } 46 | 47 | #[#hook] 48 | #(#attrs)* 49 | #vis #sig #block 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /vesper-macros/src/extractors/closure.rs: -------------------------------------------------------------------------------- 1 | use darling::{Error, FromMeta}; 2 | use proc_macro2::TokenStream; 3 | use quote::ToTokens; 4 | use syn::{Expr, ExprClosure}; 5 | use syn::parse::{Parse, ParseStream}; 6 | 7 | #[derive(Clone)] 8 | pub struct Closure(pub ExprClosure); 9 | 10 | impl FromMeta for Closure { 11 | fn from_expr(expr: &Expr) -> darling::Result { 12 | match expr { 13 | Expr::Closure(closure) => Ok(Self(closure.clone())), 14 | _ => Err(Error::unexpected_expr_type(expr)) 15 | }.map_err(|e| e.with_span(expr)) 16 | } 17 | } 18 | 19 | impl ToTokens for Closure { 20 | fn to_tokens(&self, tokens: &mut TokenStream) { 21 | self.0.to_tokens(tokens) 22 | } 23 | } 24 | 25 | impl Parse for Closure { 26 | fn parse(input: ParseStream) -> syn::Result { 27 | Ok(Self(ExprClosure::parse(input)?)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vesper-macros/src/extractors/either.rs: -------------------------------------------------------------------------------- 1 | use darling::{FromMeta, Result, export::NestedMeta, error::Accumulator}; 2 | use quote::ToTokens; 3 | use syn::parse::{Parse, ParseStream, discouraged::Speculative}; 4 | 5 | use super::FixedList; 6 | 7 | macro_rules! try_both { 8 | (@inner $fun:ident, [$($args:ident),* $(,)?]) => {{ 9 | let mut accumulator = Accumulator::default(); 10 | if let Some(parsed) = accumulator.handle(A::$fun($($args),*)) { 11 | accumulator.finish().unwrap(); // Since we are here, we know the accumulator is empty 12 | Ok(Either::Left(parsed)) 13 | } else { 14 | let parsed = accumulator.handle(B::$fun($($args),*)); 15 | if let Some(parsed) = parsed { 16 | let _ = accumulator.finish(); // Discard previous error. 17 | Ok(Either::Right(parsed)) 18 | } else { 19 | // If we are here, both parsing failed, so the accumulator will return an error 20 | // and exit the function. 21 | accumulator.finish()?; 22 | unreachable!() 23 | } 24 | } 25 | }}; 26 | ($fun:ident, $($args:ident),* $(,)?) => { 27 | try_both!(@inner $fun, [$($args),*]) 28 | }; 29 | ($fun:ident) => { 30 | try_both!(@inner $fun, []) 31 | } 32 | } 33 | 34 | #[derive(Clone)] 35 | pub enum Either { 36 | Left(A), 37 | Right(B) 38 | } 39 | 40 | impl Default for Either { 41 | fn default() -> Self { 42 | Self::Left(A::default()) 43 | } 44 | } 45 | 46 | impl FromMeta for Either { 47 | fn from_nested_meta(item: &NestedMeta) -> Result { 48 | try_both!(from_nested_meta, item) 49 | } 50 | 51 | fn from_meta(item: &syn::Meta) -> Result { 52 | try_both!(from_meta, item) 53 | } 54 | 55 | fn from_none() -> Option { 56 | let a = A::from_none(); 57 | a 58 | .map(Either::Left) 59 | .or_else(|| B::from_none().map(Either::Right)) 60 | } 61 | 62 | fn from_word() -> Result { 63 | try_both!(from_word) 64 | } 65 | 66 | fn from_list(items: &[NestedMeta]) -> Result { 67 | try_both!(from_list, items) 68 | } 69 | 70 | fn from_value(value: &syn::Lit) -> Result { 71 | try_both!(from_value, value) 72 | } 73 | 74 | fn from_expr(expr: &syn::Expr) -> Result { 75 | try_both!(from_expr, expr) 76 | } 77 | 78 | fn from_char(value: char) -> Result { 79 | try_both!(from_char, value) 80 | } 81 | 82 | fn from_string(value: &str) -> Result { 83 | try_both!(from_string, value) 84 | } 85 | 86 | fn from_bool(value: bool) -> Result { 87 | try_both!(from_bool, value) 88 | } 89 | } 90 | 91 | impl Parse for Either 92 | where 93 | A: Parse, 94 | B: Parse 95 | { 96 | fn parse(input: ParseStream) -> syn::Result { 97 | let fork = input.fork(); 98 | 99 | if let Ok(parsed) = fork.parse::() { 100 | input.advance_to(&fork); 101 | 102 | Ok(Either::Left(parsed)) 103 | } else { 104 | Ok(Either::Right(input.parse::()?)) 105 | } 106 | } 107 | } 108 | 109 | impl Either 110 | where 111 | for<'a> &'a A: IntoIterator, 112 | for<'a> &'a B: IntoIterator 113 | { 114 | #[allow(dead_code)] 115 | pub fn iter_ref<'a>(&'a self) -> Box + 'a> { 116 | match &self { 117 | Either::Left(a) => Box::new(a.into_iter()), 118 | Either::Right(b) => Box::new(b.into_iter()) 119 | } 120 | } 121 | } 122 | 123 | impl Either 124 | where 125 | for<'a> &'a mut A: IntoIterator, 126 | for<'a> &'a mut B: IntoIterator 127 | { 128 | #[allow(dead_code)] 129 | pub fn iter_mut<'a>(&'a mut self) -> Box + 'a> { 130 | match self { 131 | Either::Left(a) => Box::new(a.into_iter()), 132 | Either::Right(b) => Box::new(b.into_iter()) 133 | } 134 | } 135 | } 136 | 137 | impl Either 138 | where 139 | A: IntoIterator + 'static, 140 | B: IntoIterator + 'static 141 | { 142 | #[allow(dead_code)] 143 | pub fn into_iter(self) -> Box> { 144 | match self { 145 | Either::Left(a) => Box::new(a.into_iter()), 146 | Either::Right(b) => Box::new(b.into_iter()) 147 | } 148 | } 149 | } 150 | 151 | impl Either> { 152 | pub fn inner(&self) -> &A { 153 | match self { 154 | Self::Left(a) => a, 155 | Self::Right(list) => &list.inner[0] 156 | } 157 | } 158 | } 159 | 160 | impl Either { 161 | pub fn map_1(&self, data: &mut T, mut f1: F1, mut f2: F2) -> R 162 | where 163 | F1: FnMut(&mut T, &A) -> R, 164 | F2: FnMut(&mut T, &B) -> R 165 | { 166 | match self { 167 | Self::Left(a) => f1(data, a), 168 | Self::Right(b) => f2(data, b) 169 | } 170 | } 171 | } 172 | 173 | impl ToTokens for Either { 174 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 175 | match self { 176 | Self::Left(a) => ::to_tokens(a, tokens), 177 | Self::Right(b) => ::to_tokens(b, tokens) 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /vesper-macros/src/extractors/function_closure.rs: -------------------------------------------------------------------------------- 1 | use darling::ast::NestedMeta; 2 | use darling::FromMeta; 3 | use proc_macro2::TokenStream; 4 | use quote::ToTokens; 5 | use syn::Expr; 6 | use syn::parse::{Parse, ParseStream}; 7 | use crate::extractors::{Either, FunctionPath}; 8 | use crate::extractors::closure::Closure; 9 | 10 | #[derive(Clone)] 11 | pub struct FunctionOrClosure(Either); 12 | 13 | impl ToTokens for FunctionOrClosure { 14 | fn to_tokens(&self, tokens: &mut TokenStream) { 15 | self.0.to_tokens(tokens) 16 | } 17 | } 18 | 19 | impl FromMeta for FunctionOrClosure { 20 | fn from_nested_meta(item: &NestedMeta) -> darling::Result { 21 | Ok(Self(FromMeta::from_nested_meta(item)?)) 22 | } 23 | 24 | fn from_expr(expr: &Expr) -> darling::Result { 25 | Ok(Self(FromMeta::from_expr(expr)?)) 26 | } 27 | } 28 | 29 | impl Parse for FunctionOrClosure { 30 | fn parse(input: ParseStream) -> syn::Result { 31 | Ok(Self(Parse::parse(input)?)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /vesper-macros/src/extractors/function_path.rs: -------------------------------------------------------------------------------- 1 | use darling::ast::NestedMeta; 2 | use darling::{Error, FromMeta}; 3 | use quote::ToTokens; 4 | use syn::{Expr, ExprPath}; 5 | use syn::parse::{Parse, ParseStream}; 6 | 7 | use super::{Either, Ident}; 8 | 9 | #[derive(Clone)] 10 | pub struct FunctionPath(Either); 11 | 12 | impl ToTokens for FunctionPath { 13 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 14 | self.0.to_tokens(tokens) 15 | } 16 | } 17 | 18 | impl FromMeta for FunctionPath { 19 | fn from_nested_meta(item: &NestedMeta) -> darling::Result { 20 | Ok(Self(FromMeta::from_nested_meta(item)?)) 21 | } 22 | 23 | fn from_expr(expr: &Expr) -> darling::Result { 24 | match expr { 25 | Expr::Path(path) => Ok(Self(Either::Right(path.clone()))), 26 | _ => Err(Error::unexpected_expr_type(expr)) 27 | }.map_err(|e| e.with_span(expr)) 28 | } 29 | } 30 | 31 | impl Parse for FunctionPath { 32 | fn parse(input: ParseStream) -> syn::Result { 33 | Ok(Self(Either::Right(Parse::parse(input)?))) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vesper-macros/src/extractors/ident.rs: -------------------------------------------------------------------------------- 1 | use darling::FromMeta; 2 | use quote::ToTokens; 3 | use syn::Meta; 4 | 5 | #[derive(Clone)] 6 | pub struct Ident(syn::Ident); 7 | 8 | impl FromMeta for Ident { 9 | fn from_meta(item: &Meta) -> darling::Result { 10 | match item { 11 | Meta::Path(p) if p.get_ident().is_some() => Ok(Self(p.get_ident().unwrap().clone())), 12 | _ => Err(darling::Error::custom("Expected identifier")) 13 | } 14 | } 15 | 16 | fn from_value(value: &syn::Lit) -> darling::Result { 17 | Ok(Self(::from_value(value)?)) 18 | } 19 | 20 | fn from_string(value: &str) -> darling::Result { 21 | Ok(Self(::from_string(value)?)) 22 | } 23 | } 24 | 25 | impl ToTokens for Ident { 26 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 27 | ::to_tokens(&self.0, tokens) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vesper-macros/src/extractors/list.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | use darling::{FromMeta, Result, export::NestedMeta}; 5 | 6 | pub struct List { 7 | pub inner: Vec 8 | } 9 | 10 | impl Default for List { 11 | fn default() -> Self { 12 | Self { 13 | inner: Vec::new() 14 | } 15 | } 16 | } 17 | 18 | impl Deref for List { 19 | type Target = Vec; 20 | fn deref(&self) -> &Self::Target { 21 | &self.inner 22 | } 23 | } 24 | 25 | impl DerefMut for List { 26 | fn deref_mut(&mut self) -> &mut Self::Target { 27 | &mut self.inner 28 | } 29 | } 30 | 31 | impl FromMeta for List { 32 | fn from_list(items: &[NestedMeta]) -> Result { 33 | let items = items.iter() 34 | .map(FromMeta::from_nested_meta) 35 | .collect::>>()?; 36 | 37 | Ok(Self { 38 | inner: items 39 | }) 40 | } 41 | } 42 | 43 | pub struct FixedList { 44 | pub inner: [T; SIZE] 45 | } 46 | 47 | impl FromMeta for FixedList { 48 | fn from_list(items: &[NestedMeta]) -> Result { 49 | if items.len() > SIZE { 50 | Err(darling::Error::too_many_items(SIZE))?; 51 | } else if items.len() < SIZE { 52 | Err(darling::Error::too_few_items(SIZE))?; 53 | } 54 | 55 | let items = items.iter() 56 | .map(FromMeta::from_nested_meta) 57 | .collect::>>()?; 58 | 59 | fn to_array(vec: Vec) -> Result<[T; S]> { 60 | vec.try_into() 61 | .map_err(|_| darling::Error::custom("Failed to construct fixed list")) 62 | } 63 | 64 | Ok(Self { 65 | inner: to_array(items)? 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /vesper-macros/src/extractors/map.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, hash::Hash, ops::{Deref, DerefMut}}; 2 | 3 | use darling::{FromMeta, Result}; 4 | use syn::{Meta, punctuated::Punctuated, Token, parse::{Parse, Parser}}; 5 | 6 | use crate::extractors::Tuple2; 7 | 8 | pub struct Map { 9 | inner: HashMap 10 | } 11 | 12 | impl FromMeta for Map 13 | where 14 | K: Parse + Eq + Hash, 15 | V: Parse 16 | { 17 | fn from_meta(item: &Meta) -> Result { 18 | match item { 19 | Meta::List(inner) => { 20 | let items = Punctuated::, Token![,]>::parse_terminated 21 | .parse2(inner.tokens.clone())?; 22 | 23 | let mut map = HashMap::with_capacity(items.len()); 24 | for item in items { 25 | map.insert(item.0, item.1); 26 | } 27 | 28 | Ok(Map { 29 | inner: map 30 | }) 31 | }, 32 | _ => Err(darling::Error::unsupported_format("Item list").with_span(&item)) 33 | } 34 | } 35 | } 36 | 37 | impl Deref for Map { 38 | type Target = HashMap; 39 | 40 | fn deref(&self) -> &Self::Target { 41 | &self.inner 42 | } 43 | } 44 | 45 | impl DerefMut for Map { 46 | fn deref_mut(&mut self) -> &mut Self::Target { 47 | &mut self.inner 48 | } 49 | } 50 | 51 | impl Map { 52 | pub fn pairs(&self) -> Vec> { 53 | self.inner.iter().map(|(k, v)| Tuple2::new(k, v)).collect() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /vesper-macros/src/extractors/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod closure; 2 | pub mod either; 3 | pub mod function_closure; 4 | pub mod function_path; 5 | pub mod ident; 6 | pub mod list; 7 | pub mod map; 8 | pub mod tuple; 9 | 10 | pub use { 11 | either::*, 12 | function_path::*, 13 | ident::*, 14 | list::*, 15 | map::*, 16 | tuple::* 17 | }; 18 | -------------------------------------------------------------------------------- /vesper-macros/src/extractors/tuple.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use darling::{FromMeta, export::NestedMeta}; 4 | use proc_macro2::Span; 5 | use quote::ToTokens; 6 | use syn::{Token, parse::{Parse, ParseStream}}; 7 | 8 | pub struct Tuple2(pub K, pub V, PhantomData); 9 | 10 | impl Tuple2 { 11 | pub fn new(k: K, v: V) -> Self { 12 | Self(k, v, PhantomData) 13 | } 14 | } 15 | 16 | impl Parse for Tuple2 17 | where 18 | K: Parse, 19 | V: Parse, 20 | D: Parse 21 | { 22 | fn parse(input: ParseStream) -> syn::Result { 23 | let k = input.parse()?; 24 | let _: D = input.parse()?; 25 | let v = input.parse()?; 26 | 27 | Ok(Self::new(k, v)) 28 | } 29 | } 30 | 31 | impl FromMeta for Tuple2 32 | where 33 | K: FromMeta, 34 | V: FromMeta, 35 | { 36 | fn from_list(items: &[NestedMeta]) -> darling::Result { 37 | match items { 38 | [a, b] => 39 | Ok(Self(K::from_nested_meta(a)?, V::from_nested_meta(b)?, PhantomData)), 40 | _ => Err(darling::Error::unsupported_format("2 item tuple").with_span(&Span::call_site())) 41 | } 42 | } 43 | } 44 | 45 | impl ToTokens for Tuple2 46 | where 47 | K: ToTokens, 48 | V: ToTokens 49 | { 50 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 51 | let k = &self.0; 52 | let v = &self.1; 53 | tokens.extend(quote::quote!((#k, #v))) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /vesper-macros/src/hook.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream as TokenStream2}; 2 | use syn::{parse2, spanned::Spanned, Error, FnArg, GenericParam, ItemFn, Lifetime, LifetimeParam, Result, ReturnType, Type}; 3 | 4 | /// The implementation of the hook macro, this macro takes the given function and changes 5 | /// it's output and body to fit into a `Pin>` 6 | pub fn hook(input: TokenStream2) -> Result { 7 | let fun = parse2::(input)?; 8 | 9 | let ItemFn { 10 | attrs, 11 | vis, 12 | mut sig, 13 | block, 14 | } = fun; 15 | 16 | if sig.asyncness.is_none() { 17 | /* 18 | In order to return a `Future` object, the function must be async, so if this function is 19 | not even async, this makes no sense to even try to execute this macro's function 20 | */ 21 | return Err(Error::new(sig.span(), "Function must be marked async")); 22 | } 23 | 24 | // As we will change the return to return a Pin> 25 | // we don't need the function to be async anymore. 26 | sig.asyncness = None; 27 | 28 | // The output of the function as a token stream, so we can quote it after 29 | let output = match &sig.output { 30 | ReturnType::Default => quote::quote!(()), 31 | ReturnType::Type(_, t) => quote::quote!(#t), 32 | }; 33 | 34 | // Wrap the original result in a Pin> 35 | sig.output = parse2(quote::quote!( 36 | -> ::std::pin::Pin + 'future + Send>> 37 | ))?; 38 | 39 | /* 40 | As we know all functions marked with this macro have an `SlashContext` reference, we have to 41 | add a lifetime which will be assigned to all references used in the function and to the returned 42 | future 43 | */ 44 | sig.generics.params.insert( 45 | 0, 46 | GenericParam::Lifetime(LifetimeParam { 47 | attrs: Default::default(), 48 | lifetime: Lifetime::new("'future", Span::call_site()), 49 | colon_token: None, 50 | bounds: Default::default(), 51 | }), 52 | ); 53 | 54 | for (idx, i) in sig.inputs.iter_mut().enumerate() { 55 | if let FnArg::Typed(kind) = i { 56 | // If the argument is a reference, assign the previous defined lifetime to it 57 | if let Type::Reference(ty) = &mut *kind.ty { 58 | ty.lifetime = Some(Lifetime::new("'future", Span::call_site())); 59 | 60 | if idx == 0 && ty.mutability.is_none() { // The first context must be a mutable reference. 61 | ty.mutability = Some(parse2(quote::quote!(mut))?); 62 | } 63 | } 64 | } 65 | } 66 | 67 | Ok(quote::quote! { 68 | #(#attrs)* 69 | #vis #sig { 70 | Box::pin(async move #block) 71 | } 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /vesper-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | use proc_macro2::TokenStream as TokenStream2; 4 | 5 | mod after; 6 | mod autocomplete; 7 | mod before; 8 | mod check; 9 | mod extractors; 10 | mod command; 11 | mod error_handler; 12 | mod hook; 13 | mod modal; 14 | mod optional; 15 | mod parse; 16 | mod util; 17 | 18 | /// Converts an `async` function into a normal function returning a 19 | /// `Pin + '_>>` 20 | #[doc(hidden)] 21 | #[proc_macro_attribute] 22 | pub fn hook(_: TokenStream, input: TokenStream) -> TokenStream { 23 | extract(hook::hook(input.into())) 24 | } 25 | 26 | /// Converts an `async-compatible` function into a builder and modifies function's body 27 | /// to parse all required arguments, for further information about the behaviour of this macro, see 28 | /// the implementation. 29 | /// 30 | /// By an `async-compatible` function it's meant a function with a minimum of one argument, 31 | /// which must be an `&SlashContext`, which is always given and also used to parse all arguments. 32 | /// 33 | /// # Usage: 34 | /// 35 | /// This macro can be used two ways: 36 | /// 37 | /// - Without arguments, as #[command], which takes the caller function name as the name of the command, and the type will be chat. 38 | /// - Providing the type and name, as #[command({chat, user, message}, name = "command name")] which takes the provided name as the command name. 39 | /// 40 | /// When marking a function with this attribute macro, you **must** provide a description of the 41 | /// command that will be seen on discord when using the command, this is made by adding a 42 | /// `description` attribute, which can be added two ways: 43 | /// 44 | /// - List way: #[description("Some description")] 45 | /// 46 | /// - Named value way: #[description = "Some description"] 47 | /// 48 | /// ## Arguments: 49 | /// 50 | /// You **must** provide another `description` attribute for every argument describing what they 51 | /// are, this description will be seen on discord when filling up the argument. This needs to be 52 | /// done with all the arguments except the context, which must be the first one, the accepted 53 | /// syntax is the same as the previous `description` one. 54 | /// 55 | /// ### Renaming: 56 | /// Adding a `rename` attribute is optional, but can be used to modify the name of the argument seen 57 | /// in discord, it is allowed to have only one `rename` attribute per argument and the attribute can 58 | /// be used the same ways a the `description` one. 59 | /// 60 | /// ### Autocompletion: 61 | /// Adding an `autocomplete` attribute is also optional, but it allows the developer to complete 62 | /// the user's input for an argument. This attribute is used the same way as the description one, 63 | /// but it *must* point to a function marked with the `#[autocomplete]` attribute macro. 64 | /// 65 | /// ### Localizations: 66 | /// Localizations can be applied in both commands and their arguments, for that, the `#[localized_names]` and 67 | /// `#[localized_descriptions]` attributes can be used, these accept a comma separated list of key-value items: 68 | /// ``` 69 | /// #[command] 70 | /// #[localized_names("en-US" = "US name", "en-GB" = "GB name", "es-ES" = "Spanish name")] 71 | /// #[description("My description")] 72 | /// #[localized_descriptions("en-US" = "US description", "en-GB" = "GB description", "es-ES" = "Spanish description")] 73 | /// async fn my_command(context: &mut SlashContext) -> DefaultCommandResult { 74 | /// // code here... 75 | /// } 76 | /// ``` 77 | /// Localizations can also be provided using closures or function pointers, to do that we have the `#[localized_names_fn]` 78 | /// and `#[localized_descriptions_fn]` 79 | /// 80 | /// These functions must have the following signature: 81 | /// ``` 82 | /// fn(&Framework, &Command) -> HashMap 83 | /// ``` 84 | /// 85 | /// To use a closure directly, the attribute has to be used like `#[localized_{names/descriptions}_fn = |f, c| ...]` 86 | /// 87 | /// To use a function pointer, the attribute accepts both `#[localized_{names/descriptions}_fn = myfn]` and 88 | /// `#[localized_{names/descriptions}_fn(myfn)]` 89 | /// 90 | /// ## Specifying required permissions 91 | /// 92 | /// It is possible to specify the permissions needed to execute the command by using the 93 | /// `#[required_permissions]` attribute. It accepts as input a list of comma separated 94 | /// [twilight permissions](https://docs.rs/twilight-model/latest/twilight_model/guild/struct.Permissions.html). 95 | /// For example, to specify that a user needs to have administrator permissions to execute a command, 96 | /// the attribute would be used like this `#[required_permissions(ADMINISTRATOR)]`. 97 | #[proc_macro_attribute] 98 | pub fn command(attrs: TokenStream, input: TokenStream) -> TokenStream { 99 | extract(command::command(attrs.into(), input.into())) 100 | } 101 | 102 | /// Prepares the function to allow it to be set as an after hook, see 103 | /// the implementation for more information about this macro's behaviour. 104 | #[proc_macro_attribute] 105 | pub fn after(_: TokenStream, input: TokenStream) -> TokenStream { 106 | extract(after::after(input.into())) 107 | } 108 | 109 | /// Prepares the function to allow it to be set as a before hook, see 110 | /// the implementation for more information about this macro's behaviour. 111 | #[proc_macro_attribute] 112 | pub fn before(_: TokenStream, input: TokenStream) -> TokenStream { 113 | extract(before::before(input.into())) 114 | } 115 | 116 | #[proc_macro_attribute] 117 | pub fn check(_: TokenStream, input: TokenStream) -> TokenStream { 118 | extract(check::check(input.into())) 119 | } 120 | 121 | #[proc_macro_attribute] 122 | pub fn error_handler(_: TokenStream, input: TokenStream) -> TokenStream { 123 | extract(error_handler::error_handler(input.into())) 124 | } 125 | 126 | /// Prepares the function to be used to autocomplete command arguments. 127 | #[proc_macro_attribute] 128 | pub fn autocomplete(_: TokenStream, input: TokenStream) -> TokenStream { 129 | extract(autocomplete::autocomplete(input.into())) 130 | } 131 | 132 | 133 | /// Implements `Parse` for an enum, allowing it to be used as a command argument. 134 | /// The enum will be seen by the user as a list of options to choose from, and one of the variants 135 | /// will have to be selected. 136 | /// 137 | /// # Examples: 138 | /// 139 | /// ```rust 140 | /// use vesper::prelude::*; 141 | /// 142 | /// #[derive(Parse)] 143 | /// enum Choices { 144 | /// First, 145 | /// Second, 146 | /// Third, 147 | /// // ... 148 | /// } 149 | /// ``` 150 | /// 151 | /// Enums variants can be renamed to modify the value seen by the user using the 152 | /// `#[parse(rename = "")]` attribute. 153 | /// 154 | /// # Example: 155 | /// ```rust 156 | /// use vesper::prelude::*; 157 | /// 158 | /// #[derive(Parse)] 159 | /// enum Choices { 160 | /// First, 161 | /// Second, 162 | /// #[parse(rename = "Forth")] 163 | /// Third, // <- This item will be shown to the user as "Forth", despite its variant name being Third 164 | /// // ... 165 | /// } 166 | /// ``` 167 | /// 168 | #[proc_macro_derive(Parse, attributes(parse))] 169 | pub fn parse(input: TokenStream) -> TokenStream { 170 | extract(parse::parse(input.into())) 171 | } 172 | 173 | /// Implements the `Modal` trait for the derived struct, allowing it to create modals and collect 174 | /// the inputs provided by the user. 175 | /// 176 | /// # Examples 177 | /// 178 | /// ```rust 179 | /// use vesper::prelude::*; 180 | /// 181 | /// #[derive(Modal)] 182 | /// struct MyModal { 183 | /// something: String, 184 | /// optional_item: Option 185 | /// } 186 | /// ``` 187 | /// 188 | /// # Attributes 189 | /// 190 | /// The derive macro accepts several attributes: 191 | /// 192 | /// - `#[modal(title = "")]`: This attribute allows specifying the title of the modal, by default the 193 | /// title will be the name of the structure. 194 | /// 195 | /// ## Example 196 | /// 197 | /// ```rust 198 | /// #[derive(Modal)] 199 | /// struct MyModal { // <- This modal will have "MyModal" as title. 200 | /// //... 201 | /// } 202 | /// 203 | /// #[derive(Modal)] 204 | /// #[modal(title = "Some incredible modal")] 205 | /// struct OtherModal { // <- This one will have "Some incredible modal" as the title. 206 | /// // ... 207 | /// } 208 | /// ``` 209 | /// 210 | /// - `#[modal(label = "<LABEL>")]`: This attribute allows setting the label of the field, by default it will 211 | /// be the name of the struct field. 212 | /// 213 | /// ## Example 214 | /// 215 | /// ```rust 216 | /// use vesper::prelude::*; 217 | /// 218 | /// #[derive(Modal)] 219 | /// struct MyModal { 220 | /// #[modal(label = "My field")] 221 | /// something: String, // <- This field will be shown as "My field" 222 | /// optional_item: Option<String> // <- This one will use the struct name "optional_item" 223 | /// } 224 | /// ``` 225 | /// 226 | /// - `#[modal(max_length = x)]` and `#[modal(min_length = y)]`: These attributes allow to set a 227 | /// maximum/minimum amount of characters a field can have. 228 | /// 229 | /// ## Example 230 | /// 231 | /// ```rust 232 | /// use vesper::prelude::*; 233 | /// 234 | /// #[derive(Modal)] 235 | /// struct MyModal { 236 | /// #[modal(max_length = 150, min_length = 15)] 237 | /// something: String, // <- This field will have both maximum and minimum size constraints. 238 | /// #[modal(max_length) = 25] 239 | /// short_field: String, // <- This field will only have a maximum size constraint. 240 | /// optional_item: Option<String> // <- This one won't have any 241 | /// } 242 | /// ``` 243 | /// 244 | /// - #[modal(placeholder = "<PLACEHOLDER>")]: This attribute allows specifying a placeholder that will be seen 245 | /// before entering anything on the input. 246 | /// 247 | /// ## Example 248 | /// 249 | /// ```rust 250 | /// use vesper::prelude::*; 251 | /// 252 | /// #[derive(Modal)] 253 | /// struct MyModal { 254 | /// #[modal(placeholder = "This is a placeholder")] 255 | /// something: String, // <- This field will have as placeholder "This is a placeholder". 256 | /// } 257 | /// ``` 258 | /// 259 | /// - `#[modal(paragraph)]`: This attribute will mark the field as a paragraph. By default, all fields are 260 | /// marked as single line fields, so the user will only be able to input up to one line unless we 261 | /// mark it as a paragraph. 262 | /// 263 | /// ## Example 264 | /// 265 | /// ```rust 266 | /// use vesper::prelude::*; 267 | /// 268 | /// #[derive(Modal)] 269 | /// struct MyModal { 270 | /// #[modal(paragraph)] 271 | /// something: String, // <- This field will be shown as a multi-line field. 272 | /// optional_item: Option<String> // <- This one will be shown as a single line one. 273 | /// } 274 | /// ``` 275 | #[proc_macro_derive( 276 | Modal, 277 | attributes(modal) 278 | )] 279 | pub fn modal(input: TokenStream) -> TokenStream { 280 | extract(modal::modal(input.into())) 281 | } 282 | 283 | /// Extracts the given result, throwing a compile error if an error is given. 284 | fn extract(res: syn::Result<TokenStream2>) -> TokenStream { 285 | match res { 286 | Ok(s) => s, 287 | Err(why) => why.to_compile_error(), 288 | } 289 | .into() 290 | } 291 | -------------------------------------------------------------------------------- /vesper-macros/src/modal.rs: -------------------------------------------------------------------------------- 1 | use darling::{FromDeriveInput, FromField, FromAttributes}; 2 | use proc_macro2::{Ident, TokenStream as TokenStream2}; 3 | use quote::ToTokens; 4 | use syn::{parse2, spanned::Spanned, Error, Result, Type, DeriveInput, Fields, FieldsNamed, Data}; 5 | use crate::optional::Optional; 6 | 7 | #[derive(FromDeriveInput, Default)] 8 | #[darling(attributes(modal))] 9 | #[darling(default)] 10 | struct Modal { 11 | title: String, 12 | #[darling(skip)] 13 | fields: Vec<Field> 14 | } 15 | 16 | #[derive(FromField)] 17 | struct Field { 18 | ty: Type, 19 | ident: Option<Ident>, 20 | #[darling(skip)] 21 | attributes: FieldAttributes 22 | } 23 | 24 | #[derive(FromAttributes, Default)] 25 | #[darling(attributes(modal))] 26 | #[darling(default)] 27 | struct FieldAttributes { 28 | label: Optional<String>, 29 | placeholder: Optional<String>, 30 | paragraph: bool, 31 | max_length: Optional<u16>, 32 | min_length: Optional<u16>, 33 | value: Optional<String> 34 | } 35 | 36 | struct FieldParser<'a>(&'a Field); 37 | 38 | impl Modal { 39 | fn new(input: &DeriveInput, fields: &FieldsNamed) -> darling::Result<Self> { 40 | let mut this = Self::from_derive_input(input)?; 41 | 42 | for field in &fields.named { 43 | this.fields.push(Field::new(field)?); 44 | } 45 | 46 | Ok(this) 47 | } 48 | } 49 | 50 | impl Field { 51 | fn new(field: &syn::Field) -> darling::Result<Self> { 52 | let mut this = Field::from_field(&field)?; 53 | this.attributes = FieldAttributes::from_attributes(field.attrs.as_slice())?; 54 | 55 | if this.attributes.label.is_none() { 56 | this.attributes.label = Some(this.ident.as_ref().unwrap().to_string()).into(); 57 | } 58 | 59 | Ok(this) 60 | } 61 | } 62 | 63 | impl ToTokens for Field { 64 | fn to_tokens(&self, tokens: &mut TokenStream2) { 65 | let Self { 66 | ty: kind, 67 | ident: _, 68 | attributes 69 | } = &self; 70 | let FieldAttributes { 71 | label, 72 | placeholder, 73 | paragraph, 74 | max_length, 75 | min_length, 76 | value 77 | } = attributes; 78 | let label = label.as_ref().unwrap(); 79 | let label_ref = &label; 80 | let placeholder = placeholder.clone().map(|p| quote::quote!(String::from(#p))); 81 | 82 | let style = if *paragraph { 83 | quote::quote!(TextInputStyle::Paragraph) 84 | } else { 85 | quote::quote!(TextInputStyle::Short) 86 | }; 87 | 88 | tokens.extend(quote::quote! { 89 | Component::ActionRow(ActionRow { 90 | components: vec![ 91 | Component::TextInput(TextInput { 92 | custom_id: String::from(#label_ref), 93 | label: String::from(#label), 94 | placeholder: #placeholder, 95 | style: #style, 96 | max_length: #max_length, 97 | min_length: #min_length, 98 | required: Some(<#kind as ModalDataOption>::required()), 99 | value: #value 100 | }) 101 | ] 102 | }) 103 | }) 104 | } 105 | } 106 | 107 | impl<'a> ToTokens for FieldParser<'a> { 108 | fn to_tokens(&self, tokens: &mut TokenStream2) { 109 | let ident = &self.0.ident; 110 | let label = &self.0.attributes.label.as_ref().unwrap(); 111 | 112 | tokens.extend(quote::quote! { 113 | #label => { 114 | #ident = component.value; 115 | } 116 | }) 117 | } 118 | } 119 | 120 | fn fields(data: &Data, derive_span: impl Spanned) -> Result<&FieldsNamed> { 121 | match data { 122 | Data::Struct(s) => match &s.fields { 123 | Fields::Named(fields) => Ok(fields), 124 | Fields::Unnamed(fields) => { 125 | return Err(Error::new( 126 | fields.span(), 127 | "Tuple structs not supported", 128 | )) 129 | }, 130 | Fields::Unit => { 131 | return Err(Error::new( 132 | s.fields.span(), 133 | "Unit structs not supported", 134 | )) 135 | } 136 | }, 137 | _ => { 138 | return Err(Error::new( 139 | derive_span.span(), 140 | "This derive is only available for structs", 141 | )) 142 | } 143 | } 144 | } 145 | 146 | pub fn modal(input: TokenStream2) -> Result<TokenStream2> { 147 | let derive = parse2::<DeriveInput>(input)?; 148 | let fields = fields(&derive.data, &derive)?; 149 | 150 | let Modal { title, fields } = Modal::new(&derive, fields)?; 151 | let struct_ident = &derive.ident; 152 | 153 | let parsers = fields.iter() 154 | .map(FieldParser) 155 | .collect::<Vec<FieldParser>>(); 156 | let field_names = fields.iter() 157 | .map(|field| field.ident.as_ref().unwrap()) 158 | .collect::<Vec<&Ident>>(); 159 | let field_types = fields.iter() 160 | .map(|field| &field.ty) 161 | .collect::<Vec<&Type>>(); 162 | 163 | Ok(quote::quote! { 164 | const _: () = { 165 | use ::vesper::{ 166 | context::SlashContext, 167 | extract::ModalDataOption, 168 | twilight_exports::{ 169 | Interaction, 170 | InteractionData, 171 | InteractionResponse, 172 | InteractionResponseData, 173 | InteractionResponseType, 174 | ActionRow, 175 | Component, 176 | TextInput, 177 | TextInputStyle 178 | } 179 | }; 180 | 181 | #[automatically_derived] 182 | impl<D> ::vesper::modal::Modal<D> for #struct_ident { 183 | fn create(ctx: &SlashContext<'_, D>, custom_id: String) -> InteractionResponse { 184 | InteractionResponse { 185 | kind: InteractionResponseType::Modal, 186 | data: Some(InteractionResponseData { 187 | custom_id: Some(custom_id), 188 | title: Some(String::from(#title)), 189 | components: Some(vec![#(#fields),*]), 190 | ..std::default::Default::default() 191 | }) 192 | } 193 | } 194 | 195 | fn parse(interaction: &mut Interaction) -> Self { 196 | let Some(InteractionData::ModalSubmit(modal)) = &mut interaction.data else { 197 | unreachable!(); 198 | }; 199 | 200 | #(let mut #field_names = None;)* 201 | 202 | let components = std::mem::take(&mut modal.components); 203 | for row in components { 204 | for component in row.components { 205 | match component.custom_id.as_str() { 206 | #(#parsers,)* 207 | _ => panic!("Unrecognized field") 208 | } 209 | } 210 | } 211 | 212 | Self { 213 | #(#field_names: <#field_types as ModalDataOption>::parse(#field_names)),* 214 | } 215 | } 216 | } 217 | }; 218 | }) 219 | } 220 | -------------------------------------------------------------------------------- /vesper-macros/src/optional.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | use darling::FromMeta; 3 | use proc_macro2::TokenStream; 4 | use quote::ToTokens; 5 | 6 | #[derive(Default)] 7 | pub struct Optional<T>(Option<T>); 8 | 9 | impl<T: ToTokens> ToTokens for Optional<T> { 10 | fn to_tokens(&self, tokens: &mut TokenStream) { 11 | if let Some(inner) = &self.0 { 12 | tokens.extend(quote::quote!(Some(#inner))); 13 | } else { 14 | tokens.extend(quote::quote!(None)) 15 | } 16 | } 17 | } 18 | 19 | impl<T: ToTokens> From<Option<T>> for Optional<T> { 20 | fn from(value: Option<T>) -> Self { 21 | Self(value) 22 | } 23 | } 24 | 25 | impl<T: FromMeta> FromMeta for Optional<T> { 26 | fn from_meta(item: &syn::Meta) -> darling::Result<Self> { 27 | FromMeta::from_meta(item) 28 | .map(|s| Self(Some(s))) 29 | } 30 | 31 | fn from_none() -> Option<Self> { 32 | Some(Self(None)) 33 | } 34 | } 35 | 36 | impl<T> Deref for Optional<T> { 37 | type Target = Option<T>; 38 | 39 | fn deref(&self) -> &Self::Target { 40 | &self.0 41 | } 42 | } 43 | 44 | impl<T> DerefMut for Optional<T> { 45 | fn deref_mut(&mut self) -> &mut Self::Target { 46 | &mut self.0 47 | } 48 | } 49 | 50 | impl<T: Clone> Clone for Optional<T> { 51 | fn clone(&self) -> Self { 52 | Self(self.0.clone()) 53 | } 54 | } 55 | 56 | impl<T> Optional<T> { 57 | pub fn as_ref(&self) -> Optional<&T> { 58 | Optional(self.0.as_ref()) 59 | } 60 | 61 | #[allow(unused)] 62 | pub fn as_mut(&mut self) -> Optional<&mut T> { 63 | Optional(self.0.as_mut()) 64 | } 65 | 66 | pub fn map<F, R>(self, fun: F) -> Optional<R> 67 | where 68 | F: FnOnce(T) -> R 69 | { 70 | Optional(self.0.map(fun)) 71 | } 72 | 73 | #[allow(unused)] 74 | pub fn into_inner(self) -> Option<T> { 75 | self.0 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /vesper-macros/src/parse.rs: -------------------------------------------------------------------------------- 1 | use darling::FromAttributes; 2 | use proc_macro2::{Ident, TokenStream as TokenStream2}; 3 | use syn::{spanned::Spanned, DeriveInput, Error, Result}; 4 | use crate::extractors::{Either, FixedList}; 5 | 6 | #[derive(FromAttributes)] 7 | #[darling(attributes(parse))] 8 | struct VariantAttributes { 9 | #[darling(rename = "rename")] 10 | renaming: Option<Either<String, FixedList<1, String>>>, 11 | } 12 | 13 | struct Variant { 14 | value: String, 15 | ident: Ident, 16 | index: usize, 17 | } 18 | 19 | impl Variant { 20 | fn parse_tokens(&self, tokens: &mut TokenStream2) { 21 | let index = &self.index; 22 | let ident = &self.ident; 23 | tokens.extend(quote::quote! { 24 | #index => Ok(Self::#ident), 25 | }) 26 | } 27 | 28 | fn choice_tokens(&self, tokens: &mut TokenStream2) { 29 | let value = &self.value; 30 | let index = self.index as i64; 31 | tokens.extend(quote::quote! { 32 | choices.push(CommandOptionChoice { 33 | name: #value.to_string(), 34 | name_localizations: None, 35 | value: CommandOptionChoiceValue::Integer(#index) 36 | } 37 | ); 38 | }) 39 | } 40 | } 41 | 42 | pub fn parse(input: TokenStream2) -> Result<TokenStream2> { 43 | let derive = syn::parse2::<DeriveInput>(input)?; 44 | let enumeration = match derive.data { 45 | syn::Data::Enum(e) => e, 46 | _ => { 47 | return Err(Error::new( 48 | derive.ident.span(), 49 | "This derive is only available for enums", 50 | )) 51 | } 52 | }; 53 | 54 | let mut variants = Vec::new(); 55 | let mut index = 1; 56 | 57 | for variant in enumeration.variants { 58 | if !matches!(&variant.fields, syn::Fields::Unit) { 59 | return Err(Error::new( 60 | variant.span(), 61 | "Choice parameter cannot have inner values", 62 | )); 63 | } 64 | 65 | let attributes = VariantAttributes::from_attributes(variant.attrs.as_slice())?; 66 | 67 | let name = attributes.renaming 68 | .map(|item| item.inner().clone()) 69 | .unwrap_or(variant.ident.to_string()); 70 | 71 | variants.push(Variant { 72 | ident: variant.ident.clone(), 73 | value: name, 74 | index, 75 | }); 76 | 77 | index += 1; 78 | } 79 | 80 | let mut parse_stream = TokenStream2::new(); 81 | let mut choice_stream = TokenStream2::new(); 82 | for variant in variants { 83 | variant.parse_tokens(&mut parse_stream); 84 | variant.choice_tokens(&mut choice_stream); 85 | } 86 | 87 | let enum_name = &derive.ident; 88 | 89 | Ok(quote::quote! { 90 | const _: () = { 91 | use ::vesper::{ 92 | builder::WrappedClient, 93 | prelude::async_trait, 94 | parse::{Parse, ParseError}, 95 | twilight_exports::{ 96 | CommandInteractionDataResolved, 97 | CommandOptionChoice, 98 | CommandOptionChoiceValue, 99 | CommandOptionType, 100 | CommandOptionValue, 101 | 102 | }, 103 | }; 104 | 105 | #[automatically_derived] 106 | #[async_trait] 107 | impl<T: Send + Sync + 'static> Parse<T> for #enum_name { 108 | async fn parse( 109 | http_client: &WrappedClient, 110 | data: &T, 111 | value: Option<&CommandOptionValue>, 112 | resolved: Option<&mut CommandInteractionDataResolved> 113 | ) -> Result<Self, ParseError> 114 | { 115 | let num = usize::parse(http_client, data, value, resolved).await?; 116 | match num { 117 | #parse_stream 118 | _ => return Err(ParseError::Parsing { 119 | argument_name: String::new(), 120 | required: true, 121 | argument_type: String::from(stringify!(#enum_name)), 122 | error: String::from("Unrecognized option") 123 | } 124 | ) 125 | } 126 | } 127 | fn kind() -> CommandOptionType { 128 | CommandOptionType::Integer 129 | } 130 | fn choices() -> Option<Vec<CommandOptionChoice>> { 131 | let mut choices = Vec::new(); 132 | 133 | #choice_stream; 134 | 135 | Some(choices) 136 | } 137 | } 138 | }; 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /vesper-macros/src/util.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span, TokenStream}; 2 | use quote::ToTokens; 3 | use syn::spanned::Spanned; 4 | use syn::{parse2, Error, FnArg, GenericArgument, Pat, PatType, Path, PathArguments, Result, ReturnType, Signature, Type, Lifetime}; 5 | use crate::util; 6 | 7 | /// Gets the path of the futurize macro 8 | pub fn get_hook_macro() -> Path { 9 | parse2(quote::quote!(::vesper::macros::hook)).unwrap() 10 | } 11 | 12 | /// Gets the path of the command struct used internally by vesper 13 | pub fn get_command_path() -> Path { 14 | parse2(quote::quote!(::vesper::command::Command)).unwrap() 15 | } 16 | 17 | pub fn get_returnable_trait() -> Path { 18 | parse2(quote::quote!(::vesper::extract::Returnable)).unwrap() 19 | } 20 | 21 | /// Gets the path of the given type 22 | pub fn get_path(t: &Type, allow_references: bool) -> Result<&Path> { 23 | match t { 24 | // If the type is actually a path, just return it 25 | Type::Path(p) => Ok(&p.path), 26 | // If the type is a reference, call this function recursively until we get the path 27 | Type::Reference(r) => { 28 | if allow_references { 29 | get_path(&r.elem, allow_references) 30 | } else { 31 | Err(Error::new(r.span(), "Reference not allowed")) 32 | } 33 | }, 34 | _ => Err(Error::new( 35 | t.span(), 36 | "parameter must be a path to a context type", 37 | )), 38 | } 39 | } 40 | 41 | /// Gets the path of the given type 42 | pub fn get_path_mut(t: &mut Type) -> Result<&mut Path> { 43 | match t { 44 | // If the type is actually a path, just return it 45 | Type::Path(p) => Ok(&mut p.path), 46 | // If the type is a reference, call this function recursively until we get the path 47 | Type::Reference(r) => get_path_mut(&mut r.elem), 48 | _ => Err(Error::new( 49 | t.span(), 50 | "parameter must be a path to a context type", 51 | )), 52 | } 53 | } 54 | 55 | /// Get the ascription pattern of the given function argument 56 | pub fn get_pat(arg: &FnArg) -> Result<&PatType> { 57 | match arg { 58 | FnArg::Typed(t) => Ok(t), 59 | _ => Err(Error::new( 60 | arg.span(), 61 | "`self` parameter is not allowed here", 62 | )), 63 | } 64 | } 65 | 66 | /// Get the ascription pattern of the given function argument 67 | pub fn get_pat_mut(arg: &mut FnArg) -> Result<&mut PatType> { 68 | match arg { 69 | FnArg::Typed(t) => Ok(t), 70 | _ => Err(Error::new( 71 | arg.span(), 72 | "`self` parameter is not allowed here", 73 | )), 74 | } 75 | } 76 | 77 | /// Gets the identifier of the given pattern 78 | pub fn get_ident(p: &Pat) -> Result<Ident> { 79 | match p { 80 | Pat::Ident(pi) => Ok(pi.ident.clone()), 81 | _ => Err(Error::new(p.span(), "parameter must have an identifier")), 82 | } 83 | } 84 | 85 | /// Gets the generic arguments of the given path 86 | pub fn get_generic_arguments(path: &Path) -> Result<impl Iterator<Item = &GenericArgument> + '_> { 87 | match &path.segments.last().unwrap().arguments { 88 | PathArguments::None => Ok(Vec::new().into_iter()), 89 | PathArguments::AngleBracketed(arguments) => { 90 | Ok(arguments.args.iter().collect::<Vec<_>>().into_iter()) 91 | } 92 | _ => Err(Error::new( 93 | path.span(), 94 | "context type cannot have generic parameters in parenthesis", 95 | )), 96 | } 97 | } 98 | 99 | pub fn get_return_type(sig: &Signature) -> Result<Box<Type>> { 100 | match &sig.output { 101 | ReturnType::Default => return Err(Error::new(sig.output.span(), "Return type must be a Result<T, E>")), 102 | ReturnType::Type(_, kind) => Ok(kind.clone()) 103 | } 104 | } 105 | 106 | pub fn get_context_type(sig: &Signature, allow_references: bool) -> Result<Type> { 107 | let arg = match sig.inputs.iter().next() { 108 | None => { 109 | return Err(Error::new( 110 | sig.inputs.span(), 111 | "Expected Context as first parameter", 112 | )) 113 | } 114 | Some(c) => c, 115 | }; 116 | 117 | let ty = util::get_bracketed_generic(arg, allow_references, |ty| { 118 | if let Type::Infer(_) = ty { 119 | Err(Error::new(ty.span(), "Context must have a known type")) 120 | } else { 121 | Ok(ty.clone()) 122 | } 123 | })?; 124 | 125 | match ty { 126 | None => Err(Error::new(arg.span(), "Context type must be set")), 127 | Some(ty) => Ok(ty), 128 | } 129 | } 130 | 131 | pub fn set_context_lifetime(sig: &mut Signature) -> Result<()> { 132 | let lifetime = Lifetime::new("'future", Span::call_site()); 133 | let ctx = get_pat_mut(sig.inputs.iter_mut().next().unwrap())?; 134 | let path = get_path_mut(&mut ctx.ty)?; 135 | let mut insert_lifetime = true; 136 | 137 | { 138 | let generics = util::get_generic_arguments(path)?; 139 | for generic in generics { 140 | if let GenericArgument::Lifetime(inner) = generic { 141 | if *inner == lifetime { 142 | insert_lifetime = false; 143 | } 144 | } 145 | } 146 | } 147 | 148 | if insert_lifetime { 149 | if let PathArguments::AngleBracketed(inner) = 150 | &mut path.segments.last_mut().unwrap().arguments 151 | { 152 | inner.args.insert(0, GenericArgument::Lifetime(lifetime)); 153 | } 154 | } 155 | 156 | Ok(()) 157 | } 158 | 159 | pub fn get_bracketed_generic<F>(arg: &FnArg, allow_references: bool, fun: F) -> Result<Option<Type>> 160 | where 161 | F: Fn(&Type) -> Result<Type> 162 | { 163 | let mut generics = get_generic_arguments(get_path(&get_pat(arg)?.ty, allow_references)?)?; 164 | 165 | while let Some(next) = generics.next() { 166 | match next { 167 | GenericArgument::Lifetime(_) => (), 168 | GenericArgument::Type(ty) => return Ok(Some(fun(ty)?)), 169 | other => { 170 | return Err(Error::new(other.span(), "Generic must be a type")) 171 | } 172 | } 173 | } 174 | 175 | Ok(None) 176 | } 177 | 178 | /// Checks whether the given return type is the same as the provided one 179 | pub fn check_return_type(ret: &ReturnType, out: TokenStream) -> Result<()> { 180 | let ty = match &ret { 181 | ReturnType::Default => syn::parse2::<Type>(quote::quote!(()))?, 182 | ReturnType::Type(_, ty) => syn::parse2::<Type>(quote::quote!(#ty))?, 183 | }; 184 | 185 | let out = parse2(quote::quote!(#out))?; 186 | 187 | if ty != out { 188 | return Err(Error::new( 189 | ret.span(), 190 | format!( 191 | "Expected {} as return type, got {}", 192 | out.to_token_stream(), 193 | ty.to_token_stream() 194 | ), 195 | )); 196 | } 197 | 198 | Ok(()) 199 | } 200 | -------------------------------------------------------------------------------- /vesper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vesper" 3 | version = "0.13.0" 4 | authors = ["Alvaro <62391364+AlvaroMS25@users.noreply.github.com>"] 5 | edition = "2018" 6 | description = "A slash-command framework meant to be used with twilight" 7 | readme = "README.md" 8 | repository = "https://github.com/AlvaroMS25/vesper" 9 | license = "MIT" 10 | keywords = ["async", "twilight", "discord", "slash-command"] 11 | categories = ["asynchronous"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [package.metadata.docs.rs] 16 | all-features = true 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | 19 | [dependencies] 20 | async-trait = "0.1" 21 | vesper-macros = { path = "../vesper-macros", version = "0.13" } 22 | parking_lot = "0.12" 23 | tracing = "0.1" 24 | twilight-model = "0.15" 25 | twilight-http = { version = "0.15", default-features = false } 26 | twilight-validate = "0.15" 27 | thiserror = "1" 28 | 29 | # feature: bulk 30 | twilight-util = { version = "0.15", features = ["builder"], optional = true } 31 | 32 | [dependencies.tokio] 33 | version = "1" 34 | default-features = false 35 | features = ["sync"] 36 | 37 | [features] 38 | bulk = ["dep:twilight-util"] 39 | 40 | [dev-dependencies] 41 | futures = "0.3" 42 | tokio = { version = "1", features = ["full"] } 43 | -------------------------------------------------------------------------------- /vesper/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alvaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vesper/src/argument.rs: -------------------------------------------------------------------------------- 1 | use crate::hook::AutocompleteHook; 2 | use crate::twilight_exports::*; 3 | use crate::parse::Parse; 4 | use crate::localizations::{Localizations, LocalizationsProvider}; 5 | use crate::prelude::Framework; 6 | 7 | /// A structure representing a command argument. 8 | pub struct CommandArgument<D, T, E> { 9 | /// Argument name. 10 | pub name: &'static str, 11 | pub localized_names: Localizations<D, T, E>, 12 | /// Description of the argument. 13 | pub description: &'static str, 14 | pub localized_descriptions: Localizations<D, T, E>, 15 | /// Whether the argument is required. 16 | pub required: bool, 17 | /// The type this argument has. 18 | pub kind: CommandOptionType, 19 | /// The input options allowed to choose from in this command, only valid if it is [Some](Some) 20 | pub choices: Option<Vec<CommandOptionChoice>>, 21 | /// A function used to autocomplete fields. 22 | pub autocomplete: Option<AutocompleteHook<D>>, 23 | pub modify_fn: fn(&mut CommandOption) 24 | } 25 | 26 | impl<D, T, E> CommandArgument<D, T, E> { 27 | /// Converts the argument into a twilight's [command option](CommandOption) 28 | pub fn as_option(&self, f: &Framework<D, T, E>, c: &crate::command::Command<D, T, E>) -> CommandOption { 29 | let mut option = CommandOption { 30 | autocomplete: None, 31 | channel_types: None, 32 | choices: None, 33 | description: self.description.to_string(), 34 | description_localizations: self.localized_descriptions.get_localizations(f, c), 35 | kind: self.kind, 36 | max_length: None, 37 | max_value: None, 38 | min_length: None, 39 | min_value: None, 40 | name: self.name.to_string(), 41 | name_localizations: self.localized_names.get_localizations(f, c), 42 | options: None, 43 | required: Some(self.required) 44 | }; 45 | 46 | (self.modify_fn)(&mut option); 47 | 48 | match option.kind { 49 | CommandOptionType::String | CommandOptionType::Integer | CommandOptionType::Number => { 50 | option.autocomplete = Some(self.autocomplete.is_some()); 51 | option.choices = Some(self.choices.clone().unwrap_or_default()); 52 | }, 53 | _ => () 54 | } 55 | 56 | option 57 | } 58 | } 59 | 60 | impl<D: Send + Sync, T, E> CommandArgument<D, T, E> { 61 | pub fn new<Arg: Parse<D>>( 62 | name: &'static str, 63 | description: &'static str, 64 | autocomplete: Option<AutocompleteHook<D>> 65 | ) -> Self 66 | { 67 | Self { 68 | name, 69 | localized_names: Default::default(), 70 | description, 71 | localized_descriptions: Default::default(), 72 | required: Arg::required(), 73 | kind: Arg::kind(), 74 | choices: Arg::choices(), 75 | autocomplete, 76 | modify_fn: Arg::modify_option 77 | } 78 | } 79 | 80 | pub fn localized_names<I, K, V>(mut self, iterator: I) -> Self 81 | where 82 | I: IntoIterator<Item = (K, V)>, 83 | K: ToString, 84 | V: ToString 85 | { 86 | self.localized_names 87 | .extend(iterator.into_iter().map(|(k, v)| (k.to_string(), v.to_string()))); 88 | self 89 | } 90 | 91 | pub fn localized_names_fn(mut self, fun: LocalizationsProvider<D, T, E>) -> Self { 92 | self.localized_names.set_provider(fun); 93 | self 94 | } 95 | 96 | pub fn localized_descriptions<I, K, V>(mut self, iterator: I) -> Self 97 | where 98 | I: IntoIterator<Item = (K, V)>, 99 | K: ToString, 100 | V: ToString 101 | { 102 | self.localized_descriptions 103 | .extend(iterator.into_iter().map(|(k, v)| (k.to_string(), v.to_string()))); 104 | self 105 | } 106 | 107 | pub fn localized_descriptions_fn(mut self, fun: LocalizationsProvider<D, T, E>) -> Self { 108 | self.localized_descriptions.set_provider(fun); 109 | self 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /vesper/src/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | command::{Command, CommandMap}, 3 | framework::{DefaultError, Framework}, 4 | group::*, 5 | hook::{AfterHook, BeforeHook}, 6 | twilight_exports::{ApplicationMarker, Client, CommandType, Id, Permissions}, 7 | parse::ParseError 8 | }; 9 | 10 | use std::{ops::Deref, sync::Arc}; 11 | 12 | /// A wrapper around twilight's http client allowing the user to decide how to provide it to the framework. 13 | #[allow(clippy::large_enum_variant)] 14 | pub enum WrappedClient { 15 | Arc(Arc<Client>), 16 | Raw(Client), 17 | Boxed(Box<dyn Deref<Target = Client> + Send + Sync>), 18 | } 19 | 20 | impl WrappedClient { 21 | /// Returns the underlying http client. 22 | pub fn inner(&self) -> &Client { 23 | match self { 24 | Self::Arc(c) => c, 25 | Self::Raw(c) => c, 26 | Self::Boxed(b) => b, 27 | } 28 | } 29 | 30 | /// Casts the [client](WrappedClient) into T if it's [Boxed](WrappedClient::Boxed) 31 | /// 32 | /// **SAFETY: The caller must ensure the type given is the same as the boxed one.** 33 | #[allow(clippy::needless_lifetimes, clippy::borrow_deref_ref)] 34 | pub fn cast<'a, T>(&'a self) -> Option<&'a T> { 35 | if let WrappedClient::Boxed(inner) = self { 36 | // SAFETY: The caller must ensure here that the type provided is the original type of 37 | // the pointer. 38 | let ptr = (&*inner.as_ref()) as *const _ as *const T; 39 | // SAFETY: It is safe to dereference here the pointer as we hold the owned value, 40 | // so we ensure it is valid. 41 | Some(unsafe { &*ptr }) 42 | } else { 43 | None 44 | } 45 | } 46 | } 47 | 48 | impl From<Client> for WrappedClient { 49 | fn from(c: Client) -> Self { 50 | WrappedClient::Raw(c) 51 | } 52 | } 53 | 54 | impl From<Arc<Client>> for WrappedClient { 55 | fn from(c: Arc<Client>) -> Self { 56 | WrappedClient::Arc(c) 57 | } 58 | } 59 | 60 | impl From<Box<dyn Deref<Target = Client> + Send + Sync>> for WrappedClient { 61 | fn from(c: Box<dyn Deref<Target = Client> + Send + Sync>) -> Self { 62 | Self::Boxed(c) 63 | } 64 | } 65 | 66 | /// A pointer to a function returning a generic T type. 67 | pub(crate) type FnPointer<T> = fn() -> T; 68 | 69 | /// A builder used to set all options before framework initialization. 70 | pub struct FrameworkBuilder<D, T = (), E = DefaultError> { 71 | /// The http client used by the framework. 72 | pub http_client: WrappedClient, 73 | /// The application id of the client. 74 | pub application_id: Id<ApplicationMarker>, 75 | /// Data that will be available to all commands. 76 | pub data: D, 77 | /// The actual commands, only the simple ones. 78 | pub commands: CommandMap<D, T, E>, 79 | /// All groups containing commands. 80 | pub groups: GroupParentMap<D, T, E>, 81 | /// A hook executed before any command. 82 | pub before: Option<BeforeHook<D>>, 83 | /// A hook executed after command's completion. 84 | pub after: Option<AfterHook<D, T, E>>, 85 | } 86 | 87 | impl<D, T, E> FrameworkBuilder<D, T, E> 88 | where 89 | E: From<ParseError> 90 | { 91 | /// Creates a new [Builder](self::FrameworkBuilder). 92 | pub fn new( 93 | http_client: impl Into<WrappedClient>, 94 | application_id: Id<ApplicationMarker>, 95 | data: D, 96 | ) -> Self { 97 | Self { 98 | http_client: http_client.into(), 99 | application_id, 100 | data, 101 | commands: Default::default(), 102 | groups: Default::default(), 103 | before: None, 104 | after: None, 105 | } 106 | } 107 | 108 | /// Set the hook that will be executed before commands. 109 | /// 110 | /// # Examples 111 | /// 112 | /// ```rust 113 | /// use vesper::prelude::*; 114 | /// use twilight_http::Client; 115 | /// use twilight_model::id::Id; 116 | /// 117 | /// #[before] 118 | /// async fn before_hook(ctx: &mut SlashContext<()>, command_name: &str) -> bool { 119 | /// println!("Executing command {command_name}"); 120 | /// true 121 | /// } 122 | /// 123 | /// #[tokio::main] 124 | /// async fn main() { 125 | /// let token = std::env::var("DISCORD_TOKEN").unwrap(); 126 | /// let app_id = std::env::var("DISCORD_APP_ID").unwrap().parse::<u64>().unwrap(); 127 | /// let http_client = Client::new(token); 128 | /// 129 | /// let framework = Framework::<()>::builder(http_client, Id::new(app_id), ()) 130 | /// .before(before_hook) 131 | /// .build(); 132 | /// } 133 | /// ``` 134 | pub fn before(mut self, fun: FnPointer<BeforeHook<D>>) -> Self { 135 | self.before = Some(fun()); 136 | self 137 | } 138 | 139 | /// Set the hook that will be executed after command's completion. 140 | /// 141 | /// # Examples 142 | /// 143 | /// ```rust 144 | /// use vesper::prelude::*; 145 | /// use twilight_http::Client; 146 | /// use twilight_model::id::Id; 147 | /// 148 | /// #[after] 149 | /// async fn after_hook(ctx: &mut SlashContext<()>, command_name: &str, _: Option<DefaultCommandResult>) { 150 | /// println!("Command {command_name} finished execution"); 151 | /// } 152 | /// 153 | /// #[tokio::main] 154 | /// async fn main() { 155 | /// let token = std::env::var("DISCORD_TOKEN").unwrap(); 156 | /// let app_id = std::env::var("DISCORD_APP_ID").unwrap().parse::<u64>().unwrap(); 157 | /// let http_client = Client::new(token); 158 | /// 159 | /// let framework = Framework::builder(http_client, Id::new(app_id), ()) 160 | /// .after(after_hook) 161 | /// .build(); 162 | /// } 163 | /// ``` 164 | pub fn after(mut self, fun: FnPointer<AfterHook<D, T, E>>) -> Self { 165 | self.after = Some(fun()); 166 | self 167 | } 168 | 169 | /// Registers a new command in the framework. 170 | /// 171 | /// # Examples 172 | /// 173 | /// ```rust 174 | /// use vesper::prelude::*; 175 | /// use twilight_http::Client; 176 | /// use twilight_model::id::Id; 177 | /// 178 | ///#[command] 179 | ///#[description = "Says Hello world!"] 180 | ///async fn hello_world(ctx: &mut SlashContext<()>) -> DefaultCommandResult { 181 | /// ctx.defer(false).await?; 182 | /// ctx.interaction_client.update_response(&ctx.interaction.token) 183 | /// .content(Some("Hello world!")) 184 | /// .unwrap() 185 | /// .await?; 186 | /// 187 | /// Ok(()) 188 | ///} 189 | /// 190 | ///#[command] 191 | ///#[description = "Repeats something"] 192 | ///async fn repeat_content( 193 | /// ctx: &SlashContext<()>, 194 | /// #[rename = "content"] #[description = "The content"] c: String 195 | ///) -> DefaultCommandResult 196 | ///{ 197 | /// ctx.defer(false).await?; 198 | /// ctx.interaction_client.update_response(&ctx.interaction.token) 199 | /// .content(Some(&c)) 200 | /// .unwrap() 201 | /// .await?; 202 | /// Ok(()) 203 | ///} 204 | /// 205 | /// #[tokio::main] 206 | /// async fn main() { 207 | /// let token = std::env::var("DISCORD_TOKEN").unwrap(); 208 | /// let app_id = std::env::var("DISCORD_APP_ID").unwrap().parse::<u64>().unwrap(); 209 | /// let http_client = Client::new(token.clone()); 210 | /// 211 | /// let framework = Framework::builder(http_client, Id::new(app_id), ()) 212 | /// .command(hello_world) 213 | /// .command(repeat_content) 214 | /// .build(); 215 | /// } 216 | /// ``` 217 | pub fn command(mut self, fun: FnPointer<Command<D, T, E>>) -> Self { 218 | let cmd = fun(); 219 | if self.commands.contains_key(cmd.name) || self.groups.contains_key(cmd.name) { 220 | panic!("{} already registered", cmd.name); 221 | } 222 | self.commands.insert(cmd.name, cmd); 223 | self 224 | } 225 | 226 | /// Registers a new group of commands. 227 | pub fn group<F>(mut self, fun: F) -> Self 228 | where 229 | F: FnOnce(&mut GroupParentBuilder<D, T, E>) -> &mut GroupParentBuilder<D, T, E>, 230 | { 231 | let mut builder = GroupParentBuilder::new(); 232 | fun(&mut builder); 233 | let group = builder.build(); 234 | 235 | if self.commands.contains_key(group.name) || self.groups.contains_key(group.name) { 236 | panic!("{} already registered", group.name); 237 | } 238 | self.groups.insert(group.name, group); 239 | 240 | self 241 | } 242 | 243 | /// Builds the framework, returning a [Framework](crate::framework::Framework). 244 | pub fn build(self) -> Framework<D, T, E> { 245 | Framework::from_builder(self) 246 | } 247 | } 248 | 249 | /// A builder of a [group parent](crate::group::GroupParent), see it for documentation. 250 | pub struct GroupParentBuilder<D, T, E> { 251 | name: Option<&'static str>, 252 | description: Option<&'static str>, 253 | kind: ParentType<D, T, E>, 254 | required_permissions: Option<Permissions>, 255 | nsfw: bool, 256 | only_guilds: bool 257 | } 258 | 259 | impl<D, T, E> GroupParentBuilder<D, T, E> { 260 | /// Creates a new builder. 261 | pub(crate) fn new() -> Self { 262 | Self { 263 | name: None, 264 | description: None, 265 | kind: ParentType::Group(Default::default()), 266 | required_permissions: None, 267 | nsfw: false, 268 | only_guilds: false 269 | } 270 | } 271 | 272 | /// Sets the name of this parent group. 273 | pub fn name(&mut self, name: &'static str) -> &mut Self { 274 | self.name = Some(name); 275 | self 276 | } 277 | 278 | /// Sets the description of this parent group. 279 | pub fn description(&mut self, description: &'static str) -> &mut Self { 280 | self.description = Some(description); 281 | self 282 | } 283 | 284 | pub fn required_permissions(&mut self, permissions: Permissions) -> &mut Self { 285 | self.required_permissions = Some(permissions); 286 | self 287 | } 288 | 289 | pub fn nsfw(&mut self, nsfw: bool) -> &mut Self { 290 | self.nsfw = nsfw; 291 | self 292 | } 293 | 294 | pub fn only_guilds(&mut self, only_guilds: bool) -> &mut Self { 295 | self.only_guilds = only_guilds; 296 | self 297 | } 298 | 299 | /// Sets this parent group as a [group](crate::group::ParentType::Group), 300 | /// allowing to create subcommand groups inside of it. 301 | pub fn group<F>(&mut self, fun: F) -> &mut Self 302 | where 303 | F: FnOnce(&mut CommandGroupBuilder<D, T, E>) -> &mut CommandGroupBuilder<D, T, E>, 304 | { 305 | let mut builder = CommandGroupBuilder::new(); 306 | fun(&mut builder); 307 | let built = builder.build(); 308 | 309 | if let ParentType::Group(map) = &mut self.kind { 310 | assert!(!map.contains_key(built.name)); 311 | map.insert(built.name, built); 312 | } else { 313 | let mut map = CommandGroupMap::new(); 314 | map.insert(built.name, built); 315 | self.kind = ParentType::Group(map); 316 | } 317 | self 318 | } 319 | 320 | /// Sets this parent group as [simple](crate::group::ParentType::Simple), only allowing subcommands. 321 | pub fn command(&mut self, fun: FnPointer<Command<D, T, E>>) -> &mut Self { 322 | let command = fun(); 323 | assert!(matches!(command.kind, CommandType::ChatInput), "Only chat commands can be used inside groups"); 324 | if let ParentType::Simple(map) = &mut self.kind { 325 | map.insert(command.name, command); 326 | } else { 327 | let mut map = CommandMap::new(); 328 | map.insert(command.name, command); 329 | self.kind = ParentType::Simple(map); 330 | } 331 | self 332 | } 333 | 334 | /// Builds this parent group, returning a [group parent](crate::group::GroupParent). 335 | pub fn build(self) -> GroupParent<D, T, E> { 336 | assert!(self.name.is_some() && self.description.is_some()); 337 | GroupParent { 338 | name: self.name.unwrap(), 339 | description: self.description.unwrap(), 340 | kind: self.kind, 341 | required_permissions: self.required_permissions, 342 | nsfw: self.nsfw, 343 | only_guilds: self.only_guilds 344 | } 345 | } 346 | } 347 | 348 | /// A builder for a [command group](crate::group::CommandGroup), see it for documentation. 349 | pub struct CommandGroupBuilder<D, T, E> { 350 | name: Option<&'static str>, 351 | description: Option<&'static str>, 352 | subcommands: CommandMap<D, T, E>, 353 | } 354 | 355 | impl<D, T, E> CommandGroupBuilder<D, T, E> { 356 | /// Sets the upper command of this group. 357 | pub fn name(&mut self, name: &'static str) -> &mut Self { 358 | self.name = Some(name); 359 | self 360 | } 361 | 362 | /// Sets the description of this group. 363 | pub fn description(&mut self, description: &'static str) -> &mut Self { 364 | self.description = Some(description); 365 | self 366 | } 367 | 368 | /// Adds a command to this group. 369 | pub fn command(&mut self, fun: FnPointer<Command<D, T, E>>) -> &mut Self { 370 | let command = fun(); 371 | assert!(matches!(command.kind, CommandType::ChatInput), "Only chat commands can be used inside groups"); 372 | self.subcommands.insert(command.name, command); 373 | self 374 | } 375 | 376 | /// Builds the builder into a [group](crate::group::CommandGroup). 377 | pub(crate) fn build(self) -> CommandGroup<D, T, E> { 378 | assert!(self.name.is_some() && self.description.is_some()); 379 | 380 | CommandGroup { 381 | name: self.name.unwrap(), 382 | description: self.description.unwrap(), 383 | subcommands: self.subcommands, 384 | } 385 | } 386 | 387 | /// Creates a new builder. 388 | pub(crate) fn new() -> Self { 389 | Self { 390 | name: None, 391 | description: None, 392 | subcommands: Default::default(), 393 | } 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /vesper/src/command.rs: -------------------------------------------------------------------------------- 1 | use crate::localizations::{Localizations, LocalizationsProvider}; 2 | use crate::prelude::{CreateCommandError, Framework}; 3 | use crate::{ 4 | argument::CommandArgument, context::SlashContext, twilight_exports::Permissions, BoxFuture, framework::ProcessResult, 5 | }; 6 | use std::collections::HashMap; 7 | use tracing::{debug, info}; 8 | use twilight_http::client::InteractionClient; 9 | use twilight_model::id::{marker::GuildMarker, Id}; 10 | use crate::hook::{CheckHook, ErrorHandlerHook}; 11 | use crate::twilight_exports::{Command as TwilightCommand, CommandType}; 12 | 13 | /// A pointer to a command function. 14 | pub(crate) type CommandFn<D, T, E> = for<'cx, 'data> fn(&'cx mut SlashContext<'data, D>) -> BoxFuture<'cx, Result<T, E>>; 15 | /// A map of [commands](self::Command). 16 | pub type CommandMap<D, T, E> = HashMap<&'static str, Command<D, T, E>>; 17 | 18 | #[doc(hidden)] 19 | #[macro_export] 20 | macro_rules! if_some { 21 | ($item:expr, |$x:ident| $($tree:tt)*) => { 22 | if let Some($x) = $item { 23 | $($tree)* 24 | } 25 | }; 26 | } 27 | 28 | /// Information about the execution state of a command. 29 | #[non_exhaustive] 30 | #[derive(Copy, Clone, Debug)] 31 | pub enum ExecutionState { 32 | /// A check had an error. 33 | CheckErrored, 34 | /// A check returned `false` and the command didn't execute. 35 | CheckFailed, 36 | /// The command finished executing without errors. 37 | CommandFinished, 38 | /// The error handler raised an error. 39 | CommandErrored, 40 | /// The `before` hook returned `false` and the command didn't execute. 41 | BeforeHookFailed 42 | } 43 | 44 | /// The location of the output of the command. 45 | #[non_exhaustive] 46 | pub enum OutputLocation<T, E> { 47 | /// The command was not executed, thus there is not any output. 48 | NotExecuted, 49 | /// The output has not been taken by any hook. 50 | Present(Result<T, E>), 51 | /// The output has been forwarded to the `after` hook. 52 | TakenByAfterHook, 53 | /// The output has been taken by the `error_handler` hook. 54 | TakenByErrorHandler 55 | } 56 | 57 | /// Information about the command execution and it's output. 58 | pub struct ExecutionResult<T, E> { 59 | /// The execution state of the command. 60 | pub state: ExecutionState, 61 | /// The output of the command. 62 | pub output: OutputLocation<T, E> 63 | } 64 | 65 | impl<T, E> From<ExecutionResult<T, E>> for ProcessResult<T, E> { 66 | fn from(value: ExecutionResult<T, E>) -> Self { 67 | ProcessResult::CommandExecuted(value) 68 | } 69 | } 70 | 71 | /// A command executed by the framework. 72 | pub struct Command<D, T, E> { 73 | /// The name of the command. 74 | pub name: &'static str, 75 | pub localized_names: Localizations<D, T, E>, 76 | /// The description of the commands. 77 | pub description: &'static str, 78 | pub localized_descriptions: Localizations<D, T, E>, 79 | pub kind: CommandType, 80 | /// All the arguments the command requires. 81 | pub arguments: Vec<CommandArgument<D, T, E>>, 82 | /// A pointer to this command function. 83 | pub fun: CommandFn<D, T, E>, 84 | /// The required permissions to use this command 85 | pub required_permissions: Option<Permissions>, 86 | pub nsfw: bool, 87 | pub only_guilds: bool, 88 | pub checks: Vec<CheckHook<D, E>>, 89 | pub error_handler: Option<ErrorHandlerHook<D, E>> 90 | } 91 | 92 | impl<D, T, E> Command<D, T, E> { 93 | /// Creates a new command. 94 | pub fn new(fun: CommandFn<D, T, E>) -> Self { 95 | Self { 96 | name: Default::default(), 97 | localized_names: Default::default(), 98 | description: Default::default(), 99 | localized_descriptions: Default::default(), 100 | kind: CommandType::ChatInput, 101 | arguments: Default::default(), 102 | fun, 103 | required_permissions: Default::default(), 104 | nsfw: false, 105 | only_guilds: false, 106 | checks: Default::default(), 107 | error_handler: None 108 | } 109 | } 110 | 111 | /// Sets the command name. 112 | pub fn name(mut self, name: &'static str) -> Self { 113 | self.name = name; 114 | self 115 | } 116 | 117 | /// Sets the command description. 118 | pub fn description(mut self, description: &'static str) -> Self { 119 | self.description = description; 120 | self 121 | } 122 | 123 | pub fn kind(mut self, kind: CommandType) -> Self { 124 | self.kind = kind; 125 | self 126 | } 127 | 128 | /// Adds an argument to the command. 129 | pub fn add_argument(mut self, arg: CommandArgument<D, T, E>) -> Self { 130 | self.arguments.push(arg); 131 | self 132 | } 133 | 134 | pub fn checks(mut self, checks: Vec<CheckHook<D, E>>) -> Self { 135 | self.checks = checks; 136 | self 137 | } 138 | 139 | pub fn error_handler(mut self, hook: ErrorHandlerHook<D, E>) -> Self { 140 | self.error_handler = Some(hook); 141 | self 142 | } 143 | 144 | pub fn required_permissions(mut self, permissions: Permissions) -> Self { 145 | self.required_permissions = Some(permissions); 146 | self 147 | } 148 | 149 | pub fn nsfw(mut self, nsfw: bool) -> Self { 150 | self.nsfw = nsfw; 151 | self 152 | } 153 | 154 | pub fn only_guilds(mut self, only_guilds: bool) -> Self { 155 | self.only_guilds = only_guilds; 156 | self 157 | } 158 | 159 | pub fn localized_names<I, K, V>(mut self, iterator: I) -> Self 160 | where 161 | I: IntoIterator<Item = (K, V)>, 162 | K: ToString, 163 | V: ToString 164 | { 165 | self.localized_names 166 | .extend(iterator.into_iter().map(|(k, v)| (k.to_string(), v.to_string()))); 167 | self 168 | } 169 | 170 | pub fn localized_names_fn(mut self, fun: LocalizationsProvider<D, T, E>) -> Self { 171 | self.localized_names.set_provider(fun); 172 | self 173 | } 174 | 175 | pub fn localized_descriptions<I, K, V>(mut self, iterator: I) -> Self 176 | where 177 | I: IntoIterator<Item = (K, V)>, 178 | K: ToString, 179 | V: ToString 180 | { 181 | self.localized_descriptions 182 | .extend(iterator.into_iter().map(|(k, v)| (k.to_string(), v.to_string()))); 183 | self 184 | } 185 | 186 | pub fn localized_descriptions_fn(mut self, fun: LocalizationsProvider<D, T, E>) -> Self { 187 | self.localized_descriptions.set_provider(fun); 188 | self 189 | } 190 | 191 | pub async fn run_checks<'cx, 'data: 'cx>(&self, context: &'cx mut SlashContext<'data, D>) -> Result<bool, E> { 192 | debug!("Running command [{}] checks", self.name); 193 | for check in &self.checks { 194 | if !(check.0)(context).await? { 195 | debug!("Command [{}] check returned false", self.name); 196 | return Ok(false); 197 | } 198 | } 199 | debug!("All command [{}] checks passed", self.name); 200 | Ok(true) 201 | } 202 | 203 | async fn create_chat_command( 204 | &self, 205 | framework: &Framework<D, T, E>, 206 | http: &InteractionClient<'_>, 207 | guild: Option<Id<GuildMarker>> 208 | ) -> Result<TwilightCommand, CreateCommandError> 209 | { 210 | let options = self.arguments.iter() 211 | .map(|a| a.as_option(framework, self)) 212 | .collect::<Vec<_>>(); 213 | 214 | let name_localizations = self.localized_names.get_localizations(framework, &self); 215 | let description_localizations = self.localized_descriptions.get_localizations(framework, &self); 216 | 217 | let model = if let Some(id) = guild { 218 | let mut command = http.create_guild_command(id) 219 | .chat_input(self.name, self.description)? 220 | .command_options(&options)? 221 | .nsfw(self.nsfw); 222 | 223 | if_some!(self.required_permissions, |p| command = command.default_member_permissions(p)); 224 | if_some!(&name_localizations, |n| command = command.name_localizations(n)?); 225 | if_some!(&description_localizations, |d| command = command.description_localizations(d)?); 226 | 227 | command.await?.model().await? 228 | } else { 229 | let mut command = http.create_global_command() 230 | .chat_input(self.name, self.description)? 231 | .command_options(&options)? 232 | .nsfw(self.nsfw) 233 | .dm_permission(!self.only_guilds); 234 | 235 | if_some!(self.required_permissions, |p| command = command.default_member_permissions(p)); 236 | if_some!(&name_localizations, |n| command = command.name_localizations(n)?); 237 | if_some!(&description_localizations, |d| command = command.description_localizations(d)?); 238 | 239 | command.await?.model().await? 240 | }; 241 | 242 | Ok(model) 243 | } 244 | 245 | async fn create_user_command( 246 | &self, 247 | http: &InteractionClient<'_>, 248 | guild: Option<Id<GuildMarker>> 249 | ) -> Result<TwilightCommand, CreateCommandError> 250 | { 251 | let model = if let Some(id) = guild { 252 | let mut command = http.create_guild_command(id) 253 | .user(self.name)? 254 | .nsfw(self.nsfw); 255 | 256 | if_some!(self.required_permissions, |p| command = command.default_member_permissions(p)); 257 | 258 | command.await?.model().await? 259 | } else { 260 | let mut command = http.create_global_command() 261 | .user(self.name)? 262 | .nsfw(self.nsfw) 263 | .dm_permission(!self.only_guilds); 264 | 265 | if_some!(self.required_permissions, |p| command = command.default_member_permissions(p)); 266 | 267 | command.await?.model().await? 268 | }; 269 | 270 | Ok(model) 271 | } 272 | 273 | async fn create_message_command( 274 | &self, 275 | http: &InteractionClient<'_>, 276 | guild: Option<Id<GuildMarker>> 277 | ) -> Result<TwilightCommand, CreateCommandError> 278 | { 279 | let model = if let Some(id) = guild { 280 | let mut command = http.create_guild_command(id) 281 | .message(self.name)? 282 | .nsfw(self.nsfw); 283 | 284 | if_some!(self.required_permissions, |p| command = command.default_member_permissions(p)); 285 | 286 | command.await?.model().await? 287 | } else { 288 | let mut command = http.create_global_command() 289 | .message(self.name)? 290 | .nsfw(self.nsfw) 291 | .dm_permission(!self.only_guilds); 292 | 293 | if_some!(self.required_permissions, |p| command = command.default_member_permissions(p)); 294 | 295 | command.await?.model().await? 296 | }; 297 | 298 | Ok(model) 299 | } 300 | 301 | pub async fn create( 302 | &self, 303 | framework: &Framework<D, T, E>, 304 | http: &InteractionClient<'_>, 305 | guild: Option<Id<GuildMarker>> 306 | ) -> Result<TwilightCommand, CreateCommandError> 307 | { 308 | match self.kind { 309 | CommandType::ChatInput => self.create_chat_command(framework, http, guild).await, 310 | CommandType::Message => self.create_message_command(http, guild).await, 311 | CommandType::User => self.create_user_command(http, guild).await, 312 | _ => panic!("Invalid command type") 313 | } 314 | } 315 | 316 | pub async fn execute<'cx, 'data: 'cx>(&self, context: &'cx mut SlashContext<'data, D>) -> ExecutionResult<T, E> { 317 | let state; 318 | let location; 319 | 320 | match self.run_checks(context).await { 321 | Ok(true) => { 322 | debug!("Executing command [{}]", self.name); 323 | let output = (self.fun)(context).await; 324 | 325 | match (&self.error_handler, output) { 326 | (Some(hook), Err(why)) => { 327 | info!("Command [{}] raised an error, using established error handler", self.name); 328 | state = ExecutionState::CommandErrored; 329 | location = OutputLocation::TakenByErrorHandler; 330 | 331 | (hook.0)(context, why).await; 332 | }, 333 | (_, Ok(res)) => { 334 | debug!("Command [{}] executed successfully", self.name); 335 | state = ExecutionState::CommandFinished; 336 | location = OutputLocation::Present(Ok(res)); 337 | }, 338 | (_, Err(res)) => { 339 | info!("Command [{}] raised an error, but no error handler was established", self.name); 340 | state = ExecutionState::CommandErrored; 341 | location = OutputLocation::Present(Err(res)); 342 | } 343 | }; 344 | }, 345 | Err(why) => { 346 | state = ExecutionState::CheckErrored; 347 | // If the command has an error handler, execute it, if not, discard the error. 348 | if let Some(hook) = &self.error_handler { 349 | info!("Command [{}] check raised an error, using established error handler", self.name); 350 | (hook.0)(context, why).await; 351 | location = OutputLocation::TakenByErrorHandler; 352 | } else { 353 | info!("Command [{}] check raised an error, but no error handler was established", self.name); 354 | location = OutputLocation::Present(Err(why)); 355 | } 356 | }, 357 | _ => { 358 | state = ExecutionState::CheckFailed; 359 | location = OutputLocation::NotExecuted; 360 | } 361 | } 362 | 363 | ExecutionResult { 364 | state, 365 | output: location 366 | } 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /vesper/src/context.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::Mutex; 2 | use twilight_model::channel::message::MessageFlags; 3 | use crate::{ 4 | builder::WrappedClient, 5 | twilight_exports::*, 6 | wait::{InteractionWaiter, WaiterWaker} 7 | }; 8 | 9 | use crate::modal::{Modal, WaitModal}; 10 | use crate::wait::new_pair; 11 | 12 | /// The value the user is providing to the argument. 13 | #[derive(Debug, Clone)] 14 | pub struct Focused { 15 | pub input: String, 16 | pub kind: CommandOptionType, 17 | } 18 | 19 | /// Context given to all functions used to autocomplete arguments. 20 | pub struct AutocompleteContext<'a, D> { 21 | /// The http client used by the framework. 22 | pub http_client: &'a WrappedClient, 23 | /// The data shared across the framework. 24 | pub data: &'a D, 25 | /// The user input. 26 | pub user_input: Focused, 27 | /// The interaction itself. 28 | pub interaction: &'a mut Interaction, 29 | } 30 | 31 | impl<'a, D> AutocompleteContext<'a, D> { 32 | pub(crate) fn new( 33 | http_client: &'a WrappedClient, 34 | data: &'a D, 35 | user_input: Focused, 36 | interaction: &'a mut Interaction, 37 | ) -> Self { 38 | Self { 39 | http_client, 40 | data, 41 | user_input, 42 | interaction, 43 | } 44 | } 45 | 46 | /// Gets the http client used by the framework. 47 | pub fn http_client(&self) -> &Client { 48 | self.http_client.inner() 49 | } 50 | } 51 | 52 | /// Framework context given to all command functions, this struct contains all the necessary 53 | /// items to respond the interaction and access shared data. 54 | pub struct SlashContext<'a, D> { 55 | /// The http client used by the framework. 56 | pub http_client: &'a WrappedClient, 57 | /// The application id provided to the framework. 58 | pub application_id: Id<ApplicationMarker>, 59 | /// An [interaction client](InteractionClient) made out of the framework's [http client](Client) 60 | pub interaction_client: InteractionClient<'a>, 61 | /// The data shared across the framework. 62 | pub data: &'a D, 63 | /// Components waiting for an interaction. 64 | pub waiters: &'a Mutex<Vec<WaiterWaker>>, 65 | /// The interaction itself. 66 | pub interaction: Interaction, 67 | } 68 | 69 | impl<'a, D> Clone for SlashContext<'a, D> { 70 | fn clone(&self) -> Self { 71 | SlashContext { 72 | http_client: self.http_client, 73 | application_id: self.application_id, 74 | interaction_client: self.http_client.inner().interaction(self.application_id), 75 | data: self.data, 76 | waiters: self.waiters, 77 | interaction: self.interaction.clone(), 78 | } 79 | } 80 | } 81 | 82 | impl<'a, D> SlashContext<'a, D> { 83 | /// Creates a new context. 84 | pub(crate) fn new( 85 | http_client: &'a WrappedClient, 86 | application_id: Id<ApplicationMarker>, 87 | data: &'a D, 88 | waiters: &'a Mutex<Vec<WaiterWaker>>, 89 | interaction: Interaction, 90 | ) -> Self { 91 | let interaction_client = http_client.inner().interaction(application_id); 92 | Self { 93 | http_client, 94 | application_id, 95 | interaction_client, 96 | data, 97 | waiters, 98 | interaction, 99 | } 100 | } 101 | 102 | /// Gets the http client used by the framework. 103 | pub fn http_client(&self) -> &Client { 104 | self.http_client.inner() 105 | } 106 | 107 | /// Gets a mutable reference to the [interaction](Interaction) owned by the context. 108 | #[deprecated(since = "0.12.0", note = "Use the `interaction` field directly with a mutable context")] 109 | pub fn interaction_mut(&mut self) -> &mut Interaction { 110 | &mut self.interaction 111 | } 112 | 113 | /// Defers the interaction, allowing to respond later. 114 | /// 115 | /// # Examples 116 | /// 117 | /// ```rust 118 | /// use vesper::prelude::*; 119 | /// 120 | /// #[command] 121 | /// #[description = "My command description"] 122 | /// async fn my_command(ctx: &SlashContext<()>) -> DefaultCommandResult { 123 | /// // Defer the interaction, this way we can respond to it later. 124 | /// ctx.defer(false).await?; 125 | /// 126 | /// // Do something here 127 | /// 128 | /// // Now edit the interaction 129 | /// ctx.interaction_client.update_response(&ctx.interaction.token) 130 | /// .content(Some("Hello world")) 131 | /// .unwrap() 132 | /// .await?; 133 | /// 134 | /// Ok(()) 135 | /// } 136 | /// ``` 137 | pub async fn defer(&self, ephemeral: bool) -> Result<(), twilight_http::Error> { 138 | self.interaction_client 139 | .create_response( 140 | self.interaction.id, 141 | &self.interaction.token, 142 | &InteractionResponse { 143 | kind: InteractionResponseType::DeferredChannelMessageWithSource, 144 | data: if ephemeral { 145 | Some(InteractionResponseData { 146 | flags: Some(MessageFlags::EPHEMERAL), 147 | ..Default::default() 148 | }) 149 | } else { 150 | None 151 | }, 152 | }, 153 | ) 154 | .await?; 155 | 156 | Ok(()) 157 | } 158 | 159 | /// Creates a modal that will be prompted to the user in discord, returning a [`WaitModal`] that 160 | /// can be `.await`ed to retrieve the user input. If the returned [`WaitModal`] is not awaited, 161 | /// the modal will not close when submitted and the user won't be able to submit the modal. 162 | /// 163 | /// # Examples 164 | /// 165 | /// ```rust 166 | /// use vesper::prelude::*; 167 | /// 168 | /// #[derive(Debug, Modal)] 169 | /// struct MyModal { 170 | /// #[modal(paragraph)] 171 | /// field: String 172 | /// } 173 | /// 174 | /// #[command] 175 | /// #[description = "My command description"] 176 | /// async fn my_command(ctx: &SlashContext<()>) -> DefaultCommandResult { 177 | /// let modal = ctx.create_modal::<MyModal>().await?; 178 | /// 179 | /// // Here we can do something quick. 180 | /// 181 | /// // Now we await the modal, allowing the user to submit the modal and getting the data 182 | /// let data = modal.await?; 183 | /// 184 | /// Ok(()) 185 | /// } 186 | /// ``` 187 | /// 188 | /// [`WaitModal`]: WaitModal 189 | pub async fn create_modal<M>(&self) -> Result<WaitModal<M>, twilight_http::Error> 190 | where 191 | M: Modal<D> 192 | { 193 | let modal_id = self.interaction.id.to_string(); 194 | self.interaction_client.create_response( 195 | self.interaction.id, 196 | &self.interaction.token, 197 | &M::create(self, modal_id.clone()) 198 | ).await?; 199 | 200 | let waiter = self.wait_interaction(move |interaction| { 201 | let Some(InteractionData::ModalSubmit(data)) = &interaction.data else { 202 | return false; 203 | }; 204 | 205 | data.custom_id == modal_id 206 | }); 207 | 208 | Ok(WaitModal::new(waiter, &self.interaction_client, M::parse)) 209 | } 210 | 211 | /// Returns a waiter used to wait for a specific interaction which satisfies the provided 212 | /// closure. 213 | pub fn wait_interaction<F>(&self, fun: F) -> InteractionWaiter 214 | where 215 | F: Fn(&Interaction) -> bool + Send + 'static 216 | { 217 | let (waker, waiter) = new_pair(fun); 218 | let mut lock = self.waiters.lock(); 219 | lock.push(waker); 220 | waiter 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /vesper/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | use twilight_validate::command::CommandValidationError; 3 | use twilight_http::{Error as HttpError, response::DeserializeBodyError}; 4 | 5 | #[non_exhaustive] 6 | #[derive(Debug, Error)] 7 | #[error(transparent)] 8 | pub enum CreateCommandError { 9 | Validation(#[from] CommandValidationError), 10 | Http(#[from] HttpError), 11 | Deserialize(#[from] DeserializeBodyError) 12 | } 13 | -------------------------------------------------------------------------------- /vesper/src/extract.rs: -------------------------------------------------------------------------------- 1 | mod sealed { 2 | pub trait Sealed {} 3 | impl<T, E> Sealed for Result<T, E> {} 4 | impl<T> Sealed for Option<T> {} 5 | 6 | pub trait SealedDataOption: Sized {} 7 | impl SealedDataOption for String {} 8 | impl SealedDataOption for Option<String> {} 9 | } 10 | 11 | /// Defines what items are allowed to be returned from a command function. Since a command 12 | /// function must return a `Result<T, E>`, this trait is only implemented for that type. 13 | pub trait Returnable: sealed::Sealed { 14 | type Ok; 15 | type Err; 16 | } 17 | 18 | /// Used in the [`after hook`] to determine the inner item, which is required to implement the 19 | /// [returnable] trait. 20 | /// 21 | /// [returnable]: self::Returnable 22 | /// [`after hook`]: crate::hook::AfterHook 23 | pub trait Optional: sealed::Sealed { 24 | type Inner; 25 | } 26 | 27 | /// Defines what data types can be used when creating a modal. 28 | pub trait ModalDataOption: sealed::SealedDataOption { 29 | fn required() -> bool; 30 | fn parse(item: Option<String>) -> Self; 31 | } 32 | 33 | impl<T, E> Returnable for Result<T, E> { 34 | type Ok = T; 35 | type Err = E; 36 | } 37 | 38 | impl<T> Optional for Option<T> { 39 | type Inner = T; 40 | } 41 | 42 | impl ModalDataOption for Option<String> { 43 | fn required() -> bool { 44 | false 45 | } 46 | 47 | fn parse(item: Option<String>) -> Self { 48 | if item.as_ref()?.is_empty() { 49 | None 50 | } else { 51 | item 52 | } 53 | } 54 | } 55 | 56 | impl ModalDataOption for String { 57 | fn required() -> bool { 58 | true 59 | } 60 | 61 | fn parse(item: Option<String>) -> Self { 62 | item.expect("Item can't be null") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /vesper/src/group.rs: -------------------------------------------------------------------------------- 1 | use twilight_http::client::InteractionClient; 2 | use twilight_model::{id::{marker::GuildMarker, Id}, application::command::{CommandOption, CommandOptionType}}; 3 | 4 | use crate::{ 5 | command::{CommandMap, Command}, 6 | twilight_exports::{Command as TwilightCommand, Permissions}, prelude::{CreateCommandError, Framework}, 7 | }; 8 | use std::collections::HashMap; 9 | 10 | /// A map of [parent groups](self::GroupParent). 11 | pub type GroupParentMap<D, T, E> = HashMap<&'static str, GroupParent<D, T, E>>; 12 | /// A map of [command groups](self::CommandGroup). 13 | pub type CommandGroupMap<D, T, E> = HashMap<&'static str, CommandGroup<D, T, E>>; 14 | 15 | /// Types a [group parent](self::GroupParent) can be. 16 | pub enum ParentType<D, T, E> { 17 | /// Simple, the group only has subcommands. 18 | Simple(CommandMap<D, T, E>), 19 | /// Group, the group has other groups inside of it. 20 | Group(CommandGroupMap<D, T, E>), 21 | } 22 | 23 | impl<D, T, E> ParentType<D, T, E> { 24 | /// Tries to get the [`map`](crate::command::CommandMap) of the given 25 | /// [parent type](self::ParentType), returning `Some` if the parent variant is 26 | /// [`simple`](self::ParentType::Simple). 27 | pub fn as_simple(&self) -> Option<&CommandMap<D, T, E>> { 28 | match self { 29 | Self::Simple(map) => Some(map), 30 | _ => None, 31 | } 32 | } 33 | 34 | /// Tries to get the [`group`](self::CommandGroupMap) of the given [parent type](self::ParentType), 35 | /// returning `Some` if the parent variant is a [`group`](self::ParentType::Group). 36 | pub fn as_group(&self) -> Option<&CommandGroupMap<D, T, E>> { 37 | match self { 38 | Self::Group(group) => Some(group), 39 | _ => None, 40 | } 41 | } 42 | } 43 | 44 | /// A parent of a group of sub commands, either a 45 | /// map of [commands](crate::command::Command) referred by discord as `SubCommand` 46 | /// or a map of [groups](self::CommandGroup) referred by discord as `SubCommandGroup`. 47 | pub struct GroupParent<D, T, E> { 48 | /// The name of the upper command 49 | /// 50 | /// e.g.: /parent <subcommand..> 51 | /// 52 | /// where `parent` is `name`. 53 | pub name: &'static str, 54 | /// The description of the upper command. 55 | pub description: &'static str, 56 | /// This parent group child commands. 57 | pub kind: ParentType<D, T, E>, 58 | /// The required permissions to execute commands inside this group 59 | pub required_permissions: Option<Permissions>, 60 | pub nsfw: bool, 61 | pub only_guilds: bool 62 | } 63 | 64 | /// A group of commands, referred by discord as `SubCommandGroup`. 65 | pub struct CommandGroup<D, T, E> { 66 | /// The upper command 67 | /// 68 | /// e.g.: /parent command <subcommand..> <options..> 69 | /// 70 | /// where `command` is `name`. 71 | pub name: &'static str, 72 | /// The description of this group. 73 | pub description: &'static str, 74 | /// The commands this group has as children. 75 | pub subcommands: CommandMap<D, T, E>, 76 | } 77 | 78 | impl<D, T, E> GroupParent<D, T, E> { 79 | pub async fn create( 80 | &self, 81 | framework: &Framework<D, T, E>, 82 | http: &InteractionClient<'_>, 83 | guild: Option<Id<GuildMarker>> 84 | ) -> Result<TwilightCommand, CreateCommandError> 85 | { 86 | let options = self.get_options(framework); 87 | 88 | let model = if let Some(id) = guild { 89 | let mut command = http.create_guild_command(id) 90 | .chat_input(self.name, self.description)? 91 | .command_options(&options)? 92 | .nsfw(self.nsfw); 93 | 94 | crate::if_some!(self.required_permissions, |p| command = command.default_member_permissions(p)); 95 | 96 | command.await?.model().await? 97 | } else { 98 | let mut command = http.create_global_command() 99 | .chat_input(self.name, self.description)? 100 | .command_options(&options)? 101 | .nsfw(self.nsfw) 102 | .dm_permission(!self.only_guilds); 103 | 104 | crate::if_some!(self.required_permissions, |p| command = command.default_member_permissions(p)); 105 | 106 | command.await?.model().await? 107 | }; 108 | 109 | Ok(model) 110 | } 111 | 112 | pub fn get_options(&self, f: &Framework<D, T, E>) -> Vec<CommandOption> { 113 | if let ParentType::Group(groups) = &self.kind { 114 | let mut subgroups = Vec::new(); 115 | 116 | for group in groups.values() { 117 | let mut subcommands = Vec::new(); 118 | 119 | for cmd in group.subcommands.values() { 120 | subcommands.push(self.create_subcommand(f, cmd)); 121 | } 122 | 123 | subgroups.push(CommandOption { 124 | kind: CommandOptionType::SubCommandGroup, 125 | name: group.name.to_string(), 126 | description: group.description.to_string(), 127 | options: Some(subcommands), 128 | autocomplete: None, 129 | choices: None, 130 | required: None, 131 | channel_types: None, 132 | description_localizations: None, 133 | max_length: None, 134 | max_value: None, 135 | min_length: None, 136 | min_value: None, 137 | name_localizations: None, 138 | }); 139 | } 140 | 141 | subgroups 142 | } else if let ParentType::Simple(commands) = &self.kind { 143 | let mut subcommands = Vec::new(); 144 | for sub in commands.values() { 145 | subcommands.push(self.create_subcommand(f, sub)); 146 | } 147 | 148 | subcommands 149 | } else { 150 | unreachable!() 151 | } 152 | } 153 | 154 | /// Creates a subcommand at the given scope. 155 | fn create_subcommand(&self, f: &Framework<D, T, E>, cmd: &Command<D, T, E>) -> CommandOption { 156 | CommandOption { 157 | kind: CommandOptionType::SubCommand, 158 | name: cmd.name.to_string(), 159 | description: cmd.description.to_string(), 160 | options: Some(cmd.arguments.iter().map(|a| a.as_option(f, cmd)).collect()), 161 | autocomplete: None, 162 | choices: None, 163 | required: None, 164 | channel_types: None, 165 | description_localizations: cmd.localized_descriptions.get_localizations(f, cmd), 166 | max_length: None, 167 | max_value: None, 168 | min_length: None, 169 | min_value: None, 170 | name_localizations: cmd.localized_names.get_localizations(f, cmd), 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /vesper/src/hook.rs: -------------------------------------------------------------------------------- 1 | use crate::context::AutocompleteContext; 2 | use crate::{ 3 | context::SlashContext, twilight_exports::InteractionResponseData, 4 | BoxFuture, 5 | }; 6 | 7 | /// A pointer to a function used by [before hook](BeforeHook). 8 | pub(crate) type BeforeFn<D> = for<'cx, 'data> fn(&'cx mut SlashContext<'data, D>, &'cx str) -> BoxFuture<'cx, bool>; 9 | /// A hook executed before a command execution. 10 | /// 11 | /// The function must have as parameters a [slash context] reference and a `&str` 12 | /// which contains the name of the command to execute. 13 | /// 14 | /// [slash context]: SlashContext 15 | pub struct BeforeHook<D>(pub BeforeFn<D>); 16 | 17 | /// A pointer to a function used by [after hook](AfterHook). 18 | pub(crate) type AfterFn<D, T, E> = 19 | for<'cx, 'data> fn(&'cx mut SlashContext<'data, D>, &'cx str, Option<Result<T, E>>) -> BoxFuture<'cx, ()>; 20 | 21 | /// A hook executed after a command execution. 22 | /// 23 | /// The function must have as parameters a [slash context] reference, a `&str` which contains 24 | /// the name of the command, and an `Option<Result<T, E>>`. 25 | /// 26 | /// The result contained in the option must be the same as your command's output. 27 | /// 28 | /// Note that it will be missing only if the command had an error and an error handler was set 29 | /// to handle the error. 30 | /// 31 | /// [slash context]: SlashContext 32 | pub struct AfterHook<D, T, E>(pub AfterFn<D, T, E>); 33 | 34 | /// A pointer to a function used by [autocomplete hook](AutocompleteHook). 35 | pub(crate) type AutocompleteFn<D> = 36 | for<'data> fn(AutocompleteContext<'data, D>) -> BoxFuture<'data, Option<InteractionResponseData>>; 37 | 38 | /// A hook used to suggest inputs to the command caller. 39 | /// 40 | /// The function must have as parameter a single [autocomplete context](AutocompleteContext). 41 | pub struct AutocompleteHook<D>(pub AutocompleteFn<D>); 42 | 43 | /// A pointer to a function used by the [check hook](CheckHook). 44 | pub(crate) type CheckFn<D, E> = for<'cx, 'data> fn(&'cx mut SlashContext<'data, D>) -> BoxFuture<'cx, Result<bool, E>>; 45 | 46 | /// A hook that can be used to determine if a command should execute or not depending 47 | /// on the given function. 48 | pub struct CheckHook<D, E>(pub CheckFn<D, E>); 49 | 50 | /// A pointer to a function used by the [error handler hook](ErrorHandlerHook). 51 | pub(crate) type ErrorHandlerFn<D, E> = for<'cx, 'data> fn(&'cx mut SlashContext<'data, D>, E) -> BoxFuture<'cx, ()>; 52 | 53 | /// A hook that can be used to handle errors of an specific command and its checks. 54 | /// 55 | /// The function must have as parameters a [slash context] reference and the actual error type 56 | /// the function and check is supposed to return. 57 | /// 58 | /// [slash context]: SlashContext 59 | pub struct ErrorHandlerHook<D, E>(pub ErrorHandlerFn<D, E>); 60 | -------------------------------------------------------------------------------- /vesper/src/iter.rs: -------------------------------------------------------------------------------- 1 | use crate::builder::WrappedClient; 2 | use crate::context::SlashContext; 3 | use crate::parse::{Parse, ParseError}; 4 | use crate::twilight_exports::{InteractionData, CommandDataOption, CommandOptionType, CommandOptionValue, CommandInteractionDataResolved}; 5 | 6 | /// An iterator used to iterate through slash command options. 7 | pub struct DataIterator<'a, D> { 8 | src: Vec<&'a CommandDataOption>, 9 | resolved: &'a mut Option<CommandInteractionDataResolved>, 10 | http: &'a WrappedClient, 11 | data: &'a D 12 | } 13 | 14 | impl<'a, D> DataIterator<'a, D> { 15 | /// Creates a new [iterator](self::DataIterator) at the given source. 16 | pub fn new(ctx: &'a mut SlashContext<'_, D>) -> Self { 17 | let data = match ctx.interaction.data.as_mut().unwrap() { 18 | InteractionData::ApplicationCommand(data) => data, 19 | _ => unreachable!() 20 | }; 21 | 22 | Self { 23 | src: Self::get_data(&data.options), 24 | resolved: &mut data.resolved, 25 | http: ctx.http_client, 26 | data: ctx.data 27 | } 28 | } 29 | } 30 | 31 | impl<'a, D: 'a> DataIterator<'a, D> { 32 | /// Gets the first value which satisfies the given predicate. 33 | pub fn get<F>(&mut self, predicate: F) -> Option<&'a CommandDataOption> 34 | where 35 | F: Fn(&CommandDataOption) -> bool, 36 | { 37 | let i = { 38 | let mut idx = 0; 39 | let mut found = false; 40 | 41 | while idx < self.src.len() && !found { 42 | if predicate(self.src[idx]) { 43 | found = true; 44 | } 45 | 46 | if !found { 47 | idx += 1; 48 | } 49 | } 50 | 51 | if found { 52 | Some(idx) 53 | } else { 54 | None 55 | } 56 | }; 57 | 58 | if let Some(i) = i { 59 | Some(self.src.remove(i)) 60 | } else { 61 | None 62 | } 63 | } 64 | 65 | pub fn resolved(&mut self) -> Option<&mut CommandInteractionDataResolved> { 66 | self.resolved.as_mut() 67 | } 68 | 69 | fn get_data(options: &Vec<CommandDataOption>) -> Vec<&CommandDataOption> { 70 | if let Some(index) = options.iter().position(|item| { 71 | item.value.kind() == CommandOptionType::SubCommand 72 | || item.value.kind() == CommandOptionType::SubCommandGroup 73 | }) 74 | { 75 | let item = options.get(index).unwrap(); 76 | match &item.value { 77 | CommandOptionValue::SubCommandGroup(g) 78 | | CommandOptionValue::SubCommand(g) => Self::get_data(g), 79 | _ => unreachable!() 80 | } 81 | } else { 82 | options.iter() 83 | .collect() 84 | } 85 | } 86 | } 87 | 88 | impl<'a, D> DataIterator<'a, D> 89 | where 90 | D: Send + Sync 91 | { 92 | pub async fn named_parse<T>(&mut self, name: &str) -> Result<T, ParseError> 93 | where 94 | T: Parse<D> 95 | { 96 | let value = self.get(|s| s.name == name); 97 | if value.is_none() && <T as Parse<D>>::required() { 98 | Err(ParseError::StructureMismatch(format!("{} not found", name)).into()) 99 | } else { 100 | Ok(T::parse( 101 | self.http, 102 | self.data, 103 | value.map(|it| &it.value), 104 | self.resolved()) 105 | .await 106 | .map_err(|mut err| { 107 | if let ParseError::Parsing { argument_name, .. } = &mut err { 108 | *argument_name = name.to_string(); 109 | } 110 | err 111 | })?) 112 | } 113 | } 114 | } 115 | 116 | impl<'a, D> std::ops::Deref for DataIterator<'a, D> { 117 | type Target = Vec<&'a CommandDataOption>; 118 | 119 | fn deref(&self) -> &Self::Target { 120 | &self.src 121 | } 122 | } 123 | 124 | impl<D> std::ops::DerefMut for DataIterator<'_, D> { 125 | fn deref_mut(&mut self) -> &mut Self::Target { 126 | &mut self.src 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /vesper/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | mod parse_impl; 4 | 5 | pub mod argument; 6 | pub mod builder; 7 | pub mod command; 8 | pub mod context; 9 | pub mod error; 10 | pub mod framework; 11 | pub mod group; 12 | pub mod hook; 13 | pub mod iter; 14 | pub mod localizations; 15 | pub mod modal; 16 | pub mod parse; 17 | pub mod parsers; 18 | pub mod range; 19 | pub mod wait; 20 | 21 | // Items used to extract generics from functions, not public API. 22 | #[doc(hidden)] 23 | pub mod extract; 24 | 25 | pub use vesper_macros as macros; 26 | 27 | type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>; 28 | 29 | /// Useful exports to get started quickly 30 | pub mod prelude { 31 | pub use crate::{ 32 | builder::{FrameworkBuilder, WrappedClient}, 33 | context::{AutocompleteContext, Focused, SlashContext}, 34 | error::*, 35 | framework::{DefaultCommandResult, Framework}, 36 | modal::*, 37 | parse::{Parse, ParseError}, 38 | parsers, 39 | range::Range, 40 | }; 41 | pub use async_trait::async_trait; 42 | pub use vesper_macros::*; 43 | } 44 | 45 | #[doc(hidden)] 46 | pub mod twilight_exports { 47 | pub use twilight_http::{ 48 | client::{Client, InteractionClient}, 49 | request::application::interaction::UpdateResponse, 50 | response::DeserializeBodyError 51 | }; 52 | pub use twilight_model::{ 53 | application::{ 54 | command::{Command, CommandOption, CommandOptionChoice, CommandOptionChoiceValue, CommandOptionType, CommandType}, 55 | interaction::{ 56 | application_command::{CommandData, CommandDataOption, CommandOptionValue, CommandInteractionDataResolved}, 57 | modal::ModalInteractionData, 58 | message_component::MessageComponentInteractionData, 59 | Interaction, InteractionData, InteractionType, 60 | }, 61 | }, 62 | channel::{Message, message::{Component, component::{ActionRow, TextInput, TextInputStyle}}}, 63 | gateway::payload::incoming::InteractionCreate, 64 | guild::Permissions, 65 | http::interaction::{ 66 | InteractionResponse, InteractionResponseData, InteractionResponseType, 67 | }, 68 | id::{ 69 | marker::{ 70 | ApplicationMarker, AttachmentMarker, ChannelMarker, GenericMarker, GuildMarker, 71 | MessageMarker, RoleMarker, UserMarker, 72 | }, 73 | Id, 74 | }, 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /vesper/src/localizations.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{prelude::Framework, command::Command, if_some}; 4 | 5 | pub(crate) type LocalizationsProvider<D, T, E> = fn(&Framework<D, T, E>, &Command<D, T, E>) -> HashMap<String, String>; 6 | 7 | pub struct Localizations<D, T, E> { 8 | map: HashMap<String, String>, 9 | provider: Option<LocalizationsProvider<D, T, E>>, 10 | } 11 | 12 | impl<D, T, E> Default for Localizations<D, T, E> { 13 | fn default() -> Self { 14 | Self { 15 | map: HashMap::new(), 16 | provider: None 17 | } 18 | } 19 | } 20 | 21 | impl<D, T, E> Localizations<D, T, E> { 22 | pub fn get_localizations( 23 | &self, 24 | framework: &Framework<D, T, E>, 25 | command: &Command<D, T, E> 26 | ) -> Option<HashMap<String, String>> { 27 | let mut localizations = self.map.clone(); 28 | if_some!(&self.provider, |fun| localizations.extend(fun(framework, command))); 29 | 30 | if localizations.is_empty() { 31 | None 32 | } else { 33 | Some(localizations) 34 | } 35 | } 36 | 37 | pub fn extend<Iter, K, V>(&mut self, iter: Iter) 38 | where 39 | Iter: IntoIterator<Item = (K, V)>, 40 | K: ToString, 41 | V: ToString 42 | { 43 | self.map.extend(iter.into_iter().map(|(k, v)| (k.to_string(), v.to_string()))); 44 | } 45 | 46 | pub fn set_provider(&mut self, provider: LocalizationsProvider<D, T, E>) { 47 | self.provider = Some(provider); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /vesper/src/modal.rs: -------------------------------------------------------------------------------- 1 | use std::future::{Future, IntoFuture}; 2 | use std::marker::PhantomData; 3 | use std::pin::Pin; 4 | use std::task::{Context, Poll, ready}; 5 | use thiserror::Error; 6 | use tokio::sync::oneshot::error::RecvError; 7 | use twilight_model::channel::message::MessageFlags; 8 | use crate::context::SlashContext; 9 | use crate::wait::InteractionWaiter; 10 | use crate::twilight_exports::{Interaction, InteractionClient, InteractionResponse, InteractionResponseType, InteractionResponseData}; 11 | use std::fmt::{Debug, Formatter}; 12 | use twilight_http::response::marker::EmptyBody; 13 | use twilight_http::response::ResponseFuture; 14 | 15 | 16 | /// Errors that can be returned when awaiting modals. 17 | #[derive(Debug, Error)] 18 | #[error(transparent)] 19 | pub enum ModalError { 20 | /// An http error occurred. 21 | Http(#[from] twilight_http::Error), 22 | /// Something failed when using a [waiter](InteractionWaiter) 23 | Waiter(#[from] RecvError) 24 | } 25 | 26 | /// The outcome of `.await`ing a [WaitModal](WaitModal). 27 | /// 28 | /// This structure provides both the parsed modal and the interaction used to retrieve it. 29 | pub struct ModalOutcome<S> { 30 | /// The inner parsed modal. 31 | pub inner: S, 32 | /// The interaction used to retrieve the modal. 33 | pub interaction: Interaction 34 | } 35 | 36 | impl<S: Debug> Debug for ModalOutcome<S> { 37 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 38 | <S as Debug>::fmt(&self.inner, f) 39 | } 40 | } 41 | 42 | impl<S> std::ops::Deref for ModalOutcome<S> { 43 | type Target = S; 44 | 45 | fn deref(&self) -> &Self::Target { 46 | &self.inner 47 | } 48 | } 49 | 50 | impl<S> std::ops::DerefMut for ModalOutcome<S> { 51 | fn deref_mut(&mut self) -> &mut Self::Target { 52 | &mut self.inner 53 | } 54 | } 55 | 56 | /// A waiter used to retrieve the input of a command. This can be obtained by using 57 | /// [SlashContext::create_modal](SlashContext::create_modal). 58 | /// 59 | /// To retrieve the input of the modal, `.await` the waiter. 60 | /// 61 | /// If the waiter is never awaited, the user won't be able to submit the modal, and will have to 62 | /// close it without submitting. 63 | #[must_use = "Modals cannot be submitted if the waiter is not awaited"] 64 | pub struct WaitModal<'ctx, S> { 65 | pub(crate) waiter: Option<InteractionWaiter>, 66 | pub(crate) interaction: Option<Interaction>, 67 | pub(crate) http_client: &'ctx InteractionClient<'ctx>, 68 | pub(crate) flags: Option<MessageFlags>, 69 | pub(crate) response_type: InteractionResponseType, 70 | pub(crate) acknowledge: Option<ResponseFuture<EmptyBody>>, 71 | pub(crate) parse_fn: fn(&mut Interaction) -> S, 72 | pub(crate) _marker: PhantomData<S>, 73 | } 74 | 75 | impl<'ctx, S> WaitModal<'ctx, S> { 76 | pub(crate) fn new( 77 | waiter: InteractionWaiter, 78 | http_client: &'ctx InteractionClient<'ctx>, 79 | parse_fn: fn(&mut Interaction) -> S, 80 | ) -> WaitModal<'ctx, S> 81 | { 82 | Self { 83 | waiter: Some(waiter), 84 | interaction: None, 85 | http_client, 86 | flags: None, 87 | response_type: InteractionResponseType::DeferredUpdateMessage, 88 | acknowledge: None, 89 | parse_fn, 90 | _marker: PhantomData, 91 | } 92 | } 93 | 94 | pub fn set_flags(mut self, flags: MessageFlags) -> Self { 95 | self.flags = Some(flags); 96 | self 97 | } 98 | 99 | pub fn set_ephemeral(self) -> Self { 100 | self.set_flags(MessageFlags::EPHEMERAL) 101 | } 102 | 103 | pub fn defer_response(mut self) -> Self { 104 | self.response_type = InteractionResponseType::DeferredChannelMessageWithSource; 105 | self 106 | } 107 | 108 | 109 | } 110 | 111 | impl<'ctx, S> Future for WaitModal<'ctx, S> { 112 | type Output = Result<ModalOutcome<S>, ModalError>; 113 | 114 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { 115 | let this = unsafe { self.get_unchecked_mut() }; 116 | 117 | if let Some(waiter) = this.waiter.as_mut() { 118 | let interaction = ready!(Pin::new(waiter).poll(cx)) 119 | .map_err(ModalError::Waiter)?; 120 | 121 | this.waiter = None; 122 | this.interaction = Some(interaction); 123 | } 124 | 125 | if this.acknowledge.is_none() { 126 | let interaction = this.interaction.as_ref().unwrap(); 127 | 128 | let response = InteractionResponse { 129 | kind: this.response_type, 130 | data: if this.flags.is_none() { 131 | None 132 | } else { 133 | Some(InteractionResponseData { 134 | flags: this.flags, 135 | ..Default::default() 136 | }) 137 | } 138 | }; 139 | 140 | let response = this.http_client.create_response( 141 | interaction.id, 142 | &interaction.token, 143 | &response 144 | ); 145 | 146 | this.acknowledge = Some(response.into_future()); 147 | } 148 | 149 | ready!(Pin::new(this.acknowledge.as_mut().unwrap()).poll(cx)) 150 | .map_err(ModalError::Http)?; 151 | 152 | let mut interaction = this.interaction.take().unwrap(); 153 | 154 | Poll::Ready(Ok(ModalOutcome { 155 | inner: (this.parse_fn)(&mut interaction), 156 | interaction 157 | })) 158 | } 159 | } 160 | 161 | /// Trait used to define modals that can be sent to discord and parsed by the framework. 162 | /// 163 | /// This trait is normally implemented using the derive macro, refer to it to see full 164 | /// documentation about its usage and attributes. 165 | pub trait Modal<D> { 166 | /// Creates the modal, returning the response needed to send it to discord. 167 | /// 168 | /// The framework provides as a custom id the interaction id converted to a string, this custom 169 | /// id must be used as the response custom id in order for the framework to retrieve the modal 170 | /// data. 171 | fn create(ctx: &SlashContext<'_, D>, custom_id: String) -> InteractionResponse; 172 | /// Parses the provided interaction into the modal; 173 | fn parse(interaction: &mut Interaction) -> Self; 174 | } 175 | -------------------------------------------------------------------------------- /vesper/src/parse.rs: -------------------------------------------------------------------------------- 1 | use crate::{builder::WrappedClient, twilight_exports::*}; 2 | use async_trait::async_trait; 3 | use std::error::Error; 4 | 5 | /// The core trait of this framework, it is used to parse all command arguments 6 | #[async_trait] 7 | pub trait Parse<T: Send + Sync>: Sized { 8 | /// Parses the option into the argument. 9 | async fn parse( 10 | _http_client: &WrappedClient, 11 | _data: &T, 12 | _value: Option<&CommandOptionValue>, 13 | _resolved: Option<&mut CommandInteractionDataResolved> 14 | ) -> Result<Self, ParseError>; 15 | 16 | /// Returns the option type this argument has. 17 | fn kind() -> CommandOptionType; 18 | 19 | /// Sets if the argument is required, by default is true. 20 | fn required() -> bool { 21 | true 22 | } 23 | 24 | /// Adds the possible choices to the argument, this function is usually implemented by the 25 | /// derive macro, but can be overridden manually. 26 | fn choices() -> Option<Vec<CommandOptionChoice>> { 27 | None 28 | } 29 | 30 | fn modify_option(_option: &mut CommandOption) {} 31 | } 32 | 33 | /// The errors which can be returned from [Parse](self::Parse) [parse](self::Parse::parse) function. 34 | #[derive(Debug)] 35 | pub enum ParseError { 36 | /// The command arguments does not match with the framework ones. 37 | StructureMismatch(String), 38 | /// An argument failed parsing. 39 | Parsing { 40 | /// The name of the argument that failed to parse. 41 | argument_name: String, 42 | /// Whether if the argument is required or not- 43 | required: bool, 44 | /// The type of the argument. 45 | argument_type: String, 46 | /// The error message as a string. 47 | error: String 48 | }, 49 | /// Other error occurred. 50 | Other(Box<dyn Error + Send + Sync>), 51 | } 52 | 53 | impl std::fmt::Display for ParseError { 54 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 55 | match self { 56 | Self::StructureMismatch(why) => write!(f, "Structure mismatch: {}", why), 57 | Self::Parsing { argument_name, required, argument_type, error } => { 58 | write!(f, "Failed to parse {}({}required {}): {}", argument_name, { 59 | if !required { 60 | "not " 61 | } else { 62 | "" 63 | } 64 | }, argument_type, error) 65 | } 66 | Self::Other(why) => write!(f, "Other: {}", why), 67 | } 68 | } 69 | } 70 | impl Error for ParseError {} 71 | 72 | impl From<Box<dyn Error + Send + Sync>> for ParseError { 73 | fn from(e: Box<dyn Error + Send + Sync>) -> Self { 74 | Self::Other(e) 75 | } 76 | } 77 | 78 | impl From<&'static str> for ParseError { 79 | fn from(why: &'static str) -> Self { 80 | Self::StructureMismatch(why.to_string()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /vesper/src/parse_impl.rs: -------------------------------------------------------------------------------- 1 | use twilight_model::channel::Attachment; 2 | use twilight_model::guild::Role; 3 | use twilight_model::user::User; 4 | use crate::prelude::*; 5 | use crate::twilight_exports::*; 6 | 7 | const NUMBER_MAX_VALUE: i64 = 9007199254740991; 8 | 9 | pub(crate) fn error(type_name: &str, required: bool, why: &str) -> ParseError { 10 | ParseError::Parsing { 11 | argument_name: String::new(), 12 | required, 13 | argument_type: type_name.to_string(), 14 | error: why.to_string() 15 | } 16 | } 17 | 18 | #[async_trait] 19 | impl<T: Send + Sync> Parse<T> for String { 20 | async fn parse( 21 | _: &WrappedClient, 22 | _: &T, 23 | value: Option<&CommandOptionValue>, 24 | _: Option<&mut CommandInteractionDataResolved> 25 | ) -> Result<Self, ParseError> { 26 | if let Some(CommandOptionValue::String(s)) = value { 27 | return Ok(s.to_owned()); 28 | } 29 | Err(error("String", true, "String expected")) 30 | } 31 | 32 | fn kind() -> CommandOptionType { 33 | CommandOptionType::String 34 | } 35 | } 36 | 37 | #[async_trait] 38 | impl<T: Send + Sync> Parse<T> for i64 { 39 | async fn parse( 40 | _: &WrappedClient, 41 | _: &T, 42 | value: Option<&CommandOptionValue>, 43 | _: Option<&mut CommandInteractionDataResolved> 44 | ) -> Result<Self, ParseError> { 45 | if let Some(CommandOptionValue::Integer(i)) = value { 46 | return Ok(*i); 47 | } 48 | Err(error("i64", true, "Integer expected")) 49 | } 50 | 51 | fn kind() -> CommandOptionType { 52 | CommandOptionType::Integer 53 | } 54 | } 55 | 56 | #[async_trait] 57 | impl<T: Send + Sync> Parse<T> for u64 { 58 | async fn parse( 59 | _: &WrappedClient, 60 | _: &T, 61 | value: Option<&CommandOptionValue>, 62 | _: Option<&mut CommandInteractionDataResolved> 63 | ) -> Result<Self, ParseError> { 64 | if let Some(CommandOptionValue::Integer(i)) = value { 65 | if *i < 0 { 66 | return Err(error("u64", true, "Input out of range")) 67 | } 68 | return Ok(*i as u64); 69 | } 70 | Err(error("Integer", true, "Integer expected")) 71 | } 72 | 73 | fn kind() -> CommandOptionType { 74 | CommandOptionType::Integer 75 | } 76 | 77 | fn modify_option(option: &mut CommandOption) { 78 | use twilight_model::application::command::CommandOptionValue; 79 | option.min_value = Some(CommandOptionValue::Integer(0)); 80 | } 81 | } 82 | 83 | #[async_trait] 84 | impl<T: Send + Sync> Parse<T> for f64 { 85 | async fn parse( 86 | _: &WrappedClient, 87 | _: &T, 88 | value: Option<&CommandOptionValue>, 89 | _: Option<&mut CommandInteractionDataResolved> 90 | ) -> Result<Self, ParseError> { 91 | if let Some(CommandOptionValue::Number(i)) = value { 92 | return Ok(*i); 93 | } 94 | Err(error("f64", true, "Number expected")) 95 | } 96 | 97 | fn kind() -> CommandOptionType { 98 | CommandOptionType::Number 99 | } 100 | 101 | fn modify_option(option: &mut CommandOption) { 102 | use twilight_model::application::command::CommandOptionValue; 103 | option.min_value = Some(CommandOptionValue::Number(f64::MIN)); 104 | option.max_value = Some(CommandOptionValue::Number(f64::MAX)); 105 | } 106 | } 107 | 108 | #[async_trait] 109 | impl<T: Send + Sync> Parse<T> for f32 { 110 | async fn parse( 111 | _: &WrappedClient, 112 | _: &T, 113 | value: Option<&CommandOptionValue>, 114 | _: Option<&mut CommandInteractionDataResolved> 115 | ) -> Result<Self, ParseError> { 116 | if let Some(CommandOptionValue::Number(i)) = value { 117 | if *i > f32::MAX as f64 || *i < f32::MIN as f64 { 118 | return Err(error("f32", true, "Input out of range")) 119 | } 120 | return Ok(*i as f32); 121 | } 122 | Err(error("f32", true, "Number expected")) 123 | } 124 | 125 | fn kind() -> CommandOptionType { 126 | CommandOptionType::Number 127 | } 128 | 129 | fn modify_option(option: &mut CommandOption) { 130 | use twilight_model::application::command::CommandOptionValue; 131 | option.max_value = Some(CommandOptionValue::Number(f32::MAX as f64)); 132 | option.min_value = Some(CommandOptionValue::Number(f32::MIN as f64)); 133 | } 134 | } 135 | 136 | #[async_trait] 137 | impl<T: Send + Sync> Parse<T> for bool { 138 | async fn parse( 139 | _: &WrappedClient, 140 | _: &T, 141 | value: Option<&CommandOptionValue>, 142 | _: Option<&mut CommandInteractionDataResolved> 143 | ) -> Result<Self, ParseError> { 144 | if let Some(CommandOptionValue::Boolean(i)) = value { 145 | return Ok(*i); 146 | } 147 | Err(error("Boolean", true, "Boolean expected")) 148 | } 149 | 150 | fn kind() -> CommandOptionType { 151 | CommandOptionType::Boolean 152 | } 153 | } 154 | 155 | #[async_trait] 156 | impl<T: Send + Sync> Parse<T> for Id<AttachmentMarker> { 157 | async fn parse( 158 | _: &WrappedClient, 159 | _: &T, 160 | value: Option<&CommandOptionValue>, 161 | _: Option<&mut CommandInteractionDataResolved> 162 | ) -> Result<Self, ParseError> { 163 | if let Some(CommandOptionValue::Attachment(attachment)) = value { 164 | return Ok(*attachment); 165 | } 166 | 167 | Err(error("Attachment id", true, "Attachment expected")) 168 | } 169 | 170 | fn kind() -> CommandOptionType { 171 | CommandOptionType::Attachment 172 | } 173 | } 174 | 175 | #[async_trait] 176 | impl<T: Send + Sync> Parse<T> for Attachment { 177 | async fn parse( 178 | http_client: &WrappedClient, 179 | data: &T, 180 | value: Option<&CommandOptionValue>, 181 | resolved: Option<&mut CommandInteractionDataResolved> 182 | ) -> Result<Self, ParseError> { 183 | let id = <Id<AttachmentMarker> as Parse<T>>::parse(http_client, data, value, None).await?; 184 | 185 | resolved.map(|item| item.attachments.remove(&id)) 186 | .flatten() 187 | .ok_or_else(|| error("Attachment", true, "Attachment expected")) 188 | } 189 | 190 | fn kind() -> CommandOptionType { 191 | <Id<AttachmentMarker> as Parse<T>>::kind() 192 | } 193 | } 194 | 195 | #[async_trait] 196 | impl<T: Send + Sync> Parse<T> for Id<ChannelMarker> { 197 | async fn parse( 198 | _: &WrappedClient, 199 | _: &T, 200 | value: Option<&CommandOptionValue>, 201 | _: Option<&mut CommandInteractionDataResolved> 202 | ) -> Result<Self, ParseError> { 203 | if let Some(CommandOptionValue::Channel(channel)) = value { 204 | return Ok(*channel); 205 | } 206 | 207 | Err(error("Channel id", true, "Channel expected")) 208 | } 209 | 210 | fn kind() -> CommandOptionType { 211 | CommandOptionType::Channel 212 | } 213 | } 214 | 215 | #[async_trait] 216 | impl<T: Send + Sync> Parse<T> for Id<UserMarker> { 217 | async fn parse( 218 | _: &WrappedClient, 219 | _: &T, 220 | value: Option<&CommandOptionValue>, 221 | _: Option<&mut CommandInteractionDataResolved> 222 | ) -> Result<Self, ParseError> { 223 | if let Some(CommandOptionValue::User(user)) = value { 224 | return Ok(*user); 225 | } 226 | 227 | Err(error("User id", true, "User expected")) 228 | } 229 | 230 | fn kind() -> CommandOptionType { 231 | CommandOptionType::User 232 | } 233 | } 234 | 235 | #[async_trait] 236 | impl<T: Send + Sync> Parse<T> for User { 237 | async fn parse( 238 | http_client: &WrappedClient, 239 | data: &T, 240 | value: Option<&CommandOptionValue>, 241 | resolved: Option<&mut CommandInteractionDataResolved> 242 | ) -> Result<Self, ParseError> { 243 | let id = <Id<UserMarker> as Parse<T>>::parse(http_client, data, value, None).await?; 244 | 245 | resolved.map(|items| items.users.remove(&id)) 246 | .flatten() 247 | .ok_or_else(|| error("User", true, "User expected")) 248 | } 249 | 250 | fn kind() -> CommandOptionType { 251 | <Id<UserMarker> as Parse<T>>::kind() 252 | } 253 | } 254 | 255 | #[async_trait] 256 | impl<T: Send + Sync> Parse<T> for Id<RoleMarker> { 257 | async fn parse( 258 | _: &WrappedClient, 259 | _: &T, 260 | value: Option<&CommandOptionValue>, 261 | _: Option<&mut CommandInteractionDataResolved> 262 | ) -> Result<Self, ParseError> { 263 | if let Some(CommandOptionValue::Role(role)) = value { 264 | return Ok(*role); 265 | } 266 | 267 | Err(error("Role id", true, "Role expected")) 268 | } 269 | 270 | fn kind() -> CommandOptionType { 271 | CommandOptionType::Role 272 | } 273 | } 274 | 275 | #[async_trait] 276 | impl<T: Send + Sync> Parse<T> for Role { 277 | async fn parse( 278 | http_client: &WrappedClient, 279 | data: &T, 280 | value: Option<&CommandOptionValue>, 281 | resolved: Option<&mut CommandInteractionDataResolved> 282 | ) -> Result<Self, ParseError> { 283 | let id = <Id<RoleMarker> as Parse<T>>::parse(http_client, data, value, None).await?; 284 | 285 | resolved.map(|items| items.roles.remove(&id)) 286 | .flatten() 287 | .ok_or_else(|| error("Role", true, "Role expected")) 288 | } 289 | 290 | fn kind() -> CommandOptionType { 291 | <Id<RoleMarker> as Parse<T>>::kind() 292 | } 293 | } 294 | 295 | #[async_trait] 296 | impl<T: Send + Sync> Parse<T> for Id<GenericMarker> { 297 | async fn parse( 298 | _: &WrappedClient, 299 | _: &T, 300 | value: Option<&CommandOptionValue>, 301 | _: Option<&mut CommandInteractionDataResolved> 302 | ) -> Result<Self, ParseError> { 303 | if let Some(CommandOptionValue::Mentionable(id)) = value { 304 | return Ok(*id); 305 | } 306 | 307 | Err(error("Id", true, "Mentionable expected")) 308 | } 309 | 310 | fn kind() -> CommandOptionType { 311 | CommandOptionType::Mentionable 312 | } 313 | } 314 | 315 | #[async_trait] 316 | impl<T: Parse<E>, E: Send + Sync> Parse<E> for Option<T> { 317 | async fn parse( 318 | http_client: &WrappedClient, 319 | data: &E, 320 | value: Option<&CommandOptionValue>, 321 | resolved: Option<&mut CommandInteractionDataResolved> 322 | ) -> Result<Self, ParseError> { 323 | match T::parse(http_client, data, value, resolved).await { 324 | Ok(parsed) => Ok(Some(parsed)), 325 | Err(mut why) => { 326 | if value.is_some() { 327 | if let ParseError::Parsing {required, ..} = &mut why { 328 | *required = false; 329 | } 330 | 331 | Err(why) 332 | } else { 333 | Ok(None) 334 | } 335 | } 336 | } 337 | } 338 | 339 | fn kind() -> CommandOptionType { 340 | T::kind() 341 | } 342 | 343 | fn required() -> bool { 344 | false 345 | } 346 | 347 | fn choices() -> Option<Vec<CommandOptionChoice>> { 348 | T::choices() 349 | } 350 | 351 | fn modify_option(option: &mut CommandOption) { 352 | T::modify_option(option) 353 | } 354 | } 355 | 356 | #[async_trait] 357 | impl<T, E, C> Parse<C> for Result<T, E> 358 | where 359 | T: Parse<C>, 360 | E: From<ParseError>, 361 | C: Send + Sync, 362 | { 363 | async fn parse( 364 | http_client: &WrappedClient, 365 | data: &C, 366 | value: Option<&CommandOptionValue>, 367 | resolved: Option<&mut CommandInteractionDataResolved> 368 | ) -> Result<Self, ParseError> { 369 | // as we want to return the error if occurs, we'll map the error and always return Ok 370 | Ok(T::parse(http_client, data, value, resolved).await.map_err(From::from)) 371 | } 372 | 373 | fn kind() -> CommandOptionType { 374 | T::kind() 375 | } 376 | 377 | fn required() -> bool { 378 | T::required() 379 | } 380 | 381 | fn choices() -> Option<Vec<CommandOptionChoice>> { 382 | T::choices() 383 | } 384 | 385 | fn modify_option(option: &mut CommandOption) { 386 | T::modify_option(option) 387 | } 388 | } 389 | 390 | macro_rules! impl_derived_parse { 391 | ($([$($derived:ty),+] from $prim:ty),* $(,)?) => { 392 | $($( 393 | #[async_trait] 394 | impl<T: Send + Sync> Parse<T> for $derived { 395 | async fn parse( 396 | http_client: &WrappedClient, 397 | data: &T, 398 | value: Option<&CommandOptionValue>, 399 | resolved: Option<&mut CommandInteractionDataResolved> 400 | ) -> Result<Self, ParseError> { 401 | let p = <$prim>::parse(http_client, data, value, resolved).await?; 402 | 403 | if p > <$derived>::MAX as $prim { 404 | Err(error( 405 | stringify!($derived), 406 | true, 407 | concat!( 408 | "Failed to parse to ", 409 | stringify!($derived), 410 | ": the value is greater than ", 411 | stringify!($derived), 412 | "'s ", 413 | "range of values" 414 | ) 415 | )) 416 | } else if p < <$derived>::MIN as $prim { 417 | Err(error( 418 | stringify!($derived), 419 | true, 420 | concat!( 421 | "Failed to parse to ", 422 | stringify!($derived), 423 | ": the value is less than ", 424 | stringify!($derived), 425 | "'s ", 426 | "range of values" 427 | ) 428 | )) 429 | } else { 430 | Ok(p as $derived) 431 | } 432 | } 433 | 434 | fn kind() -> CommandOptionType { 435 | <$prim as Parse<T>>::kind() 436 | } 437 | 438 | fn modify_option(option: &mut CommandOption) { 439 | use twilight_model::application::command::CommandOptionValue; 440 | option.max_value = Some(CommandOptionValue::Integer({ 441 | if <$derived>::MAX as i64 > NUMBER_MAX_VALUE { 442 | NUMBER_MAX_VALUE 443 | } else { 444 | <$derived>::MAX as i64 445 | } 446 | })); 447 | 448 | option.min_value = Some(CommandOptionValue::Integer(<$derived>::MIN as i64)); 449 | } 450 | } 451 | )*)* 452 | }; 453 | } 454 | 455 | impl_derived_parse! { 456 | [i8, i16, i32, isize] from i64, 457 | [u8, u16, u32, usize] from u64, 458 | } 459 | -------------------------------------------------------------------------------- /vesper/src/parsers.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | use async_trait::async_trait; 3 | use twilight_model::application::command::{CommandOption, CommandOptionType}; 4 | use twilight_model::application::interaction::application_command::{CommandInteractionDataResolved, CommandOptionValue, InteractionChannel}; 5 | use twilight_model::channel::ChannelType; 6 | use twilight_model::id::Id; 7 | use twilight_model::id::marker::ChannelMarker; 8 | use crate::builder::WrappedClient; 9 | use crate::parse::{Parse, ParseError}; 10 | use crate::parse_impl::error; 11 | 12 | macro_rules! newtype_struct { 13 | ($($(#[$meta:meta])* $v: vis struct $name: ident($inner: ty)),* $(,)?) => { 14 | $( 15 | newtype_struct!(@inner $(#[$meta])* $v struct $name($inner)); 16 | )* 17 | }; 18 | (@inner $(#[$meta:meta])* $v: vis struct $name: ident($inner: ty)) => { 19 | $(#[$meta])* 20 | $v struct $name($inner); 21 | 22 | impl Deref for $name { 23 | type Target = $inner; 24 | 25 | fn deref(&self) -> &Self::Target { 26 | &self.0 27 | } 28 | } 29 | 30 | impl DerefMut for $name { 31 | fn deref_mut(&mut self) -> &mut Self::Target { 32 | &mut self.0 33 | } 34 | } 35 | } 36 | } 37 | 38 | macro_rules! parse_id { 39 | ($($name: ty, $kind: expr, [$($allowed: expr),* $(,)?]),* $(,)?) => { 40 | $( 41 | parse_id!(@inner $name, $kind, [$($allowed),*]); 42 | )* 43 | }; 44 | (@inner $name: ty, $kind: expr, [$($allowed: expr),* $(,)?]) => { 45 | #[async_trait] 46 | impl<T: Send + Sync> Parse<T> for $name { 47 | async fn parse( 48 | http_client: &WrappedClient, 49 | data: &T, 50 | value: Option<&CommandOptionValue>, 51 | resolved: Option<&mut CommandInteractionDataResolved> 52 | ) -> Result<Self, ParseError> { 53 | Ok(Self(Id::parse(http_client, data, value, resolved).await?)) 54 | } 55 | 56 | fn kind() -> CommandOptionType { 57 | $kind 58 | } 59 | 60 | fn modify_option(option: &mut CommandOption) { 61 | option.channel_types = Some(vec![$($allowed),*]) 62 | } 63 | } 64 | }; 65 | } 66 | 67 | macro_rules! parse_derived_channel { 68 | ($($name_t: ty, $id: ty, $name: literal),* $(,)?) => { 69 | $( 70 | parse_derived_channel!(@inner $name_t, $id, $name); 71 | )* 72 | }; 73 | (@inner $name_t: ty, $id: ty, $name: literal) => { 74 | #[async_trait] 75 | impl<T: Send + Sync> Parse<T> for $name_t { 76 | async fn parse( 77 | http_client: &WrappedClient, 78 | data: &T, 79 | value: Option<&CommandOptionValue>, 80 | resolved: Option<&mut CommandInteractionDataResolved> 81 | ) -> Result<Self, ParseError> { 82 | let id = <$id>::parse(http_client, data, value, None).await?; 83 | 84 | resolved.map(|items| items.channels.remove(&*id)) 85 | .flatten() 86 | .ok_or_else(|| error($name, true, concat!($name, " expected"))) 87 | .map(Self) 88 | } 89 | 90 | fn kind() -> CommandOptionType { 91 | <$id as Parse<T>>::kind() 92 | } 93 | 94 | fn modify_option(option: &mut CommandOption) { 95 | <$id as Parse<T>>::modify_option(option) 96 | } 97 | } 98 | }; 99 | } 100 | 101 | newtype_struct! { 102 | /// An object that parses into a discord only **text** channel. 103 | pub struct TextChannel(InteractionChannel), 104 | /// An object that parses into a discord only **text** channel id. 105 | pub struct TextChannelId(Id<ChannelMarker>), 106 | /// An object that parses into a discord only **voice** channel. 107 | pub struct VoiceChannel(InteractionChannel), 108 | /// An object that parses into a discord only **voice** channel id. 109 | pub struct VoiceChannelId(Id<ChannelMarker>), 110 | /// An object that parses into a discord **only public** thread. 111 | pub struct PublicThread(InteractionChannel), 112 | /// An object that parses into a discord **only public** thread id. 113 | pub struct PublicThreadId(Id<ChannelMarker>), 114 | /// An object that parses into a discord **only private** thread. 115 | pub struct PrivateThread(InteractionChannel), 116 | /// An object that parses into a discord **only private** thread id. 117 | pub struct PrivateThreadId(Id<ChannelMarker>), 118 | /// An object that parses into a discord **either public, private or announcement** thread. 119 | pub struct Thread(InteractionChannel), 120 | /// An object that parses into a discord **either public, private or announcement** thread id. 121 | pub struct ThreadId(Id<ChannelMarker>) 122 | } 123 | 124 | parse_id! { 125 | TextChannelId, CommandOptionType::Channel, [ChannelType::GuildText], 126 | VoiceChannelId, CommandOptionType::Channel, [ChannelType::GuildVoice], 127 | PublicThreadId, CommandOptionType::Channel, [ChannelType::PublicThread], 128 | PrivateThreadId, CommandOptionType::Channel, [ChannelType::PrivateThread], 129 | ThreadId, CommandOptionType::Channel, [ChannelType::PublicThread, ChannelType::PrivateThread], 130 | } 131 | 132 | parse_derived_channel! { 133 | TextChannel, TextChannelId, "Text Channel", 134 | VoiceChannel, VoiceChannelId, "Voice Channel", 135 | PublicThread, PublicThreadId, "Public Thread", 136 | PrivateThread, PrivateThreadId, "Private Thread", 137 | Thread, ThreadId, "Thread" 138 | } 139 | -------------------------------------------------------------------------------- /vesper/src/range.rs: -------------------------------------------------------------------------------- 1 | use std::any::type_name; 2 | use crate::prelude::*; 3 | use crate::twilight_exports::*; 4 | use crate::parse_impl::error; 5 | use std::ops::{Deref, DerefMut}; 6 | use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; 7 | 8 | 9 | mod sealed { 10 | use super::*; 11 | 12 | /// A trait used to specify the values [range](super::Range) can take. 13 | pub trait Number: Copy + Debug + Display { 14 | fn as_i64(&self) -> i64; 15 | } 16 | 17 | macro_rules! number { 18 | ($($t:ty),* $(,)?) => { 19 | $( 20 | impl Number for $t { 21 | fn as_i64(&self) -> i64 { 22 | *self as i64 23 | } 24 | } 25 | )* 26 | }; 27 | } 28 | 29 | number![i8, i16, i32, i64, isize, u8, u16, u32, u64, usize]; 30 | } 31 | 32 | use sealed::Number; 33 | 34 | /// A range-like type used to constraint the input provided by the user. This is equivalent to 35 | /// using a [RangeInclusive], but implements the [parse] trait. 36 | /// 37 | /// [RangeInclusive]: std::ops::RangeInclusive 38 | /// [parse]: Parse 39 | #[derive(Copy, Clone)] 40 | pub struct Range<T: Number, const START: i64, const END: i64>(T); 41 | 42 | impl<T: Number, const START: i64, const END: i64> Deref for Range<T, START, END> { 43 | type Target = T; 44 | fn deref(&self) -> &Self::Target { 45 | &self.0 46 | } 47 | } 48 | 49 | impl<T: Number, const START: i64, const END: i64> DerefMut for Range<T, START, END> { 50 | fn deref_mut(&mut self) -> &mut Self::Target { 51 | &mut self.0 52 | } 53 | } 54 | 55 | #[async_trait] 56 | impl<T, E, const START: i64, const END: i64> Parse<T> for Range<E, START, END> 57 | where 58 | T: Send + Sync, 59 | E: Parse<T> + Number 60 | { 61 | async fn parse( 62 | http_client: &WrappedClient, 63 | data: &T, 64 | value: Option<&CommandOptionValue>, 65 | resolved: Option<&mut CommandInteractionDataResolved> 66 | ) -> Result<Self, ParseError> { 67 | let value = E::parse(http_client, data, value, resolved).await?; 68 | 69 | let v = value.as_i64(); 70 | 71 | if v < START || v > END { 72 | return Err(error( 73 | &format!("Range<{}, {}, {}>", type_name::<E>(), START, END), 74 | true, 75 | "Input out of range" 76 | )); 77 | } 78 | 79 | Ok(Self(value)) 80 | } 81 | 82 | fn kind() -> CommandOptionType { 83 | E::kind() 84 | } 85 | 86 | fn modify_option(option: &mut CommandOption) { 87 | use twilight_model::application::command::CommandOptionValue; 88 | option.max_value = Some(CommandOptionValue::Integer(END)); 89 | option.min_value = Some(CommandOptionValue::Integer(START)); 90 | } 91 | } 92 | 93 | impl<T: Number, const START: i64, const END: i64> Debug for Range<T, START, END> { 94 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 95 | write!(f, "Range<{}, {}, {}>({})", type_name::<T>(), START, END, self.0) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /vesper/src/wait.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, task::{Context, Poll}}; 2 | use std::pin::Pin; 3 | use tokio::sync::oneshot::{Sender, Receiver, channel, error::RecvError}; 4 | use crate::twilight_exports::Interaction; 5 | 6 | pub(crate) fn new_pair<F>(fun: F) -> (WaiterWaker, InteractionWaiter) 7 | where 8 | F: Fn(&Interaction) -> bool + Send + 'static 9 | { 10 | let (sender, receiver) = channel(); 11 | 12 | ( 13 | WaiterWaker { 14 | predicate: Box::new(fun), 15 | sender 16 | }, 17 | InteractionWaiter { 18 | receiver 19 | } 20 | ) 21 | } 22 | 23 | /// A waiter used to wait for an interaction. 24 | /// 25 | /// The waiter implements [`Future`], so in order to retrieve the interaction, just await the waiter. 26 | /// 27 | /// # Examples: 28 | /// 29 | /// ```rust 30 | /// use vesper::prelude::{command, SlashContext, DefaultCommandResult}; 31 | /// 32 | /// #[command] 33 | /// #[description = "My Command"] 34 | /// async fn my_command(ctx: &mut SlashContext<()>) -> DefaultCommandResult { 35 | /// ctx.defer(false).await?; 36 | /// let interaction = ctx.wait_interaction(|interaction| { 37 | /// // predicate here 38 | /// false 39 | /// }).await?; 40 | /// 41 | /// Ok(()) 42 | /// } 43 | /// ``` 44 | /// 45 | /// [`Future`]: Future 46 | pub struct InteractionWaiter { 47 | receiver: Receiver<Interaction> 48 | } 49 | 50 | impl Future for InteractionWaiter { 51 | type Output = Result<Interaction, RecvError>; 52 | 53 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { 54 | Pin::new(&mut self.receiver).poll(cx) 55 | } 56 | } 57 | 58 | 59 | /// A waker used to notify its associate [`waiter`] when the predicate has been satisfied and 60 | /// deliver the interaction. 61 | /// 62 | /// [`waiter`]: InteractionWaiter 63 | pub struct WaiterWaker { 64 | pub predicate: Box<dyn Fn(&Interaction) -> bool + Send + 'static>, 65 | pub sender: Sender<Interaction> 66 | } 67 | 68 | impl WaiterWaker { 69 | pub fn check(&self, interaction: &Interaction) -> bool { 70 | (self.predicate)(interaction) 71 | } 72 | 73 | pub fn wake(self, interaction: Interaction) { 74 | let _ = self.sender.send(interaction); 75 | } 76 | } 77 | --------------------------------------------------------------------------------