From 0e6e1010038327a18888034c7415eaaa31ea4ab1 Mon Sep 17 00:00:00 2001 From: Michael Krasnitski <42564254+mkrasnitski@users.noreply.github.com> Date: Sat, 23 Jul 2022 23:32:55 -0400 Subject: [PATCH] Overhaul usage of the builder pattern (#2024) --- examples/e03_struct_utilities/src/main.rs | 4 +- examples/e05_command_framework/src/main.rs | 6 +- .../e09_create_message_builder/src/main.rs | 41 +- examples/e13_parallel_loops/src/main.rs | 32 +- .../src/commands/attachmentinput.rs | 17 +- .../e14_slash_commands/src/commands/id.rs | 12 +- .../src/commands/numberinput.rs | 24 +- .../e14_slash_commands/src/commands/ping.rs | 4 +- .../src/commands/welcome.rs | 22 +- .../src/commands/wonderful_command.rs | 4 +- examples/e14_slash_commands/src/main.rs | 49 +- examples/e17_message_components/src/main.rs | 132 ++--- examples/e18_webhook/src/main.rs | 7 +- src/builder/add_member.rs | 42 +- src/builder/bot_auth_parameters.rs | 18 +- src/builder/create_allowed_mentions.rs | 75 ++- src/builder/create_application_command.rs | 228 ++++---- .../create_application_command_permission.rs | 137 ++--- src/builder/create_channel.rs | 99 ++-- src/builder/create_components.rs | 162 ++---- src/builder/create_embed.rs | 266 +++++----- src/builder/create_interaction_response.rs | 222 ++++---- .../create_interaction_response_followup.rs | 168 +++--- src/builder/create_invite.rs | 87 +++- src/builder/create_message.rs | 301 ++++++----- src/builder/create_scheduled_event.rs | 106 ++-- src/builder/create_stage_instance.rs | 48 +- src/builder/create_sticker.rs | 95 ++-- src/builder/create_thread.rs | 39 +- src/builder/create_webhook.rs | 87 +++- src/builder/edit_automod_rule.rs | 57 +- src/builder/edit_channel.rs | 130 +++-- src/builder/edit_guild.rs | 199 +++---- src/builder/edit_guild_welcome_screen.rs | 54 +- src/builder/edit_guild_widget.rs | 31 +- src/builder/edit_interaction_response.rs | 104 ++-- src/builder/edit_member.rs | 97 ++-- src/builder/edit_message.rs | 179 ++++--- src/builder/edit_profile.rs | 82 ++- src/builder/edit_role.rs | 142 +++-- src/builder/edit_scheduled_event.rs | 93 +++- src/builder/edit_stage_instance.rs | 46 +- src/builder/edit_sticker.rs | 33 +- src/builder/edit_thread.rs | 40 +- src/builder/edit_voice_state.rs | 103 ++-- src/builder/edit_webhook.rs | 62 ++- src/builder/edit_webhook_message.rs | 76 ++- src/builder/execute_webhook.rs | 247 +++++---- src/builder/get_messages.rs | 109 ++-- src/framework/standard/help_commands.rs | 141 +++-- src/http/client.rs | 38 +- src/model/application/command.rs | 100 ++-- .../interaction/application_command.rs | 188 ++----- .../interaction/message_component.rs | 129 ++--- src/model/application/interaction/modal.rs | 128 ++--- src/model/channel/channel_category.rs | 75 ++- src/model/channel/channel_id.rs | 413 ++++++--------- src/model/channel/embed.rs | 36 -- src/model/channel/guild_channel.rs | 441 +++++++--------- src/model/channel/message.rs | 241 ++------- src/model/channel/private_channel.rs | 106 ++-- src/model/channel/reaction.rs | 4 +- src/model/guild/guild_id.rs | 481 ++++++++--------- src/model/guild/member.rs | 36 +- src/model/guild/mod.rs | 487 +++++++----------- src/model/guild/partial_guild.rs | 365 ++++++------- src/model/guild/role.rs | 18 +- src/model/invite.rs | 47 +- src/model/mention.rs | 21 +- src/model/sticker/mod.rs | 28 +- src/model/sticker/sticker_id.rs | 22 +- src/model/user.rs | 77 ++- src/model/utils.rs | 76 --- src/model/webhook.rs | 177 ++----- src/utils/mod.rs | 75 ++- 75 files changed, 4188 insertions(+), 4180 deletions(-) diff --git a/examples/e03_struct_utilities/src/main.rs b/examples/e03_struct_utilities/src/main.rs index 29a854ba0a9..3ac64c51cb9 100644 --- a/examples/e03_struct_utilities/src/main.rs +++ b/examples/e03_struct_utilities/src/main.rs @@ -1,6 +1,7 @@ use std::env; use serenity::async_trait; +use serenity::builder::CreateMessage; use serenity::model::channel::Message; use serenity::model::gateway::Ready; use serenity::prelude::*; @@ -19,7 +20,8 @@ impl EventHandler for Handler { // In this case, you can direct message a User directly by simply // calling a method on its instance, with the content of the // message. - let dm = msg.author.dm(&context, |m| m.content("Hello!")).await; + let builder = CreateMessage::default().content("Hello!"); + let dm = msg.author.dm(&context, builder).await; if let Err(why) = dm { println!("Error when direct messaging user: {:?}", why); diff --git a/examples/e05_command_framework/src/main.rs b/examples/e05_command_framework/src/main.rs index 0f49c2bdaf5..638b649d8ac 100644 --- a/examples/e05_command_framework/src/main.rs +++ b/examples/e05_command_framework/src/main.rs @@ -14,6 +14,7 @@ use std::fmt::Write; use std::sync::Arc; use serenity::async_trait; +use serenity::builder::EditChannel; use serenity::client::bridge::gateway::{ShardId, ShardManager}; use serenity::framework::standard::buckets::{LimitedFor, RevertBucket}; use serenity::framework::standard::macros::{check, command, group, help, hook}; @@ -564,9 +565,8 @@ async fn am_i_admin(ctx: &Context, msg: &Message, _args: Args) -> CommandResult #[command] async fn slow_mode(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let say_content = if let Ok(slow_mode_rate_seconds) = args.single::() { - if let Err(why) = - msg.channel_id.edit(&ctx.http, |c| c.rate_limit_per_user(slow_mode_rate_seconds)).await - { + let builder = EditChannel::default().rate_limit_per_user(slow_mode_rate_seconds); + if let Err(why) = msg.channel_id.edit(&ctx.http, builder).await { println!("Error setting channel's slow mode rate: {:?}", why); format!("Failed to set slow mode to `{}` seconds.", slow_mode_rate_seconds) diff --git a/examples/e09_create_message_builder/src/main.rs b/examples/e09_create_message_builder/src/main.rs index b8523e53aa9..0ee38ea36ad 100644 --- a/examples/e09_create_message_builder/src/main.rs +++ b/examples/e09_create_message_builder/src/main.rs @@ -1,6 +1,7 @@ use std::env; use serenity::async_trait; +use serenity::builder::{CreateEmbed, CreateEmbedFooter, CreateMessage}; use serenity::model::channel::Message; use serenity::model::gateway::Ready; use serenity::model::Timestamp; @@ -16,27 +17,25 @@ impl EventHandler for Handler { // using a builder syntax. // This example will create a message that says "Hello, World!", with an embed that has // a title, description, an image, three fields, and a footer. - let msg = msg - .channel_id - .send_message(&ctx.http, |m| { - m.content("Hello, World!") - .embed(|e| { - e.title("This is a title") - .description("This is a description") - .image("attachment://ferris_eyes.png") - .fields(vec![ - ("This is the first field", "This is a field body", true), - ("This is the second field", "Both fields are inline", true), - ]) - .field("This is the third field", "This is not an inline field", false) - .footer(|f| f.text("This is a footer")) - // Add a timestamp for the current time - // This also accepts a rfc3339 Timestamp - .timestamp(Timestamp::now()) - }) - .add_file("./ferris_eyes.png") - }) - .await; + let footer = CreateEmbedFooter::default().text("This is a footer"); + let embed = CreateEmbed::default() + .title("This is a title") + .description("This is a description") + .image("attachment://ferris_eyes.png") + .fields(vec![ + ("This is the first field", "This is a field body", true), + ("This is the second field", "Both fields are inline", true), + ]) + .field("This is the third field", "This is not an inline field", false) + .footer(footer) + // Add a timestamp for the current time + // This also accepts a rfc3339 Timestamp + .timestamp(Timestamp::now()); + let builder = CreateMessage::default() + .content("Hello, World!") + .embed(embed) + .add_file("./ferris_eyes.png"); + let msg = msg.channel_id.send_message(&ctx.http, builder).await; if let Err(why) = msg { println!("Error sending message: {:?}", why); diff --git a/examples/e13_parallel_loops/src/main.rs b/examples/e13_parallel_loops/src/main.rs index 1c75f0e1166..8f0d0a1ca9c 100644 --- a/examples/e13_parallel_loops/src/main.rs +++ b/examples/e13_parallel_loops/src/main.rs @@ -5,6 +5,7 @@ use std::time::Duration; use chrono::offset::Utc; use serenity::async_trait; +use serenity::builder::{CreateEmbed, CreateMessage}; use serenity::gateway::ActivityData; use serenity::model::channel::Message; use serenity::model::gateway::Ready; @@ -79,23 +80,20 @@ async fn log_system_load(ctx: Arc) { // We can use ChannelId directly to send a message to a specific channel; in this case, the // message would be sent to the #testing channel on the discord server. - let message = ChannelId::new(381926291785383946) - .send_message(&ctx, |m| { - m.embed(|e| { - e.title("System Resource Load") - .field("CPU Load Average", &format!("{:.2}%", cpu_load.one * 10.0), false) - .field( - "Memory Usage", - &format!( - "{:.2} MB Free out of {:.2} MB", - mem_use.free as f32 / 1000.0, - mem_use.total as f32 / 1000.0 - ), - false, - ) - }) - }) - .await; + let embed = CreateEmbed::default() + .title("System Resource Load") + .field("CPU Load Average", &format!("{:.2}%", cpu_load.one * 10.0), false) + .field( + "Memory Usage", + &format!( + "{:.2} MB Free out of {:.2} MB", + mem_use.free as f32 / 1000.0, + mem_use.total as f32 / 1000.0 + ), + false, + ); + let builder = CreateMessage::default().embed(embed); + let message = ChannelId::new(381926291785383946).send_message(&ctx, builder).await; if let Err(why) = message { eprintln!("Error sending message: {:?}", why); }; diff --git a/examples/e14_slash_commands/src/commands/attachmentinput.rs b/examples/e14_slash_commands/src/commands/attachmentinput.rs index 445af92b33b..8ed2e0230ec 100644 --- a/examples/e14_slash_commands/src/commands/attachmentinput.rs +++ b/examples/e14_slash_commands/src/commands/attachmentinput.rs @@ -1,4 +1,4 @@ -use serenity::builder::CreateApplicationCommand; +use serenity::builder::{CreateApplicationCommand, CreateApplicationCommandOption}; use serenity::model::prelude::command::CommandOptionType; use serenity::model::prelude::interaction::application_command::{ResolvedOption, ResolvedValue}; @@ -13,14 +13,15 @@ pub fn run(options: &[ResolvedOption]) -> String { } } -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command.name("attachmentinput").description("Test command for attachment input").create_option( - |option| { - option +pub fn register() -> CreateApplicationCommand { + CreateApplicationCommand::default() + .name("attachmentinput") + .description("Test command for attachment input") + .add_option( + CreateApplicationCommandOption::default() .name("attachment") .description("A file") .kind(CommandOptionType::Attachment) - .required(true) - }, - ) + .required(true), + ) } diff --git a/examples/e14_slash_commands/src/commands/id.rs b/examples/e14_slash_commands/src/commands/id.rs index 2d9f6bb0386..717d4fb489f 100644 --- a/examples/e14_slash_commands/src/commands/id.rs +++ b/examples/e14_slash_commands/src/commands/id.rs @@ -1,4 +1,4 @@ -use serenity::builder::CreateApplicationCommand; +use serenity::builder::{CreateApplicationCommand, CreateApplicationCommandOption}; use serenity::model::prelude::command::CommandOptionType; use serenity::model::prelude::interaction::application_command::{ResolvedOption, ResolvedValue}; @@ -13,12 +13,12 @@ pub fn run(options: &[ResolvedOption]) -> String { } } -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command.name("id").description("Get a user id").create_option(|option| { - option +pub fn register() -> CreateApplicationCommand { + CreateApplicationCommand::default().name("id").description("Get a user id").add_option( + CreateApplicationCommandOption::default() .name("id") .description("The user to lookup") .kind(CommandOptionType::User) - .required(true) - }) + .required(true), + ) } diff --git a/examples/e14_slash_commands/src/commands/numberinput.rs b/examples/e14_slash_commands/src/commands/numberinput.rs index 484a6e847ac..6ade0681939 100644 --- a/examples/e14_slash_commands/src/commands/numberinput.rs +++ b/examples/e14_slash_commands/src/commands/numberinput.rs @@ -1,28 +1,26 @@ -use serenity::builder; +use serenity::builder::{CreateApplicationCommand, CreateApplicationCommandOption}; use serenity::model::prelude::command::CommandOptionType; -pub fn register( - command: &mut builder::CreateApplicationCommand, -) -> &mut builder::CreateApplicationCommand { - command +pub fn register() -> CreateApplicationCommand { + CreateApplicationCommand::default() .name("numberinput") .description("Test command for number input") - .create_option(|option| { - option + .add_option( + CreateApplicationCommandOption::default() .name("int") .description("An integer from 5 to 10") .kind(CommandOptionType::Integer) .min_int_value(5) .max_int_value(10) - .required(true) - }) - .create_option(|option| { - option + .required(true), + ) + .add_option( + CreateApplicationCommandOption::default() .name("number") .description("A float from -3.3 to 234.5") .kind(CommandOptionType::Number) .min_number_value(-3.3) .max_number_value(234.5) - .required(true) - }) + .required(true), + ) } diff --git a/examples/e14_slash_commands/src/commands/ping.rs b/examples/e14_slash_commands/src/commands/ping.rs index cead453c3c4..9f31d4373df 100644 --- a/examples/e14_slash_commands/src/commands/ping.rs +++ b/examples/e14_slash_commands/src/commands/ping.rs @@ -5,6 +5,6 @@ pub fn run(_options: &[ResolvedOption]) -> String { "Hey, I'm alive!".to_string() } -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command.name("ping").description("A ping command") +pub fn register() -> CreateApplicationCommand { + CreateApplicationCommand::default().name("ping").description("A ping command") } diff --git a/examples/e14_slash_commands/src/commands/welcome.rs b/examples/e14_slash_commands/src/commands/welcome.rs index c81d1b2e3ff..9ff9a187017 100644 --- a/examples/e14_slash_commands/src/commands/welcome.rs +++ b/examples/e14_slash_commands/src/commands/welcome.rs @@ -1,23 +1,23 @@ -use serenity::builder::CreateApplicationCommand; +use serenity::builder::{CreateApplicationCommand, CreateApplicationCommandOption}; use serenity::model::prelude::command::CommandOptionType; -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command +pub fn register() -> CreateApplicationCommand { + CreateApplicationCommand::default() .name("welcome") .name_localized("de", "begrüßen") .description("Welcome a user") .description_localized("de", "Einen Nutzer begrüßen") - .create_option(|option| { - option + .add_option( + CreateApplicationCommandOption::default() .name("user") .name_localized("de", "nutzer") .description("The user to welcome") .description_localized("de", "Der zu begrüßende Nutzer") .kind(CommandOptionType::User) - .required(true) - }) - .create_option(|option| { - option + .required(true), + ) + .add_option( + CreateApplicationCommandOption::default() .name("message") .name_localized("de", "nachricht") .description("The message to send") @@ -48,6 +48,6 @@ pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicatio "I hope that you brought a controller to play together!", "game", [("de", "Ich hoffe du hast einen Controller zum Spielen mitgebracht!")], - ) - }) + ), + ) } diff --git a/examples/e14_slash_commands/src/commands/wonderful_command.rs b/examples/e14_slash_commands/src/commands/wonderful_command.rs index 0a0b87675ba..6355588d8d0 100644 --- a/examples/e14_slash_commands/src/commands/wonderful_command.rs +++ b/examples/e14_slash_commands/src/commands/wonderful_command.rs @@ -1,5 +1,5 @@ use serenity::builder::CreateApplicationCommand; -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command.name("wonderful_command").description("An amazing command") +pub fn register() -> CreateApplicationCommand { + CreateApplicationCommand::default().name("wonderful_command").description("An amazing command") } diff --git a/examples/e14_slash_commands/src/main.rs b/examples/e14_slash_commands/src/main.rs index e576b3d1a1f..763b5d514f9 100644 --- a/examples/e14_slash_commands/src/main.rs +++ b/examples/e14_slash_commands/src/main.rs @@ -3,6 +3,11 @@ mod commands; use std::env; use serenity::async_trait; +use serenity::builder::{ + CreateApplicationCommands as CreateCommands, + CreateInteractionResponse, + CreateInteractionResponseData, +}; use serenity::model::application::command::Command; use serenity::model::application::interaction::{Interaction, InteractionResponseType}; use serenity::model::gateway::Ready; @@ -24,14 +29,11 @@ impl EventHandler for Handler { _ => "not implemented :(".to_string(), }; - if let Err(why) = command - .create_interaction_response(&ctx.http, |response| { - response - .kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|message| message.content(content)) - }) - .await - { + let data = CreateInteractionResponseData::default().content(content); + let builder = CreateInteractionResponse::default() + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(data); + if let Err(why) = command.create_interaction_response(&ctx.http, builder).await { println!("Cannot respond to slash command: {}", why); } } @@ -47,21 +49,24 @@ impl EventHandler for Handler { .expect("GUILD_ID must be an integer"), ); - let commands = GuildId::set_application_commands(&guild_id, &ctx.http, |commands| { - commands - .create_application_command(|command| commands::ping::register(command)) - .create_application_command(|command| commands::id::register(command)) - .create_application_command(|command| commands::welcome::register(command)) - .create_application_command(|command| commands::numberinput::register(command)) - .create_application_command(|command| commands::attachmentinput::register(command)) - }) - .await; + let commands = guild_id + .set_application_commands( + &ctx.http, + CreateCommands::default() + .add_application_command(commands::ping::register()) + .add_application_command(commands::id::register()) + .add_application_command(commands::welcome::register()) + .add_application_command(commands::numberinput::register()) + .add_application_command(commands::attachmentinput::register()), + ) + .await; println!("I now have the following guild slash commands: {:#?}", commands); - let guild_command = Command::create_global_application_command(&ctx.http, |command| { - commands::wonderful_command::register(command) - }) + let guild_command = Command::create_global_application_command( + &ctx.http, + commands::wonderful_command::register(), + ) .await; println!("I created the following global slash command: {:#?}", guild_command); @@ -81,8 +86,8 @@ async fn main() { // Finally, start a single shard, and start listening to events. // - // Shards will automatically attempt to reconnect, and will perform - // exponential backoff until it reconnects. + // Shards will automatically attempt to reconnect, and will perform exponential backoff until + // it reconnects. if let Err(why) = client.start().await { println!("Client error: {:?}", why); } diff --git a/examples/e17_message_components/src/main.rs b/examples/e17_message_components/src/main.rs index 885e2e0fb68..f890ec4272b 100644 --- a/examples/e17_message_components/src/main.rs +++ b/examples/e17_message_components/src/main.rs @@ -3,7 +3,17 @@ use std::time::Duration; use dotenv::dotenv; use serenity::async_trait; -use serenity::builder::CreateButton; +use serenity::builder::{ + CreateActionRow, + CreateButton, + CreateComponents, + CreateInteractionResponse, + CreateInteractionResponseData, + CreateMessage, + CreateSelectMenu, + CreateSelectMenuOption, + CreateSelectMenuOptions, +}; use serenity::client::{Context, EventHandler}; use serenity::futures::StreamExt; use serenity::model::application::component::ButtonStyle; @@ -11,15 +21,10 @@ use serenity::model::prelude::*; use serenity::prelude::*; fn sound_button(name: &str, emoji: ReactionType) -> CreateButton { - let mut b = CreateButton::default(); - b.custom_id(name); // To add an emoji to buttons, use .emoji(). The method accepts anything ReactionType or // anything that can be converted to it. For a list of that, search Trait Implementations in the // docs for From<...>. - b.emoji(emoji); - b.label(name); - b.style(ButtonStyle::Primary); - b + CreateButton::default().custom_id(name).emoji(emoji).label(name).style(ButtonStyle::Primary) } struct Handler; @@ -34,24 +39,25 @@ impl EventHandler for Handler { // Ask the user for its favorite animal let m = msg .channel_id - .send_message(&ctx, |m| { - m.content("Please select your favorite animal").components(|c| { - c.create_action_row(|row| { + .send_message( + &ctx, + CreateMessage::default().content("Please select your favorite animal").components( + CreateComponents::default().set_action_row(CreateActionRow::default() // An action row can only contain one select menu! - row.create_select_menu(|menu| { - menu.custom_id("animal_select"); - menu.placeholder("No animal selected"); - menu.options(|f| { - f.create_option(|o| o.label("🐈 meow").value("Cat")); - f.create_option(|o| o.label("🐕 woof").value("Dog")); - f.create_option(|o| o.label("🐎 neigh").value("Horse")); - f.create_option(|o| o.label("🦙 hoooooooonk").value("Alpaca")); - f.create_option(|o| o.label("🦀 crab rave").value("Ferris")) - }) - }) - }) - }) - }) + .add_select_menu(CreateSelectMenu::default() + .custom_id("animal_select") + .placeholder("No animal selected") + .options(CreateSelectMenuOptions::default().set_options(vec![ + CreateSelectMenuOption::default().label("🐈 meow").value("Cat"), + CreateSelectMenuOption::default().label("🐕 woof").value("Dog"), + CreateSelectMenuOption::default().label("🐎 neigh").value("Horse"), + CreateSelectMenuOption::default().label("🦙 hoooooooonk").value("Alpaca"), + CreateSelectMenuOption::default().label("🦀 crab rave").value("Ferris"), + ])) + ) + ) + ), + ) .await .unwrap(); @@ -77,32 +83,39 @@ impl EventHandler for Handler { // Acknowledge the interaction and edit the message interaction - .create_interaction_response(&ctx, |r| { - r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(|d| { - d.content(format!("You chose: **{}**\nNow choose a sound!", animal)).components( - |c| { - c.create_action_row(|r| { - // add_XXX methods are an alternative to create_XXX methods - r.add_button(sound_button("meow", "🐈".parse().unwrap())); - r.add_button(sound_button("woof", "🐕".parse().unwrap())); - r.add_button(sound_button("neigh", "🐎".parse().unwrap())); - r.add_button(sound_button("hoooooooonk", "🦙".parse().unwrap())); - r.add_button(sound_button( - "crab rave", - // Custom emojis in Discord are represented with - // `<:EMOJI_NAME:EMOJI_ID>`. You can see this by - // posting an emoji in your server and putting a backslash - // before the emoji. - // - // Because ReactionType implements FromStr, we can use .parse() - // to convert the textual emoji representation to ReactionType - "<:ferris:381919740114763787>".parse().unwrap(), - )) - }) - }, - ) - }) - }) + .create_interaction_response( + &ctx, + CreateInteractionResponse::default() + .kind(InteractionResponseType::UpdateMessage) + .interaction_response_data( + CreateInteractionResponseData::default() + .content(format!("You chose: **{}**\nNow choose a sound!", animal)) + .components( + CreateComponents::default().set_action_row( + CreateActionRow::default() + // add_XXX methods are an alternative to create_XXX methods + .add_button(sound_button("meow", "🐈".parse().unwrap())) + .add_button(sound_button("woof", "🐕".parse().unwrap())) + .add_button(sound_button("neigh", "🐎".parse().unwrap())) + .add_button(sound_button( + "hoooooooonk", + "🦙".parse().unwrap(), + )) + .add_button(sound_button( + "crab rave", + // Custom emojis in Discord are represented with + // `<:EMOJI_NAME:EMOJI_ID>`. You can see this by + // posting an emoji in your server and putting a backslash + // before the emoji. + // + // Because ReactionType implements FromStr, we can use .parse() + // to convert the textual emoji representation to ReactionType + "<:ferris:381919740114763787>".parse().unwrap(), + )), + ), + ), + ), + ) .await .unwrap(); @@ -116,15 +129,18 @@ impl EventHandler for Handler { let sound = &interaction.data.custom_id; // Acknowledge the interaction and send a reply interaction - .create_interaction_response(&ctx, |r| { - // This time we dont edit the message but reply to it - r.kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|d| { - // Make the message hidden for other users by setting `ephemeral(true)`. - d.ephemeral(true) + .create_interaction_response( + &ctx, + CreateInteractionResponse::default() + // This time we dont edit the message but reply to it + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data( + CreateInteractionResponseData::default() + // Make the message hidden for other users by setting `ephemeral(true)`. + .ephemeral(true) .content(format!("The **{}** says __{}__", animal, sound)) - }) - }) + ), + ) .await .unwrap(); } diff --git a/examples/e18_webhook/src/main.rs b/examples/e18_webhook/src/main.rs index 3f146ed6c9a..7a3131f2b05 100644 --- a/examples/e18_webhook/src/main.rs +++ b/examples/e18_webhook/src/main.rs @@ -1,3 +1,4 @@ +use serenity::builder::ExecuteWebhook; use serenity::http::Http; use serenity::model::webhook::Webhook; @@ -7,8 +8,6 @@ async fn main() { let http = Http::new(""); let webhook = Webhook::from_url(&http, "https://discord.com/api/webhooks/133742013374206969/hello-there-oPNtRN5UY5DVmBe7m1N0HE-replace-me-Dw9LRkgq3zI7LoW3Rb-k-q").await.expect("Replace the webhook with your own"); - webhook - .execute(&http, false, |w| w.content("hello there").username("Webhook test")) - .await - .expect("Could not execute webhook."); + let builder = ExecuteWebhook::default().content("hello there").username("Webhook test"); + webhook.execute(&http, false, builder).await.expect("Could not execute webhook."); } diff --git a/src/builder/add_member.rs b/src/builder/add_member.rs index 9b125ff0a04..07b796b73ed 100644 --- a/src/builder/add_member.rs +++ b/src/builder/add_member.rs @@ -1,16 +1,21 @@ -use crate::model::id::RoleId; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; /// A builder to add parameters when using [`GuildId::add_member`]. /// /// [`GuildId::add_member`]: crate::model::id::GuildId::add_member #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct AddMember { #[serde(skip_serializing_if = "Option::is_none")] access_token: Option, #[serde(skip_serializing_if = "Option::is_none")] nick: Option, - #[serde(skip_serializing_if = "Option::is_none")] - roles: Option>, + #[serde(skip_serializing_if = "Vec::is_empty")] + roles: Vec, #[serde(skip_serializing_if = "Option::is_none")] mute: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -18,10 +23,29 @@ pub struct AddMember { } impl AddMember { + /// Adds a [`User`] to this guild with a valid OAuth2 access token. + /// + /// Returns the created [`Member`] object, or nothing if the user is already a member of the + /// guild. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + #[cfg(feature = "http")] + #[inline] + pub async fn execute( + self, + http: impl AsRef, + guild_id: GuildId, + user_id: UserId, + ) -> Result> { + http.as_ref().add_guild_member(guild_id.into(), user_id.into(), &self).await + } + /// Sets the OAuth2 access token for this request. /// /// Requires the access token to have the `guilds.join` scope granted. - pub fn access_token(&mut self, access_token: impl Into) -> &mut Self { + pub fn access_token(mut self, access_token: impl Into) -> Self { self.access_token = Some(access_token.into()); self } @@ -31,7 +55,7 @@ impl AddMember { /// Requires the [Manage Nicknames] permission. /// /// [Manage Nicknames]: crate::model::permissions::Permissions::MANAGE_NICKNAMES - pub fn nickname(&mut self, nickname: impl Into) -> &mut Self { + pub fn nickname(mut self, nickname: impl Into) -> Self { self.nick = Some(nickname.into()); self } @@ -41,8 +65,8 @@ impl AddMember { /// Requires the [Manage Roles] permission. /// /// [Manage Roles]: crate::model::permissions::Permissions::MANAGE_ROLES - pub fn roles(&mut self, roles: impl IntoIterator>) -> &mut Self { - self.roles = Some(roles.into_iter().map(Into::into).collect()); + pub fn roles(mut self, roles: impl IntoIterator>) -> Self { + self.roles = roles.into_iter().map(Into::into).collect(); self } @@ -51,7 +75,7 @@ impl AddMember { /// Requires the [Mute Members] permission. /// /// [Mute Members]: crate::model::permissions::Permissions::MUTE_MEMBERS - pub fn mute(&mut self, mute: bool) -> &mut Self { + pub fn mute(mut self, mute: bool) -> Self { self.mute = Some(mute); self } @@ -61,7 +85,7 @@ impl AddMember { /// Requires the [Deafen Members] permission. /// /// [Deafen Members]: crate::model::permissions::Permissions::DEAFEN_MEMBERS - pub fn deafen(&mut self, deafen: bool) -> &mut Self { + pub fn deafen(mut self, deafen: bool) -> Self { self.deaf = Some(deafen); self } diff --git a/src/builder/bot_auth_parameters.rs b/src/builder/bot_auth_parameters.rs index e8f1b67283e..d24e46997f9 100644 --- a/src/builder/bot_auth_parameters.rs +++ b/src/builder/bot_auth_parameters.rs @@ -9,6 +9,7 @@ use crate::model::prelude::*; /// A builder for constructing an invite link with custom OAuth2 scopes. #[derive(Debug, Clone, Default)] +#[must_use] pub struct CreateBotAuthParameters { client_id: Option, scopes: Vec, @@ -54,7 +55,7 @@ impl CreateBotAuthParameters { } /// Specify the client Id of your application. - pub fn client_id>(&mut self, client_id: U) -> &mut Self { + pub fn client_id(mut self, client_id: impl Into) -> Self { self.client_id = Some(client_id.into()); self } @@ -63,13 +64,12 @@ impl CreateBotAuthParameters { /// /// # Errors /// - /// Returns an - /// [`HttpError::UnsuccessfulRequest(Unauthorized)`][`HttpError::UnsuccessfulRequest`] - /// If the user is not authorized for this endpoint. + /// Returns an [`HttpError::UnsuccessfulRequest`] if the user is not authorized for this + /// endpoint. /// /// [`HttpError::UnsuccessfulRequest`]: crate::http::HttpError::UnsuccessfulRequest #[cfg(feature = "http")] - pub async fn auto_client_id(&mut self, http: impl AsRef) -> Result<&mut Self> { + pub async fn auto_client_id(mut self, http: impl AsRef) -> Result { self.client_id = http.as_ref().get_current_application_info().await.map(|v| Some(v.id))?; Ok(self) } @@ -79,25 +79,25 @@ impl CreateBotAuthParameters { /// **Note**: This needs to include the [`Bot`] scope. /// /// [`Bot`]: Scope::Bot - pub fn scopes(&mut self, scopes: &[Scope]) -> &mut Self { + pub fn scopes(mut self, scopes: &[Scope]) -> Self { self.scopes = scopes.to_vec(); self } /// Specify the permissions your application requires. - pub fn permissions(&mut self, permissions: Permissions) -> &mut Self { + pub fn permissions(mut self, permissions: Permissions) -> Self { self.permissions = permissions; self } /// Specify the Id of the guild to prefill the dropdown picker for the user. - pub fn guild_id>(&mut self, guild_id: G) -> &mut Self { + pub fn guild_id(mut self, guild_id: impl Into) -> Self { self.guild_id = Some(guild_id.into()); self } /// Specify whether the user cannot change the guild in the dropdown picker. - pub fn disable_guild_select(&mut self, disable: bool) -> &mut Self { + pub fn disable_guild_select(mut self, disable: bool) -> Self { self.disable_guild_select = disable; self } diff --git a/src/builder/create_allowed_mentions.rs b/src/builder/create_allowed_mentions.rs index b9c927ea402..a059ccd71d3 100644 --- a/src/builder/create_allowed_mentions.rs +++ b/src/builder/create_allowed_mentions.rs @@ -10,39 +10,57 @@ pub enum ParseValue { Roles, } -/// A builder to manage the allowed mentions on a message, -/// used by the [`ChannelId::send_message`] and -/// [`ChannelId::edit_message`] methods. +/// A builder to manage the allowed mentions on a message, used by the [`ChannelId::send_message`] +/// and [`ChannelId::edit_message`] methods. /// /// # Examples /// -/// ```rust,ignore -/// use serenity::builder::ParseValue; +/// ```rust,no_run +/// # use serenity::builder::CreateMessage; +/// # use serenity::http::Http; +/// # use serenity::model::id::{ChannelId, MessageId}; +/// # +/// # async fn run() -> Result<(), Box> { +/// # let http = Http::new("token"); +/// # let b = CreateMessage::default(); +/// # let msg = ChannelId::new(7).message(&http, MessageId::new(8)).await?; +/// use serenity::builder::{CreateAllowedMentions as Am, ParseValue}; /// /// // Mention only the user 110372470472613888 -/// m.allowed_mentions(|am| am.empty_parse().users(vec![110372470472613888])); +/// # let m = b.clone(); +/// m.allowed_mentions(Am::default().users(vec![110372470472613888])); /// /// // Mention all users and the role 182894738100322304 -/// m.allowed_mentions(|am| am.parse(ParseValue::Users).roles(vec![182894738100322304])); +/// # let m = b.clone(); +/// m.allowed_mentions(Am::default().parse(ParseValue::Users).roles(vec![182894738100322304])); /// /// // Mention all roles and nothing else -/// m.allowed_mentions(|am| am.parse(ParseValue::Roles)); +/// # let m = b.clone(); +/// m.allowed_mentions(Am::default().parse(ParseValue::Roles)); /// /// // Mention all roles and users, but not everyone -/// m.allowed_mentions(|am| am.parse(ParseValue::Users).parse(ParseValue::Roles)); +/// # let m = b.clone(); +/// m.allowed_mentions(Am::default().parse(ParseValue::Users).parse(ParseValue::Roles)); /// /// // Mention everyone and the users 182891574139682816, 110372470472613888 -/// m.allowed_mentions(|am| { -/// am.parse(ParseValue::Everyone).users(vec![182891574139682816, 110372470472613888]) -/// }); +/// # let m = b.clone(); +/// m.allowed_mentions( +/// Am::default() +/// .parse(ParseValue::Everyone) +/// .users(vec![182891574139682816, 110372470472613888]), +/// ); /// /// // Mention everyone and the message author. -/// m.allowed_mentions(|am| am.parse(ParseValue::Everyone).users(vec![msg.author.id])); +/// # let m = b.clone(); +/// m.allowed_mentions(Am::default().parse(ParseValue::Everyone).users(vec![msg.author.id])); +/// # Ok(()) +/// # } /// ``` /// /// [`ChannelId::send_message`]: crate::model::id::ChannelId::send_message /// [`ChannelId::edit_message`]: crate::model::id::ChannelId::edit_message #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateAllowedMentions { parse: Vec, users: Vec, @@ -54,55 +72,56 @@ pub struct CreateAllowedMentions { impl CreateAllowedMentions { /// Add a value that's allowed to be mentioned. /// - /// If users or roles is specified, [`Self::users`] and [`Self::roles`] will not work.\ - /// If you use either, do not specify it's same type here. + /// If passing in [`ParseValue::Users`] or [`ParseValue::Roles`], note that later calling + /// [`Self::users`] or [`Self::roles`] will then not work as intended, as the [`ParseValue`] + /// will take precedence. #[inline] - pub fn parse(&mut self, value: ParseValue) -> &mut Self { + pub fn parse(mut self, value: ParseValue) -> Self { self.parse.push(value); self } /// Clear all the values that would be mentioned. /// - /// If parse is empty, the message will not mention anyone, unless they are specified on - /// [`Self::users`] or [`Self::roles`]. + /// Will disable all mentions, except for any specific ones added with [`Self::users`] or + /// [`Self::roles`]. #[inline] - pub fn empty_parse(&mut self) -> &mut Self { + pub fn empty_parse(mut self) -> Self { self.parse.clear(); self } - /// Sets the users that will be allowed to be mentioned. + /// Sets the *specific* users that will be allowed mentionable. #[inline] - pub fn users(&mut self, users: impl IntoIterator>) -> &mut Self { + pub fn users(mut self, users: impl IntoIterator>) -> Self { self.users = users.into_iter().map(Into::into).collect(); self } - /// Makes users unable to be mentioned. + /// Clear the list of mentionable users. #[inline] - pub fn empty_users(&mut self) -> &mut Self { + pub fn empty_users(mut self) -> Self { self.users.clear(); self } - /// Sets the roles that will be allowed to be mentioned. + /// Sets the *specific* roles that will be allowed mentionable. #[inline] - pub fn roles(&mut self, roles: impl IntoIterator>) -> &mut Self { + pub fn roles(mut self, roles: impl IntoIterator>) -> Self { self.roles = roles.into_iter().map(Into::into).collect(); self } - /// Makes roles unable to be mentioned. + /// Clear the list of mentionable roles. #[inline] - pub fn empty_roles(&mut self) -> &mut Self { + pub fn empty_roles(mut self) -> Self { self.roles.clear(); self } /// Makes the reply mention/ping the user. #[inline] - pub fn replied_user(&mut self, mention_user: bool) -> &mut Self { + pub fn replied_user(mut self, mention_user: bool) -> Self { self.replied_user = Some(mention_user); self } diff --git a/src/builder/create_application_command.rs b/src/builder/create_application_command.rs index 018a47e2c59..18e3b2f3143 100644 --- a/src/builder/create_application_command.rs +++ b/src/builder/create_application_command.rs @@ -1,9 +1,12 @@ use std::collections::HashMap; -use crate::json::prelude::*; +#[cfg(feature = "http")] +use crate::http::Http; +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::application::command::Command; use crate::model::application::command::{CommandOptionType, CommandType}; -use crate::model::channel::ChannelType; -use crate::model::Permissions; +use crate::model::prelude::*; #[derive(Clone, Debug, Serialize)] pub struct CommandOptionChoice { @@ -24,10 +27,8 @@ enum Number { /// [`Self::kind`], [`Self::name`], and [`Self::description`] are required fields. /// /// [`CommandOption`]: crate::model::application::command::CommandOption -/// [`kind`]: Self::kind -/// [`name`]: Self::name -/// [`description`]: Self::description #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateApplicationCommandOption { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "type")] @@ -60,7 +61,7 @@ pub struct CreateApplicationCommandOption { impl CreateApplicationCommandOption { /// Sets the `CommandOptionType`. - pub fn kind(&mut self, kind: CommandOptionType) -> &mut Self { + pub fn kind(mut self, kind: CommandOptionType) -> Self { self.kind = Some(kind); self } @@ -68,7 +69,7 @@ impl CreateApplicationCommandOption { /// Sets the name of the option. /// /// **Note**: Must be between 1 and 32 lowercase characters, matching `r"^[\w-]{1,32}$"`. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } @@ -81,11 +82,7 @@ impl CreateApplicationCommandOption { /// .name_localized("zh-CN", "岁数") /// # ; /// ``` - pub fn name_localized( - &mut self, - locale: impl Into, - name: impl Into, - ) -> &mut Self { + pub fn name_localized(mut self, locale: impl Into, name: impl Into) -> Self { self.name_localizations.insert(locale.into(), name.into()); self } @@ -93,7 +90,7 @@ impl CreateApplicationCommandOption { /// Sets the description for the option. /// /// **Note**: Must be between 1 and 100 characters. - pub fn description(&mut self, description: impl Into) -> &mut Self { + pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); self } @@ -107,10 +104,10 @@ impl CreateApplicationCommandOption { /// # ; /// ``` pub fn description_localized( - &mut self, + mut self, locale: impl Into, description: impl Into, - ) -> &mut Self { + ) -> Self { self.description_localizations.insert(locale.into(), description.into()); self } @@ -118,7 +115,7 @@ impl CreateApplicationCommandOption { /// The first required option for the user to complete. /// /// **Note**: Only one option can be `default`. - pub fn default_option(&mut self, default: bool) -> &mut Self { + pub fn default_option(mut self, default: bool) -> Self { self.default = Some(default); self } @@ -126,15 +123,16 @@ impl CreateApplicationCommandOption { /// Sets if this option is required or optional. /// /// **Note**: This defaults to `false`. - pub fn required(&mut self, required: bool) -> &mut Self { + pub fn required(mut self, required: bool) -> Self { self.required = Some(required); self } /// Adds an optional int-choice. /// - /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 characters. Value must be between -2^53 and 2^53. - pub fn add_int_choice(&mut self, name: impl Into, value: i32) -> &mut Self { + /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 + /// characters. Value must be between -2^53 and 2^53. + pub fn add_int_choice(self, name: impl Into, value: i32) -> Self { self.add_choice(CommandOptionChoice { name: name.into(), value: Value::from(value), @@ -144,11 +142,11 @@ impl CreateApplicationCommandOption { /// Adds a localized optional int-choice. See [`Self::add_int_choice`] for more info. pub fn add_int_choice_localized( - &mut self, + self, name: impl Into, value: i32, locales: impl IntoIterator, impl Into)>, - ) -> &mut Self { + ) -> Self { self.add_choice(CommandOptionChoice { name: name.into(), value: Value::from(value), @@ -158,12 +156,9 @@ impl CreateApplicationCommandOption { /// Adds an optional string-choice. /// - /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 characters. Value must be up to 100 characters. - pub fn add_string_choice( - &mut self, - name: impl Into, - value: impl Into, - ) -> &mut Self { + /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 + /// characters. Value must be up to 100 characters. + pub fn add_string_choice(self, name: impl Into, value: impl Into) -> Self { self.add_choice(CommandOptionChoice { name: name.into(), value: Value::String(value.into()), @@ -173,11 +168,11 @@ impl CreateApplicationCommandOption { /// Adds a localized optional string-choice. See [`Self::add_string_choice`] for more info. pub fn add_string_choice_localized( - &mut self, + self, name: impl Into, value: impl Into, locales: impl IntoIterator, impl Into)>, - ) -> &mut Self { + ) -> Self { self.add_choice(CommandOptionChoice { name: name.into(), value: Value::String(value.into()), @@ -187,8 +182,9 @@ impl CreateApplicationCommandOption { /// Adds an optional number-choice. /// - /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 characters. Value must be between -2^53 and 2^53. - pub fn add_number_choice(&mut self, name: impl Into, value: f64) -> &mut Self { + /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 + /// characters. Value must be between -2^53 and 2^53. + pub fn add_number_choice(self, name: impl Into, value: f64) -> Self { self.add_choice(CommandOptionChoice { name: name.into(), value: Value::from(value), @@ -198,11 +194,11 @@ impl CreateApplicationCommandOption { /// Adds a localized optional number-choice. See [`Self::add_number_choice`] for more info. pub fn add_number_choice_localized( - &mut self, + self, name: impl Into, value: f64, locales: impl IntoIterator, impl Into)>, - ) -> &mut Self { + ) -> Self { self.add_choice(CommandOptionChoice { name: name.into(), value: Value::from(value), @@ -210,7 +206,7 @@ impl CreateApplicationCommandOption { }) } - fn add_choice(&mut self, value: CommandOptionChoice) -> &mut Self { + fn add_choice(mut self, value: CommandOptionChoice) -> Self { self.choices.push(value); self } @@ -220,73 +216,52 @@ impl CreateApplicationCommandOption { /// **Notes**: /// - May not be set to `true` if `choices` are set /// - Options using `autocomplete` are not confined to only use given choices - pub fn set_autocomplete(&mut self, value: bool) -> &mut Self { + pub fn set_autocomplete(mut self, value: bool) -> Self { self.autocomplete = Some(value); - self } /// If the option is a [`SubCommandGroup`] or [`SubCommand`], nested options are its parameters. /// - /// **Note**: A command can have up to 25 subcommand groups, or subcommands. A subcommand group can have up to 25 subcommands. A subcommand can have up to 25 options. - /// - /// [`SubCommandGroup`]: crate::model::application::command::CommandOptionType::SubCommandGroup - /// [`SubCommand`]: crate::model::application::command::CommandOptionType::SubCommand - pub fn create_sub_option(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateApplicationCommandOption) -> &mut CreateApplicationCommandOption, - { - let mut data = CreateApplicationCommandOption::default(); - f(&mut data); - self.add_sub_option(data) - } - - /// If the option is a [`SubCommandGroup`] or [`SubCommand`], nested options are its parameters. - /// - /// **Note**: A command can have up to 25 subcommand groups, or subcommands. A subcommand group can have up to 25 subcommands. A subcommand can have up to 25 options. + /// **Note**: A command can have up to 25 subcommand groups, or subcommands. A subcommand group + /// can have up to 25 subcommands. A subcommand can have up to 25 options. /// /// [`SubCommandGroup`]: crate::model::application::command::CommandOptionType::SubCommandGroup /// [`SubCommand`]: crate::model::application::command::CommandOptionType::SubCommand - pub fn add_sub_option(&mut self, sub_option: CreateApplicationCommandOption) -> &mut Self { + pub fn add_sub_option(mut self, sub_option: CreateApplicationCommandOption) -> Self { self.options.push(sub_option); - self } /// If the option is a [`Channel`], it will only be able to show these types. /// /// [`Channel`]: crate::model::application::command::CommandOptionType::Channel - pub fn channel_types(&mut self, channel_types: Vec) -> &mut Self { + pub fn channel_types(mut self, channel_types: Vec) -> Self { self.channel_types = channel_types; - self } /// Sets the minimum permitted value for this integer option - pub fn min_int_value(&mut self, value: u64) -> &mut Self { + pub fn min_int_value(mut self, value: u64) -> Self { self.min_value = Some(Number::Integer(value)); - self } /// Sets the maximum permitted value for this integer option - pub fn max_int_value(&mut self, value: u64) -> &mut Self { + pub fn max_int_value(mut self, value: u64) -> Self { self.max_value = Some(Number::Integer(value)); - self } /// Sets the minimum permitted value for this number option - pub fn min_number_value(&mut self, value: f64) -> &mut Self { + pub fn min_number_value(mut self, value: f64) -> Self { self.min_value = Some(Number::Float(value)); - self } /// Sets the maximum permitted value for this number option - pub fn max_number_value(&mut self, value: f64) -> &mut Self { + pub fn max_number_value(mut self, value: f64) -> Self { self.max_value = Some(Number::Float(value)); - self } @@ -315,6 +290,7 @@ impl CreateApplicationCommandOption { /// /// [`Command`]: crate::model::application::command::Command #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateApplicationCommand { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "type")] @@ -335,10 +311,48 @@ pub struct CreateApplicationCommand { } impl CreateApplicationCommand { + /// Create a [`Command`], overriding an existing one with the same name if it exists. + /// + /// Providing a `command_id` will edit the corresponding command. + /// + /// Providing a `guild_id` will create a command in the corresponding [`Guild`]. Otherwise, a + /// global command will be created. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if invalid data is given. See [Discord's docs] for more details. + /// + /// May also return [`Error::Json`] if there is an error in deserializing the API response. + /// + /// [Discord's docs]: https://discord.com/developers/docs/interactions/slash-commands + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + guild_id: Option, + command_id: Option, + ) -> Result { + let http = http.as_ref(); + match (guild_id, command_id) { + (Some(guild_id), Some(command_id)) => { + http.edit_guild_application_command(guild_id.into(), command_id.into(), &self).await + }, + (Some(guild_id), None) => { + http.create_guild_application_command(guild_id.into(), &self).await + }, + (None, Some(command_id)) => { + http.edit_global_application_command(command_id.into(), &self).await + }, + (None, None) => http.create_global_application_command(&self).await, + } + } + /// Specifies the name of the application command. /// - /// **Note**: Must be between 1 and 32 lowercase characters, matching `r"^[\w-]{1,32}$"`. Two global commands of the same app cannot have the same name. Two guild-specific commands of the same app cannot have the same name. - pub fn name(&mut self, name: impl Into) -> &mut Self { + /// **Note**: Must be between 1 and 32 lowercase characters, matching `r"^[\w-]{1,32}$"`. Two + /// global commands of the same app cannot have the same name. Two guild-specific commands of + /// the same app cannot have the same name. + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } @@ -352,29 +366,25 @@ impl CreateApplicationCommand { /// .name_localized("el", "γενέθλια") /// # ; /// ``` - pub fn name_localized( - &mut self, - locale: impl Into, - name: impl Into, - ) -> &mut Self { + pub fn name_localized(mut self, locale: impl Into, name: impl Into) -> Self { self.name_localizations.insert(locale.into(), name.into()); self } /// Specifies the type of the application command. - pub fn kind(&mut self, kind: CommandType) -> &mut Self { + pub fn kind(mut self, kind: CommandType) -> Self { self.kind = Some(kind); self } /// Specifies the default permissions required to execute the command. - pub fn default_member_permissions(&mut self, permissions: Permissions) -> &mut Self { + pub fn default_member_permissions(mut self, permissions: Permissions) -> Self { self.default_member_permissions = Some(permissions.bits().to_string()); self } /// Specifies if the command is available in DMs. - pub fn dm_permission(&mut self, enabled: bool) -> &mut Self { + pub fn dm_permission(mut self, enabled: bool) -> Self { self.dm_permission = Some(enabled); self @@ -383,7 +393,7 @@ impl CreateApplicationCommand { /// Specifies the description of the application command. /// /// **Note**: Must be between 1 and 100 characters long. - pub fn description(&mut self, description: impl Into) -> &mut Self { + pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); self } @@ -397,75 +407,71 @@ impl CreateApplicationCommand { /// # ; /// ``` pub fn description_localized( - &mut self, + mut self, locale: impl Into, + description: impl Into, - ) -> &mut Self { + ) -> Self { self.description_localizations.insert(locale.into(), description.into()); self } - /// Creates an application command option for the application command. - /// - /// **Note**: Application commands can have up to 25 options. - pub fn create_option(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateApplicationCommandOption) -> &mut CreateApplicationCommandOption, - { - let mut data = CreateApplicationCommandOption::default(); - f(&mut data); - self.add_option(data) - } - /// Adds an application command option for the application command. /// /// **Note**: Application commands can have up to 25 options. - pub fn add_option(&mut self, option: CreateApplicationCommandOption) -> &mut Self { + pub fn add_option(mut self, option: CreateApplicationCommandOption) -> Self { self.options.push(option); - self } /// Sets all the application command options for the application command. /// /// **Note**: Application commands can have up to 25 options. - pub fn set_options(&mut self, options: Vec) -> &mut Self { + pub fn set_options(mut self, options: Vec) -> Self { self.options = options; self } } #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateApplicationCommands(pub Vec); impl CreateApplicationCommands { - /// Creates a new application command. - pub fn create_application_command(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, - { - let mut data = CreateApplicationCommand::default(); - f(&mut data); - - self.add_application_command(data); - - self + /// Create multiple application commands in bulk, overwriting the existing command list. + /// + /// Providing a `guild_id` will overwrite all application commands in the corresponding + /// [`Guild`]. Otherwise, will overwrite all global application commands. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if invalid data is given. See [Discord's docs] for more details. + /// + /// May also return [`Error::Json`] if there is an error in deserializing the API response. + /// + /// [Discord's docs]: https://discord.com/developers/docs/interactions/slash-commands + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + guild_id: Option, + ) -> Result> { + let http = http.as_ref(); + match guild_id { + Some(guild_id) => http.create_guild_application_commands(guild_id.into(), &self).await, + None => http.create_global_application_commands(&self).await, + } } /// Adds a new application command. - pub fn add_application_command(&mut self, command: CreateApplicationCommand) -> &mut Self { + pub fn add_application_command(mut self, command: CreateApplicationCommand) -> Self { self.0.push(command); - self } /// Sets all the application commands. - pub fn set_application_commands( - &mut self, - commands: Vec, - ) -> &mut Self { + pub fn set_application_commands(mut self, commands: Vec) -> Self { self.0.extend(commands); - self } } diff --git a/src/builder/create_application_command_permission.rs b/src/builder/create_application_command_permission.rs index 546d3cb81e9..b12b0662b27 100644 --- a/src/builder/create_application_command_permission.rs +++ b/src/builder/create_application_command_permission.rs @@ -1,47 +1,57 @@ +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::application::command::CommandPermission; use crate::model::application::command::CommandPermissionType; -use crate::model::id::CommandId; +use crate::model::prelude::*; /// A builder for creating several [`CommandPermission`]. -/// -/// [`CommandPermission`]: crate::model::application::command::CommandPermission #[derive(Clone, Debug, Default, Serialize)] #[deprecated(note = "use `CreateApplicationCommandPermissionsData`")] +#[must_use] pub struct CreateApplicationCommandsPermissions(Vec); #[allow(deprecated)] impl CreateApplicationCommandsPermissions { - /// Creates a new application command. - pub fn create_application_command(&mut self, f: F) -> &mut Self - where - F: FnOnce( - &mut CreateApplicationCommandPermissions, - ) -> &mut CreateApplicationCommandPermissions, - { - let mut data = CreateApplicationCommandPermissions::default(); - f(&mut data); - - self.add_application_command(data); - - self + /// Overwrite permissions for all application commands in the guild. + /// + /// **Note**: Per [Discord's docs], this endpoint has been disabled and will always return an + /// error. Use [`CreateApplicationCommandPermissionsData`] instead to update permissions one + /// command at a time. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if invalid data is given. + /// + /// May also return [`Error::Json`] if there is an error in deserializing the API response. + /// + /// [Discord's docs]: https://discord.com/developers/docs/interactions/application-commands#batch-edit-application-command-permissions + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + guild_id: GuildId, + ) -> Result> { + http.as_ref().edit_guild_application_commands_permissions(guild_id.into(), &self).await } /// Adds a new application command. pub fn add_application_command( - &mut self, + mut self, application_command: CreateApplicationCommandPermissions, - ) -> &mut Self { + ) -> Self { self.0.push(application_command); - self } /// Sets all the application commands. pub fn set_application_commands( - &mut self, + mut self, application_commands: Vec, - ) -> &mut Self { + ) -> Self { self.0 = application_commands; - self } } @@ -51,6 +61,7 @@ impl CreateApplicationCommandsPermissions { /// [`CommandPermission`]: crate::model::application::command::CommandPermission #[derive(Clone, Debug, Default, Serialize)] #[deprecated(note = "use `CreateApplicationCommandPermissionsData`")] +#[must_use] pub struct CreateApplicationCommandPermissions { #[serde(skip_serializing_if = "Option::is_none")] id: Option, @@ -62,40 +73,22 @@ impl CreateApplicationCommandPermissions { /// The [`CommandId`] these permissions belong to. /// /// [`CommandId`]: crate::model::id::CommandId - pub fn id(&mut self, application_command_id: impl Into) -> &mut Self { + pub fn id(mut self, application_command_id: impl Into) -> Self { self.id = Some(application_command_id.into()); self } - /// Creates permissions for the application command. - pub fn create_permissions(&mut self, f: F) -> &mut Self - where - F: FnOnce( - &mut CreateApplicationCommandPermissionData, - ) -> &mut CreateApplicationCommandPermissionData, - { - let mut data = CreateApplicationCommandPermissionData::default(); - f(&mut data); - - self.add_permissions(data); - - self - } - /// Adds permission for the application command. - pub fn add_permissions( - &mut self, - permission: CreateApplicationCommandPermissionData, - ) -> &mut Self { + pub fn add_permissions(mut self, permission: CreateApplicationCommandPermissionData) -> Self { self.permissions.push(permission); self } /// Sets permissions for the application command. pub fn set_permissions( - &mut self, + mut self, permissions: Vec, - ) -> &mut Self { + ) -> Self { self.permissions = permissions; self } @@ -105,40 +98,47 @@ impl CreateApplicationCommandPermissions { /// /// [`CommandPermissionData`]: crate::model::application::command::CommandPermissionData #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateApplicationCommandPermissionsData { permissions: Vec, } impl CreateApplicationCommandPermissionsData { - /// Creates a permission for the application command. - pub fn create_permission(&mut self, f: F) -> &mut Self - where - F: FnOnce( - &mut CreateApplicationCommandPermissionData, - ) -> &mut CreateApplicationCommandPermissionData, - { - let mut data = CreateApplicationCommandPermissionData::default(); - f(&mut data); - - self.add_permission(data); - - self + /// Create permissions for a guild application command. These will overwrite any existing + /// permissions for that command. + /// + /// **Note**: The permissions will update instantly. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if invalid data is given. See [Discord's docs] for more details. + /// + /// May also return [`Error::Json`] if there is an error in deserializing the API response. + /// + /// [Discord's docs]: https://discord.com/developers/docs/interactions/slash-commands + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + guild_id: GuildId, + command_id: CommandId, + ) -> Result { + http.as_ref() + .edit_guild_application_command_permissions(guild_id.into(), command_id.into(), &self) + .await } /// Adds a permission for the application command. - pub fn add_permission( - &mut self, - permission: CreateApplicationCommandPermissionData, - ) -> &mut Self { + pub fn add_permission(mut self, permission: CreateApplicationCommandPermissionData) -> Self { self.permissions.push(permission); self } /// Sets permissions for the application command. pub fn set_permissions( - &mut self, + mut self, permissions: Vec, - ) -> &mut Self { + ) -> Self { self.permissions = permissions; self } @@ -150,6 +150,7 @@ impl CreateApplicationCommandPermissionsData { /// /// [`CommandPermissionData`]: crate::model::application::command::CommandPermissionData #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateApplicationCommandPermissionData { #[serde(rename = "type")] #[serde(skip_serializing_if = "Option::is_none")] @@ -164,7 +165,7 @@ impl CreateApplicationCommandPermissionData { /// Sets the `CommandPermissionType` for the [`CommandPermissionData`]. /// /// [`CommandPermissionData`]: crate::model::application::command::CommandPermissionData - pub fn kind(&mut self, kind: CommandPermissionType) -> &mut Self { + pub fn kind(mut self, kind: CommandPermissionType) -> Self { self.kind = Some(kind); self } @@ -172,18 +173,18 @@ impl CreateApplicationCommandPermissionData { /// Sets the CommandPermissionId for the [`CommandPermissionData`]. /// /// [`CommandPermissionData`]: crate::model::application::command::CommandPermissionData - pub fn id(&mut self, id: u64) -> &mut Self { + pub fn id(mut self, id: u64) -> Self { self.id = Some(id.to_string()); self } /// Sets the permission for the [`CommandPermissionData`]. /// - /// **Note**: Setting it to `false` will only grey the application command in the - /// list, it will not fully hide it to the user. + /// **Note**: Passing `false` will only grey-out the application command in the list, and will + /// not fully hide it from the user. /// /// [`CommandPermissionData`]: crate::model::application::command::CommandPermissionData - pub fn permission(&mut self, permission: bool) -> &mut Self { + pub fn permission(mut self, permission: bool) -> Self { self.permission = Some(permission); self } diff --git a/src/builder/create_channel.rs b/src/builder/create_channel.rs index 5954c1001f8..92e62ff93ea 100644 --- a/src/builder/create_channel.rs +++ b/src/builder/create_channel.rs @@ -1,12 +1,14 @@ +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; use crate::model::prelude::*; /// A builder for creating a new [`GuildChannel`] in a [`Guild`]. /// /// Except [`Self::name`], all fields are optional. -/// -/// [`GuildChannel`]: crate::model::channel::GuildChannel -/// [`Guild`]: crate::model::guild::Guild -#[derive(Debug, Clone, Serialize)] +#[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateChannel { kind: ChannelType, #[serde(skip_serializing_if = "Option::is_none")] @@ -29,55 +31,87 @@ pub struct CreateChannel { } impl CreateChannel { + /// Creates a new [`Channel`] in the guild. + /// + /// **Note**: Requires the [Manage Channels] permission. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// + /// [Manage Channels]: Permissions::MANAGE_CHANNELS + #[cfg(feature = "http")] + #[inline] + pub async fn execute( + self, + cache_http: impl CacheHttp, + guild_id: GuildId, + ) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(guild) = cache.guild(guild_id) { + let req = Permissions::MANAGE_CHANNELS; + + if !guild.has_perms(&cache_http, req).await { + return Err(Error::Model(ModelError::InvalidPermissions(req))); + } + } + } + } + + self._execute(cache_http.http(), guild_id).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http, guild_id: GuildId) -> Result { + http.create_channel(guild_id.into(), &self, None).await + } + /// Specify how to call this new channel. /// /// **Note**: Must be between 2 and 100 characters long. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); - self } + /// Specify what type the channel is, whether it's a text, voice, category or news channel. - pub fn kind(&mut self, kind: ChannelType) -> &mut Self { + pub fn kind(mut self, kind: ChannelType) -> Self { self.kind = kind; - self } /// Specify the category, the "parent" of this channel. - pub fn category>(&mut self, id: I) -> &mut Self { + pub fn category>(mut self, id: I) -> Self { self.parent_id = Some(id.into()); - self } /// Set an interesting topic. /// /// **Note**: Must be between 0 and 1000 characters long. - pub fn topic(&mut self, topic: impl Into) -> &mut Self { + pub fn topic(mut self, topic: impl Into) -> Self { self.topic = Some(topic.into()); - self } /// Specify if this channel will be inappropriate to browse while at work. - pub fn nsfw(&mut self, b: bool) -> &mut Self { + pub fn nsfw(mut self, b: bool) -> Self { self.nsfw = Some(b); - self } /// [Voice-only] Specify the bitrate at which sound plays in the voice channel. - pub fn bitrate(&mut self, rate: u32) -> &mut Self { + pub fn bitrate(mut self, rate: u32) -> Self { self.bitrate = Some(rate); - self } /// [Voice-only] Set how many users may occupy this voice channel. - pub fn user_limit(&mut self, limit: u32) -> &mut Self { + pub fn user_limit(mut self, limit: u32) -> Self { self.user_limit = Some(limit); - self } @@ -91,21 +125,19 @@ impl CreateChannel { /// [`MANAGE_MESSAGES`]: crate::model::permissions::Permissions::MANAGE_MESSAGES /// [`MANAGE_CHANNELS`]: crate::model::permissions::Permissions::MANAGE_CHANNELS #[doc(alias = "slowmode")] - pub fn rate_limit_per_user(&mut self, seconds: u64) -> &mut Self { + pub fn rate_limit_per_user(mut self, seconds: u64) -> Self { self.rate_limit_per_user = Some(seconds); - self } /// Specify where the channel should be located. - pub fn position(&mut self, pos: u32) -> &mut Self { + pub fn position(mut self, pos: u32) -> Self { self.position = Some(pos); - self } - /// A set of overwrites defining what a user or a user carrying a certain role can - /// and cannot do. + /// A set of overwrites defining what a user or a user carrying a certain role can and cannot + /// do. /// /// # Example /// @@ -118,6 +150,7 @@ impl CreateChannel { /// # async fn run() -> Result<(), Box> { /// # let http = Arc::new(Http::new("token")); /// # let mut guild = GuildId::new(1).to_partial_guild(&http).await?; + /// use serenity::builder::CreateChannel; /// use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType}; /// use serenity::model::id::UserId; /// use serenity::model::permissions::Permissions; @@ -129,34 +162,24 @@ impl CreateChannel { /// kind: PermissionOverwriteType::Member(UserId::new(1234)), /// }]; /// - /// guild.create_channel(http, |c| c.name("my_new_cool_channel").permissions(permissions)).await?; + /// let builder = CreateChannel::default().name("my_new_cool_channel").permissions(permissions); + /// guild.create_channel(&http, builder).await?; /// # Ok(()) /// # } /// ``` - pub fn permissions(&mut self, perms: I) -> &mut Self + pub fn permissions(mut self, perms: I) -> Self where I: IntoIterator, { self.permission_overwrites = perms.into_iter().map(Into::into).collect(); - self } } impl Default for CreateChannel { /// Creates a builder with default values, setting [`Self::kind`] to [`ChannelType::Text`]. - /// - /// # Examples - /// - /// Create a default [`CreateChannel`] builder: - /// - /// ```rust - /// use serenity::builder::CreateChannel; - /// - /// let channel_builder = CreateChannel::default(); - /// ``` fn default() -> Self { - CreateChannel { + Self { name: None, nsfw: None, topic: None, diff --git a/src/builder/create_components.rs b/src/builder/create_components.rs index f2009e22486..caf577d7032 100644 --- a/src/builder/create_components.rs +++ b/src/builder/create_components.rs @@ -5,40 +5,30 @@ use crate::model::channel::ReactionType; /// /// [`ActionRow`]: crate::model::application::component::ActionRow #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateComponents(pub Vec); impl CreateComponents { - /// Creates an action row. - pub fn create_action_row(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateActionRow) -> &mut CreateActionRow, - { - let mut data = CreateActionRow::default(); - f(&mut data); - - self.add_action_row(data); - - self - } - /// Adds an action row. - pub fn add_action_row(&mut self, row: CreateActionRow) -> &mut Self { + pub fn add_action_row(mut self, row: CreateActionRow) -> Self { self.0.push(row); + self + } + pub fn add_action_rows(mut self, rows: Vec) -> Self { + self.0.extend(rows); self } - /// Set a single action row. - /// Calling this will overwrite all action rows. - pub fn set_action_row(&mut self, row: CreateActionRow) -> &mut Self { + /// Set a single action row. Calling this will overwrite all action rows. + pub fn set_action_row(mut self, row: CreateActionRow) -> Self { self.0 = vec![row]; - self } - /// Sets all the action rows. - pub fn set_action_rows(&mut self, rows: Vec) -> &mut Self { - self.0.extend(rows); + /// Sets all the action rows. Calling this will overwrite all action rows. + pub fn set_action_rows(mut self, rows: Vec) -> Self { + self.0 = rows; self } } @@ -55,6 +45,7 @@ enum ComponentBuilder { /// /// [`ActionRow`]: crate::model::application::component::ActionRow #[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateActionRow { components: Vec, #[serde(rename = "type")] @@ -71,59 +62,20 @@ impl Default for CreateActionRow { } impl CreateActionRow { - /// Creates a button. - pub fn create_button(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateButton) -> &mut CreateButton, - { - let mut data = CreateButton::default(); - f(&mut data); - - self.add_button(data); - - self - } - /// Adds a button. - pub fn add_button(&mut self, button: CreateButton) -> &mut Self { + pub fn add_button(mut self, button: CreateButton) -> Self { self.components.push(ComponentBuilder::Button(button)); self } - /// Creates a select menu. - pub fn create_select_menu(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateSelectMenu) -> &mut CreateSelectMenu, - { - let mut data = CreateSelectMenu::default(); - f(&mut data); - - self.add_select_menu(data); - - self - } - /// Adds a select menu. - pub fn add_select_menu(&mut self, menu: CreateSelectMenu) -> &mut Self { + pub fn add_select_menu(mut self, menu: CreateSelectMenu) -> Self { self.components.push(ComponentBuilder::SelectMenu(menu)); self } - /// Creates an input text. - pub fn create_input_text(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateInputText) -> &mut CreateInputText, - { - let mut data = CreateInputText::default(); - f(&mut data); - - self.add_input_text(data); - - self - } - /// Adds an input text. - pub fn add_input_text(&mut self, input_text: CreateInputText) -> &mut Self { + pub fn add_input_text(mut self, input_text: CreateInputText) -> Self { self.components.push(ComponentBuilder::InputText(input_text)); self } @@ -133,6 +85,7 @@ impl CreateActionRow { /// /// [`Button`]: crate::model::application::component::Button #[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateButton { #[serde(skip_serializing_if = "Option::is_none")] label: Option, @@ -167,37 +120,37 @@ impl Default for CreateButton { impl CreateButton { /// Sets the style of the button. - pub fn style(&mut self, kind: ButtonStyle) -> &mut Self { + pub fn style(mut self, kind: ButtonStyle) -> Self { self.style = kind; self } /// The label of the button. - pub fn label(&mut self, label: impl Into) -> &mut Self { + pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); self } /// Sets the custom id of the button, a developer-defined identifier. - pub fn custom_id(&mut self, id: impl Into) -> &mut Self { + pub fn custom_id(mut self, id: impl Into) -> Self { self.custom_id = Some(id.into()); self } /// The url for url style button. - pub fn url(&mut self, url: impl Into) -> &mut Self { + pub fn url(mut self, url: impl Into) -> Self { self.url = Some(url.into()); self } /// Sets emoji of the button. - pub fn emoji>(&mut self, emoji: R) -> &mut Self { + pub fn emoji(mut self, emoji: impl Into) -> Self { self.emoji = Some(emoji.into()); self } /// Sets the disabled state for the button. - pub fn disabled(&mut self, disabled: bool) -> &mut Self { + pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = Some(disabled); self } @@ -207,6 +160,7 @@ impl CreateButton { /// /// [`SelectMenu`]: crate::model::application::component::SelectMenu #[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateSelectMenu { #[serde(skip_serializing_if = "Option::is_none")] placeholder: Option, @@ -241,42 +195,36 @@ impl Default for CreateSelectMenu { impl CreateSelectMenu { /// The placeholder of the select menu. - pub fn placeholder(&mut self, label: impl Into) -> &mut Self { + pub fn placeholder(mut self, label: impl Into) -> Self { self.placeholder = Some(label.into()); self } /// Sets the custom id of the select menu, a developer-defined identifier. - pub fn custom_id(&mut self, id: impl Into) -> &mut Self { + pub fn custom_id(mut self, id: impl Into) -> Self { self.custom_id = Some(id.into()); self } /// Sets the minimum values for the user to select. - pub fn min_values(&mut self, min: u64) -> &mut Self { + pub fn min_values(mut self, min: u64) -> Self { self.min_values = Some(min); self } /// Sets the maximum values for the user to select. - pub fn max_values(&mut self, max: u64) -> &mut Self { + pub fn max_values(mut self, max: u64) -> Self { self.max_values = Some(max); self } /// Sets the disabled state for the button. - pub fn disabled(&mut self, disabled: bool) -> &mut Self { + pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = Some(disabled); self } - pub fn options(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateSelectMenuOptions) -> &mut CreateSelectMenuOptions, - { - let mut options = CreateSelectMenuOptions::default(); - f(&mut options); - + pub fn options(mut self, options: CreateSelectMenuOptions) -> Self { self.options = Some(options); self } @@ -286,33 +234,19 @@ impl CreateSelectMenu { /// /// [`SelectMenuOption`]: crate::model::application::component::SelectMenuOption #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateSelectMenuOptions(pub Vec); impl CreateSelectMenuOptions { - /// Creates an option. - pub fn create_option(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateSelectMenuOption) -> &mut CreateSelectMenuOption, - { - let mut data = CreateSelectMenuOption::default(); - f(&mut data); - - self.add_option(data); - - self - } - /// Adds an option. - pub fn add_option(&mut self, option: CreateSelectMenuOption) -> &mut Self { + pub fn add_option(mut self, option: CreateSelectMenuOption) -> Self { self.0.push(option); - self } /// Sets all the options. - pub fn set_options(&mut self, options: Vec) -> &mut Self { + pub fn set_options(mut self, options: Vec) -> Self { self.0.extend(options); - self } } @@ -321,6 +255,7 @@ impl CreateSelectMenuOptions { /// /// [`SelectMenuOption`]: crate::model::application::component::SelectMenuOption #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateSelectMenuOption { #[serde(skip_serializing_if = "Option::is_none")] label: Option, @@ -337,37 +272,35 @@ pub struct CreateSelectMenuOption { impl CreateSelectMenuOption { /// Creates an option. pub fn new(label: impl Into, value: impl Into) -> Self { - let mut opt = Self::default(); - opt.label(label).value(value); - opt + Self::default().label(label).value(value) } /// Sets the label of this option. - pub fn label(&mut self, label: impl Into) -> &mut Self { + pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); self } /// Sets the value of this option. - pub fn value(&mut self, value: impl Into) -> &mut Self { + pub fn value(mut self, value: impl Into) -> Self { self.value = Some(value.into()); self } /// Sets the description shown on this option. - pub fn description(&mut self, description: impl Into) -> &mut Self { + pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); self } /// Sets emoji of the option. - pub fn emoji>(&mut self, emoji: R) -> &mut Self { + pub fn emoji(mut self, emoji: impl Into) -> Self { self.emoji = Some(emoji.into()); self } /// Sets this option as selected by default. - pub fn default_selection(&mut self, disabled: bool) -> &mut Self { + pub fn default_selection(mut self, disabled: bool) -> Self { self.default = Some(disabled); self } @@ -377,6 +310,7 @@ impl CreateSelectMenuOption { /// /// [`InputText`]: crate::model::application::component::InputText #[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateInputText { #[serde(skip_serializing_if = "Option::is_none")] custom_id: Option, @@ -417,49 +351,49 @@ impl Default for CreateInputText { impl CreateInputText { /// Sets the custom id of the input text, a developer-defined identifier. - pub fn custom_id(&mut self, id: impl Into) -> &mut Self { + pub fn custom_id(mut self, id: impl Into) -> Self { self.custom_id = Some(id.into()); self } /// Sets the style of this input text - pub fn style(&mut self, kind: InputTextStyle) -> &mut Self { + pub fn style(mut self, kind: InputTextStyle) -> Self { self.style = Some(kind); self } /// Sets the label of this input text. - pub fn label(&mut self, label: impl Into) -> &mut Self { + pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); self } /// Sets the placeholder of this input text. - pub fn placeholder(&mut self, label: impl Into) -> &mut Self { + pub fn placeholder(mut self, label: impl Into) -> Self { self.placeholder = Some(label.into()); self } /// Sets the minimum length required for the input text - pub fn min_length(&mut self, min: u64) -> &mut Self { + pub fn min_length(mut self, min: u64) -> Self { self.min_length = Some(min); self } /// Sets the maximum length required for the input text - pub fn max_length(&mut self, max: u64) -> &mut Self { + pub fn max_length(mut self, max: u64) -> Self { self.max_length = Some(max); self } /// Sets the value of this input text. - pub fn value(&mut self, value: impl Into) -> &mut Self { + pub fn value(mut self, value: impl Into) -> Self { self.value = Some(value.into()); self } /// Sets if the input text is required - pub fn required(&mut self, required: bool) -> &mut Self { + pub fn required(mut self, required: bool) -> Self { self.required = Some(required); self } diff --git a/src/builder/create_embed.rs b/src/builder/create_embed.rs index b974e65d723..d9643c6eba0 100644 --- a/src/builder/create_embed.rs +++ b/src/builder/create_embed.rs @@ -14,8 +14,9 @@ //! [`ExecuteWebhook::embeds`]: crate::builder::ExecuteWebhook::embeds //! [here]: https://discord.com/developers/docs/resources/channel#embed-object -use crate::model::channel::{Embed, EmbedField}; -use crate::model::Timestamp; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; #[cfg(feature = "utils")] use crate::utils::Colour; @@ -39,6 +40,7 @@ impl HoldsUrl { /// [`Embed`]: crate::model::channel::Embed /// [`ExecuteWebhook::embeds`]: crate::builder::ExecuteWebhook::embeds #[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateEmbed { fields: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -66,21 +68,10 @@ pub struct CreateEmbed { } impl CreateEmbed { - /// Build the author of the embed. - /// - /// Refer to the documentation for [`CreateEmbedAuthor`] for more - /// information. - pub fn author(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateEmbedAuthor) -> &mut CreateEmbedAuthor, - { - let mut author = CreateEmbedAuthor::default(); - f(&mut author); - self.set_author(author) - } - /// Set the author of the embed. - pub fn set_author(&mut self, author: CreateEmbedAuthor) -> &mut Self { + /// + /// Refer to the documentation for [`CreateEmbedAuthor`] for more information. + pub fn author(mut self, author: CreateEmbedAuthor) -> Self { self.author = Some(author); self } @@ -90,15 +81,14 @@ impl CreateEmbed { /// This is an alias of [`Self::colour`]. #[cfg(feature = "utils")] #[inline] - pub fn color>(&mut self, colour: C) -> &mut Self { - self.colour(colour); - self + pub fn color>(self, colour: C) -> Self { + self.colour(colour) } /// Set the colour of the left-hand side of the embed. #[cfg(feature = "utils")] #[inline] - pub fn colour>(&mut self, colour: C) -> &mut Self { + pub fn colour>(mut self, colour: C) -> Self { self._colour(colour.into()); self } @@ -113,14 +103,13 @@ impl CreateEmbed { /// This is an alias of [`colour`]. #[cfg(not(feature = "utils"))] #[inline] - pub fn color(&mut self, colour: u32) -> &mut Self { - self.colour(colour); - self + pub fn color(self, colour: u32) -> Self { + self.colour(colour) } /// Set the colour of the left-hand side of the embed. #[cfg(not(feature = "utils"))] - pub fn colour(&mut self, colour: u32) -> &mut Self { + pub fn colour(mut self, colour: u32) -> Self { self.colour = Some(colour); self } @@ -129,32 +118,30 @@ impl CreateEmbed { /// /// **Note**: This can't be longer than 4096 characters. #[inline] - pub fn description(&mut self, description: impl Into) -> &mut Self { + pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); self } - /// Set a field. Note that this will not overwrite other fields, and will - /// add to them. + /// Set a field. Note that this will not overwrite other fields, and will add to them. /// - /// **Note**: Maximum amount of characters you can put is 256 in a field - /// name and 1024 in a field value. + /// **Note**: Maximum amount of characters you can put is 256 in a field name and 1024 in a + /// field value. #[inline] pub fn field( - &mut self, + mut self, name: impl Into, value: impl Into, inline: bool, - ) -> &mut Self { + ) -> Self { self.fields.push(EmbedField::new(name, value, inline)); - self } /// Adds multiple fields at once. /// /// This is sugar to reduce the need of calling [`Self::field`] manually multiple times. - pub fn fields(&mut self, fields: impl IntoIterator) -> &mut Self + pub fn fields(mut self, fields: impl IntoIterator) -> Self where N: Into, V: Into, @@ -162,39 +149,27 @@ impl CreateEmbed { let fields = fields.into_iter().map(|(name, value, inline)| EmbedField::new(name, value, inline)); self.fields.extend(fields); - self } - /// Build the footer of the embed. - /// - /// Refer to the documentation for [`CreateEmbedFooter`] for more - /// information. - pub fn footer(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter, - { - let mut create_embed_footer = CreateEmbedFooter::default(); - f(&mut create_embed_footer); - self.set_footer(create_embed_footer) - } - /// Set the footer of the embed. - pub fn set_footer(&mut self, create_embed_footer: CreateEmbedFooter) -> &mut Self { - self.footer = Some(create_embed_footer); + /// + /// Refer to the documentation for [`CreateEmbedFooter`] for more information. + pub fn footer(mut self, footer: CreateEmbedFooter) -> Self { + self.footer = Some(footer); self } /// Set the image associated with the embed. This only supports HTTP(S). #[inline] - pub fn image(&mut self, url: impl Into) -> &mut Self { + pub fn image(mut self, url: impl Into) -> Self { self.image = Some(HoldsUrl::new(url.into())); self } /// Set the thumbnail of the embed. This only supports HTTP(S). #[inline] - pub fn thumbnail(&mut self, url: impl Into) -> &mut Self { + pub fn thumbnail(mut self, url: impl Into) -> Self { self.thumbnail = Some(HoldsUrl::new(url.into())); self } @@ -211,8 +186,7 @@ impl CreateEmbed { /// # use serenity::builder::CreateEmbed; /// # use serenity::model::Timestamp; /// let timestamp: Timestamp = "2004-06-08T16:04:23Z".parse().expect("Invalid timestamp!"); - /// let mut embed = CreateEmbed::default(); - /// embed.title("hello").timestamp(timestamp); + /// let embed = CreateEmbed::default().title("hello").timestamp(timestamp); /// ``` /// /// Creating a join-log: @@ -222,6 +196,7 @@ impl CreateEmbed { /// ```rust,no_run /// # #[cfg(all(feature = "cache", feature = "client"))] /// # async fn run() -> Result<(), Box> { + /// use serenity::builder::{CreateEmbed, CreateEmbedAuthor, CreateMessage}; /// use serenity::model::guild::Member; /// use serenity::model::id::GuildId; /// use serenity::prelude::*; @@ -238,19 +213,15 @@ impl CreateEmbed { /// let channel_search = channels.values().find(|c| c.name == "join-log"); /// /// if let Some(channel) = channel_search { - /// let user = &member.user; - /// - /// let _ = channel - /// .send_message(&context, |m| { - /// m.embed(|e| { - /// if let Some(joined_at) = member.joined_at { - /// e.timestamp(joined_at); - /// } - /// e.author(|a| a.icon_url(user.face()).name(&user.name)) - /// .title("Member Join") - /// }) - /// }) - /// .await; + /// let author = CreateEmbedAuthor::default() + /// .icon_url(member.user.face()) + /// .name(member.user.name); + /// let mut embed = CreateEmbed::default().title("Member Join").author(author); + /// if let Some(joined_at) = member.joined_at { + /// embed = embed.timestamp(joined_at); + /// } + /// let builder = CreateMessage::default().embed(embed); + /// let _ = channel.send_message(&context, builder).await; /// } /// } /// } @@ -264,21 +235,21 @@ impl CreateEmbed { /// # } /// ``` #[inline] - pub fn timestamp>(&mut self, timestamp: T) -> &mut Self { + pub fn timestamp>(mut self, timestamp: T) -> Self { self.timestamp = Some(timestamp.into()); self } /// Set the title of the embed. #[inline] - pub fn title(&mut self, title: impl Into) -> &mut Self { + pub fn title(mut self, title: impl Into) -> Self { self.title = Some(title.into()); self } /// Set the URL to direct to when clicking on the title. #[inline] - pub fn url(&mut self, url: impl Into) -> &mut Self { + pub fn url(mut self, url: impl Into) -> Self { self.url = Some(url.into()); self } @@ -290,13 +261,50 @@ impl CreateEmbed { /// /// [`ChannelId::send_files`]: crate::model::id::ChannelId::send_files #[inline] - pub fn attachment(&mut self, filename: impl Into) -> &mut Self { + pub fn attachment(mut self, filename: impl Into) -> Self { let mut filename = filename.into(); filename.insert_str(0, "attachment://"); self.image = Some(HoldsUrl::new(filename)); self } + + #[cfg(feature = "http")] + pub(super) fn check_length(&self) -> Result<()> { + let mut length = 0; + if let Some(ref author) = self.author { + if let Some(ref name) = author.name { + length += name.chars().count(); + } + } + + if let Some(ref description) = self.description { + length += description.chars().count(); + } + + for field in &self.fields { + length += field.name.chars().count(); + length += field.value.chars().count(); + } + + if let Some(ref footer) = self.footer { + if let Some(ref text) = footer.text { + length += text.chars().count(); + } + } + + if let Some(ref title) = self.title { + length += title.chars().count(); + } + + let max_length = crate::constants::EMBED_MAX_LENGTH; + if length > max_length { + let overflow = length - max_length; + return Err(Error::Model(ModelError::EmbedTooLarge(overflow))); + } + + Ok(()) + } } impl Default for CreateEmbed { @@ -323,76 +331,48 @@ impl From for CreateEmbed { /// /// Some values - such as Proxy URLs - are not preserved. fn from(embed: Embed) -> Self { - let mut b = CreateEmbed::default(); + let mut b = CreateEmbed { + description: embed.description, + timestamp: embed.timestamp, + title: embed.title, + url: embed.url, + ..Default::default() + }; if let Some(colour) = embed.colour { - b.colour(colour); + b = b.colour(colour); } - if let Some(author) = embed.author { - b.author(move |a| { - a.name(author.name); - - if let Some(icon_url) = author.icon_url { - a.icon_url(icon_url); - } - - if let Some(url) = author.url { - a.url(url); - } - - a - }); - } - - if let Some(description) = embed.description { - b.description(description); - } - - for field in embed.fields { - b.field(field.name, field.value, field.inline); + if let Some(thumbnail) = embed.thumbnail { + b = b.thumbnail(thumbnail.url); } if let Some(image) = embed.image { - b.image(image.url); - } - - if let Some(timestamp) = embed.timestamp { - b.timestamp(timestamp); - } - - if let Some(thumbnail) = embed.thumbnail { - b.thumbnail(thumbnail.url); + b = b.image(image.url); } - if let Some(url) = embed.url { - b.url(url); + if let Some(author) = embed.author { + b = b.author(author.into()); } - if let Some(title) = embed.title { - b.title(title); + if let Some(footer) = embed.footer { + b = b.footer(footer.into()); } - if let Some(footer) = embed.footer { - b.footer(move |f| { - if let Some(icon_url) = footer.icon_url { - f.icon_url(icon_url); - } - f.text(footer.text) - }); + for field in embed.fields { + b = b.field(field.name, field.value, field.inline); } b } } -/// A builder to create a fake [`Embed`] object's author, for use with the -/// [`CreateEmbed::author`] method. +/// A builder to create a fake [`Embed`] object's author, for use with the [`CreateEmbed::author`] +/// method. /// /// Requires that you specify a [`Self::name`]. -/// -/// [`Embed`]: crate::model::channel::Embed #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateEmbedAuthor { #[serde(skip_serializing_if = "Option::is_none")] icon_url: Option, @@ -404,31 +384,40 @@ pub struct CreateEmbedAuthor { impl CreateEmbedAuthor { /// Set the URL of the author's icon. - pub fn icon_url(&mut self, icon_url: impl Into) -> &mut Self { + pub fn icon_url(mut self, icon_url: impl Into) -> Self { self.icon_url = Some(icon_url.into()); self } /// Set the author's name. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } /// Set the author's URL. - pub fn url(&mut self, url: impl Into) -> &mut Self { + pub fn url(mut self, url: impl Into) -> Self { self.url = Some(url.into()); self } } -/// A builder to create a fake [`Embed`] object's footer, for use with the -/// [`CreateEmbed::footer`] method. -/// -/// This does not require any field be set. +impl From for CreateEmbedAuthor { + fn from(author: EmbedAuthor) -> Self { + Self { + icon_url: author.icon_url, + name: Some(author.name), + url: author.url, + } + } +} + +/// A builder to create a fake [`Embed`] object's footer, for use with the [`CreateEmbed::footer`] +/// method. /// -/// [`Embed`]: crate::model::channel::Embed +/// This does not have any required fields. #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateEmbedFooter { #[serde(skip_serializing_if = "Option::is_none")] icon_url: Option, @@ -438,18 +427,27 @@ pub struct CreateEmbedFooter { impl CreateEmbedFooter { /// Set the icon URL's value. This only supports HTTP(S). - pub fn icon_url(&mut self, icon_url: impl Into) -> &mut Self { + pub fn icon_url(mut self, icon_url: impl Into) -> Self { self.icon_url = Some(icon_url.into()); self } /// Set the footer's text. - pub fn text(&mut self, text: impl Into) -> &mut Self { + pub fn text(mut self, text: impl Into) -> Self { self.text = Some(text.into()); self } } +impl From for CreateEmbedFooter { + fn from(footer: EmbedFooter) -> Self { + Self { + icon_url: footer.icon_url, + text: Some(footer.text), + } + } +} + #[cfg(test)] mod test { use super::CreateEmbed; @@ -500,12 +498,12 @@ mod test { }), }; - let mut builder = CreateEmbed::from(embed); - builder.colour(0xFF0011); - builder.description("This is a hakase description"); - builder.image("https://i.imgur.com/XfWpfCV.gif"); - builder.title("still a hakase"); - builder.url("https://i.imgur.com/XfWpfCV.gif"); + let builder = CreateEmbed::from(embed) + .colour(0xFF0011) + .description("This is a hakase description") + .image("https://i.imgur.com/XfWpfCV.gif") + .title("still a hakase") + .url("https://i.imgur.com/XfWpfCV.gif"); let built = to_value(builder).unwrap(); diff --git a/src/builder/create_interaction_response.rs b/src/builder/create_interaction_response.rs index 2105d16f05b..a9dfea957b4 100644 --- a/src/builder/create_interaction_response.rs +++ b/src/builder/create_interaction_response.rs @@ -1,35 +1,80 @@ use super::{CreateAllowedMentions, CreateComponents, CreateEmbed}; -use crate::json::prelude::*; -use crate::model::application::interaction::{InteractionResponseType, MessageFlags}; -use crate::model::channel::AttachmentType; +#[cfg(feature = "http")] +use crate::http::Http; +use crate::internal::prelude::*; +use crate::model::application::interaction::InteractionResponseType; +use crate::model::prelude::*; #[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateInteractionResponse<'a> { #[serde(rename = "type")] kind: InteractionResponseType, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) data: Option>, + data: Option>, } impl<'a> CreateInteractionResponse<'a> { + /// Creates a response to the interaction received. + /// + /// **Note**: Message contents must be under 2000 unicode code points, and embeds must be under + /// 6000 code points. + /// + /// # Errors + /// + /// Returns an [`Error::Model`] if the message content is too long. May also return an + /// [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is an error in + /// deserializing the API response. + #[cfg(feature = "http")] + pub async fn execute( + mut self, + http: impl AsRef, + interaction_id: InteractionId, + token: &str, + ) -> Result<()> { + self.check_length()?; + let files = self.data.as_mut().map_or_else(Vec::new, |d| std::mem::take(&mut d.files)); + + if files.is_empty() { + http.as_ref().create_interaction_response(interaction_id.into(), token, &self).await + } else { + http.as_ref() + .create_interaction_response_with_files(interaction_id.into(), token, &self, files) + .await + } + } + + #[cfg(feature = "http")] + fn check_length(&self) -> Result<()> { + if let Some(data) = &self.data { + if let Some(content) = &data.content { + let length = content.chars().count(); + let max_length = crate::constants::MESSAGE_CODE_LIMIT; + if length > max_length { + return Err(Error::Model(ModelError::MessageTooLong(length - max_length))); + } + } + + if data.embeds.len() > crate::constants::EMBED_MAX_COUNT { + return Err(Error::Model(ModelError::EmbedAmount)); + } + for embed in &data.embeds { + embed.check_length()?; + } + } + Ok(()) + } + /// Sets the InteractionResponseType of the message. /// /// Defaults to `ChannelMessageWithSource`. - pub fn kind(&mut self, kind: InteractionResponseType) -> &mut Self { + pub fn kind(mut self, kind: InteractionResponseType) -> Self { self.kind = kind; self } - /// Sets the `InteractionApplicationCommandCallbackData` for the message. - pub fn interaction_response_data(&mut self, f: F) -> &mut Self - where - for<'b> F: FnOnce( - &'b mut CreateInteractionResponseData<'a>, - ) -> &'b mut CreateInteractionResponseData<'a>, - { - let mut data = CreateInteractionResponseData::default(); - f(&mut data); - + /// Sets the data for the message. See [`CreateInteractionResponseData`] for details on fields. + pub fn interaction_response_data(mut self, data: CreateInteractionResponseData<'a>) -> Self { self.data = Some(data); self } @@ -45,6 +90,7 @@ impl<'a> Default for CreateInteractionResponse<'a> { } #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateInteractionResponseData<'a> { embeds: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -63,7 +109,7 @@ pub struct CreateInteractionResponseData<'a> { title: Option, #[serde(skip)] - pub(crate) files: Vec>, + files: Vec>, } impl<'a> CreateInteractionResponseData<'a> { @@ -72,34 +118,34 @@ impl<'a> CreateInteractionResponseData<'a> { /// Think carefully before setting this to `true`. /// /// Defaults to `false`. - pub fn tts(&mut self, tts: bool) -> &mut Self { + pub fn tts(mut self, tts: bool) -> Self { self.tts = Some(tts); self } /// Appends a file to the message. - pub fn add_file>>(&mut self, file: T) -> &mut Self { + pub fn add_file>>(mut self, file: T) -> Self { self.files.push(file.into()); self } /// Appends a list of files to the message. pub fn add_files>, It: IntoIterator>( - &mut self, + mut self, files: It, - ) -> &mut Self { + ) -> Self { self.files.extend(files.into_iter().map(Into::into)); self } /// Sets a list of files to include in the message. /// - /// Calling this multiple times will overwrite the file list. - /// To append files, call [`Self::add_file`] or [`Self::add_files`] instead. + /// Calling this multiple times will overwrite the file list. To append files, call + /// [`Self::add_file`] or [`Self::add_files`] instead. pub fn files>, It: IntoIterator>( - &mut self, + mut self, files: It, - ) -> &mut Self { + ) -> Self { self.files = files.into_iter().map(Into::into).collect(); self } @@ -108,108 +154,80 @@ impl<'a> CreateInteractionResponseData<'a> { /// /// **Note**: Message contents must be under 2000 unicode code points. #[inline] - pub fn content(&mut self, content: impl Into) -> &mut Self { + pub fn content(mut self, content: impl Into) -> Self { self.content = Some(content.into()); self } - /// Create an embed for the message. - pub fn embed(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, - { - let mut embed = CreateEmbed::default(); - f(&mut embed); - self.add_embed(embed) - } - /// Adds an embed to the message. - pub fn add_embed(&mut self, embed: CreateEmbed) -> &mut Self { + pub fn add_embed(mut self, embed: CreateEmbed) -> Self { self.embeds.push(embed); self } /// Adds multiple embeds for the message. - pub fn add_embeds(&mut self, embeds: Vec) -> &mut Self { + pub fn add_embeds(mut self, embeds: Vec) -> Self { self.embeds.extend(embeds); self } /// Sets a single embed to include in the message /// - /// Calling this will overwrite the embed list. - /// To append embeds, call [`Self::add_embed`] instead. - pub fn set_embed(&mut self, embed: CreateEmbed) -> &mut Self { - self.set_embeds(vec![embed]); - self + /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embed`] + /// instead. + pub fn embed(self, embed: CreateEmbed) -> Self { + self.embeds(vec![embed]) } /// Sets a list of embeds to include in the message. /// - /// Calling this multiple times will overwrite the embed list. - /// To append embeds, call [`Self::add_embed`] instead. - pub fn set_embeds(&mut self, embeds: Vec) -> &mut Self { - self.embeds = embeds.into_iter().collect(); + /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embeds`] + /// instead. + pub fn embeds(mut self, embeds: Vec) -> Self { + self.embeds = embeds; self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, - { - let mut allowed_mentions = CreateAllowedMentions::default(); - f(&mut allowed_mentions); - + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { self.allowed_mentions = Some(allowed_mentions); self } /// Sets the flags for the message. - pub fn flags(&mut self, flags: MessageFlags) -> &mut Self { + pub fn flags(mut self, flags: MessageFlags) -> Self { self.flags = Some(flags); self } - /// Adds or removes the ephemeral flag - pub fn ephemeral(&mut self, ephemeral: bool) -> &mut Self { - let flags = self.flags.unwrap_or_else(MessageFlags::empty); + /// Adds or removes the ephemeral flag. + pub fn ephemeral(mut self, ephemeral: bool) -> Self { + let mut flags = self.flags.unwrap_or_else(MessageFlags::empty); - let flags = if ephemeral { - flags | MessageFlags::EPHEMERAL + if ephemeral { + flags |= MessageFlags::EPHEMERAL; } else { - flags & !MessageFlags::EPHEMERAL + flags &= !MessageFlags::EPHEMERAL; }; self.flags = Some(flags); self } - /// Creates components for this message. - pub fn components(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, - { - let mut components = CreateComponents::default(); - f(&mut components); - - self.set_components(components) - } - /// Sets the components of this message. - pub fn set_components(&mut self, components: CreateComponents) -> &mut Self { + pub fn components(mut self, components: CreateComponents) -> Self { self.components = Some(components); self } - /// Sets the custom id for modal interactions - pub fn custom_id(&mut self, id: impl Into) -> &mut Self { + /// Sets the custom id for modal interactions. + pub fn custom_id(mut self, id: impl Into) -> Self { self.custom_id = Some(id.into()); self } - /// Sets the title for modal interactions - pub fn title(&mut self, title: impl Into) -> &mut Self { + /// Sets the title for modal interactions. + pub fn title(mut self, title: impl Into) -> Self { self.title = Some(title.into()); self } @@ -222,26 +240,49 @@ pub struct AutocompleteChoice { pub value: Value, } -#[derive(Clone, Debug, Default, Serialize)] +#[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateAutocompleteResponse { + data: CreateAutocompleteResponseData, + #[serde(rename = "type")] + kind: InteractionResponseType, +} + +#[derive(Clone, Debug, Default, Serialize)] +struct CreateAutocompleteResponseData { choices: Vec, } impl CreateAutocompleteResponse { + /// Creates a response to an autocomplete interaction. + /// + /// # Errors + /// + /// Returns an [`Error::Http`] if the API returns an error. + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + interaction_id: InteractionId, + token: &str, + ) -> Result<()> { + http.as_ref().create_interaction_response(interaction_id.into(), token, &self).await + } + /// For autocomplete responses this sets their autocomplete suggestions. /// /// See the official docs on [`Application Command Option Choices`] for more information. /// /// [`Application Command Option Choices`]: https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-choice-structure - pub fn set_choices(&mut self, choices: Vec) -> &mut Self { - self.choices = choices; + pub fn set_choices(mut self, choices: Vec) -> Self { + self.data.choices = choices; self } /// Add an int autocomplete choice. /// /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 characters. Value must be between -2^53 and 2^53. - pub fn add_int_choice(&mut self, name: impl Into, value: i64) -> &mut Self { + pub fn add_int_choice(self, name: impl Into, value: i64) -> Self { self.add_choice(AutocompleteChoice { name: name.into(), value: Value::from(value), @@ -251,11 +292,7 @@ impl CreateAutocompleteResponse { /// Adds a string autocomplete choice. /// /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 characters. Value must be up to 100 characters. - pub fn add_string_choice( - &mut self, - name: impl Into, - value: impl Into, - ) -> &mut Self { + pub fn add_string_choice(self, name: impl Into, value: impl Into) -> Self { self.add_choice(AutocompleteChoice { name: name.into(), value: Value::String(value.into()), @@ -265,15 +302,24 @@ impl CreateAutocompleteResponse { /// Adds a number autocomplete choice. /// /// **Note**: There can be no more than 25 choices set. Name must be between 1 and 100 characters. Value must be between -2^53 and 2^53. - pub fn add_number_choice(&mut self, name: impl Into, value: f64) -> &mut Self { + pub fn add_number_choice(self, name: impl Into, value: f64) -> Self { self.add_choice(AutocompleteChoice { name: name.into(), value: Value::from(value), }) } - fn add_choice(&mut self, value: AutocompleteChoice) -> &mut Self { - self.choices.push(value); + fn add_choice(mut self, value: AutocompleteChoice) -> Self { + self.data.choices.push(value); self } } + +impl Default for CreateAutocompleteResponse { + fn default() -> Self { + Self { + data: CreateAutocompleteResponseData::default(), + kind: InteractionResponseType::Autocomplete, + } + } +} diff --git a/src/builder/create_interaction_response_followup.rs b/src/builder/create_interaction_response_followup.rs index 30b086b1c7b..e1749ffb8f2 100644 --- a/src/builder/create_interaction_response_followup.rs +++ b/src/builder/create_interaction_response_followup.rs @@ -1,12 +1,12 @@ -#[cfg(not(feature = "model"))] -use std::marker::PhantomData; - use super::{CreateAllowedMentions, CreateComponents, CreateEmbed}; -use crate::model::application::interaction::MessageFlags; -#[cfg(feature = "model")] -use crate::model::channel::AttachmentType; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateInteractionResponseFollowup<'a> { embeds: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -25,32 +25,90 @@ pub struct CreateInteractionResponseFollowup<'a> { components: Option, #[serde(skip)] - #[cfg(feature = "model")] - pub(crate) files: Vec>, - #[cfg(not(feature = "model"))] - pub(crate) files: PhantomData<&'a ()>, + files: Vec>, } impl<'a> CreateInteractionResponseFollowup<'a> { + /// Creates or edits a followup response to the response sent. If `message_id` is not `None`, + /// then the corresponding message will be edited. Otherwise, a new message will be created. + /// + /// **Note**: Message contents must be under 2000 unicode code points, and embeds must be under + /// 6000 code points. + /// + /// # Errors + /// + /// Returns [`Error::Model`] if the content is too long. May also return [`Error::Http`] if the + /// API returns an error, or [`Error::Json`] if there is an error in deserializing the + /// response. + #[cfg(feature = "http")] + pub async fn execute( + mut self, + http: impl AsRef, + message_id: Option, + token: &str, + ) -> Result { + self.check_length()?; + let files = std::mem::take(&mut self.files); + + match message_id { + Some(id) => { + if files.is_empty() { + http.as_ref().edit_followup_message(token, id.into(), &self).await + } else { + http.as_ref() + .edit_followup_message_and_attachments(token, id.into(), &self, files) + .await + } + }, + None => { + if files.is_empty() { + http.as_ref().create_followup_message(token, &self).await + } else { + http.as_ref().create_followup_message_with_files(token, &self, files).await + } + }, + } + } + + #[cfg(feature = "http")] + fn check_length(&self) -> Result<()> { + if let Some(content) = &self.content { + let length = content.chars().count(); + let max_length = crate::constants::MESSAGE_CODE_LIMIT; + if length > max_length { + let overflow = length - max_length; + return Err(Error::Model(ModelError::MessageTooLong(overflow))); + } + } + + if self.embeds.len() > crate::constants::EMBED_MAX_COUNT { + return Err(Error::Model(ModelError::EmbedAmount)); + } + for embed in &self.embeds { + embed.check_length()?; + } + Ok(()) + } + /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. #[inline] - pub fn content(&mut self, content: impl Into) -> &mut Self { + pub fn content(mut self, content: impl Into) -> Self { self.content = Some(content.into()); self } /// Override the default username of the webhook #[inline] - pub fn username(&mut self, username: impl Into) -> &mut Self { + pub fn username(mut self, username: impl Into) -> Self { self.username = Some(username.into()); self } /// Override the default avatar of the webhook #[inline] - pub fn avatar(&mut self, avatar_url: impl Into) -> &mut Self { + pub fn avatar(mut self, avatar_url: impl Into) -> Self { self.avatar_url = Some(avatar_url.into()); self } @@ -60,24 +118,21 @@ impl<'a> CreateInteractionResponseFollowup<'a> { /// Think carefully before setting this to `true`. /// /// Defaults to `false`. - pub fn tts(&mut self, tts: bool) -> &mut Self { + pub fn tts(mut self, tts: bool) -> Self { self.tts = Some(tts); self } /// Appends a file to the message. - #[cfg(feature = "model")] - pub fn add_file>>(&mut self, file: T) -> &mut Self { - self.add_files(vec![file]); - self + pub fn add_file>>(self, file: T) -> Self { + self.add_files(vec![file]) } /// Appends a list of files to the message. - #[cfg(feature = "model")] pub fn add_files>, It: IntoIterator>( - &mut self, + mut self, files: It, - ) -> &mut Self { + ) -> Self { self.files.extend(files.into_iter().map(Into::into)); self } @@ -86,100 +141,71 @@ impl<'a> CreateInteractionResponseFollowup<'a> { /// /// Calling this multiple times will overwrite the file list. /// To append files, call [`Self::add_file`] or [`Self::add_files`] instead. - #[cfg(feature = "model")] pub fn files>, It: IntoIterator>( - &mut self, + mut self, files: It, - ) -> &mut Self { + ) -> Self { self.files = files.into_iter().map(Into::into).collect(); self } - /// Create an embed for the message. - pub fn embed(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, - { - let mut embed = CreateEmbed::default(); - f(&mut embed); - self.add_embed(embed) - } - /// Adds an embed to the message. - pub fn add_embed(&mut self, embed: CreateEmbed) -> &mut Self { + pub fn add_embed(mut self, embed: CreateEmbed) -> Self { self.embeds.push(embed); self } /// Adds multiple embeds to the message. - pub fn add_embeds(&mut self, embeds: Vec) -> &mut Self { + pub fn add_embeds(mut self, embeds: Vec) -> Self { self.embeds.extend(embeds); self } - /// Sets a single embed to include in the message + /// Sets a single embed to include in the message. /// - /// Calling this will overwrite the embed list. - /// To append embeds, call [`Self::add_embed`] instead. - pub fn set_embed(&mut self, embed: CreateEmbed) -> &mut Self { - self.set_embeds(vec![embed]); - self + /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embed`] + /// instead. + pub fn embed(self, embed: CreateEmbed) -> Self { + self.embeds(vec![embed]) } /// Sets a list of embeds to include in the message. /// - /// Calling this multiple times will overwrite the embed list. - /// To append embeds, call [`Self::add_embed`] instead. - pub fn set_embeds(&mut self, embeds: impl IntoIterator) -> &mut Self { - self.embeds = embeds.into_iter().collect(); + /// Calling this multiple times will overwrite the embed list. To append embeds, call + /// [`Self::add_embeds`] instead. + pub fn embeds(mut self, embeds: Vec) -> Self { + self.embeds = embeds; self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, - { - let mut allowed_mentions = CreateAllowedMentions::default(); - f(&mut allowed_mentions); - + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { self.allowed_mentions = Some(allowed_mentions); self } /// Sets the flags for the response. - pub fn flags(&mut self, flags: MessageFlags) -> &mut Self { + pub fn flags(mut self, flags: MessageFlags) -> Self { self.flags = Some(flags); self } /// Adds or removes the ephemeral flag - pub fn ephemeral(&mut self, ephemeral: bool) -> &mut Self { - let flags = self.flags.unwrap_or_else(MessageFlags::empty); + pub fn ephemeral(mut self, ephemeral: bool) -> Self { + let mut flags = self.flags.unwrap_or_else(MessageFlags::empty); - let flags = if ephemeral { - flags | MessageFlags::EPHEMERAL + if ephemeral { + flags |= MessageFlags::EPHEMERAL; } else { - flags & !MessageFlags::EPHEMERAL + flags &= !MessageFlags::EPHEMERAL; }; self.flags = Some(flags); self } - /// Creates components for this message. - pub fn components(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, - { - let mut components = CreateComponents::default(); - f(&mut components); - - self.set_components(components) - } - /// Sets the components of this message. - pub fn set_components(&mut self, components: CreateComponents) -> &mut Self { + pub fn components(mut self, components: CreateComponents) -> Self { self.components = Some(components); self } diff --git a/src/builder/create_invite.rs b/src/builder/create_invite.rs index 382d538d6d0..f66f1922fd2 100644 --- a/src/builder/create_invite.rs +++ b/src/builder/create_invite.rs @@ -1,10 +1,12 @@ -use crate::model::id::{ApplicationId, UserId}; -use crate::model::invite::InviteTargetType; +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; /// A builder to create a [`RichInvite`] for use via [`GuildChannel::create_invite`]. /// -/// This is a structured and cleaner way of creating an invite, as all -/// parameters are optional. +/// This is a structured and cleaner way of creating an invite, as all parameters are optional. /// /// # Examples /// @@ -16,6 +18,7 @@ use crate::model::invite::InviteTargetType; /// # use serenity::prelude::*; /// # use serenity::model::prelude::*; /// # use serenity::model::channel::Channel; +/// use serenity::builder::CreateInvite; /// /// struct Handler; /// @@ -32,8 +35,8 @@ use crate::model::invite::InviteTargetType; /// }, /// }; /// -/// let creation = -/// channel.create_invite(&context, |i| i.max_age(3600).max_uses(10)).await; +/// let builder = CreateInvite::default().max_age(3600).max_uses(10); +/// let creation = channel.create_invite(&context, builder).await; /// /// let invite = match creation { /// Ok(invite) => invite, @@ -62,10 +65,8 @@ use crate::model::invite::InviteTargetType; /// # Ok(()) /// # } /// ``` -/// -/// [`GuildChannel::create_invite`]: crate::model::channel::GuildChannel::create_invite -/// [`RichInvite`]: crate::model::invite::RichInvite #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateInvite { #[serde(skip_serializing_if = "Option::is_none")] max_age: Option, @@ -84,6 +85,44 @@ pub struct CreateInvite { } impl CreateInvite { + /// Creates an invite for the given channel. + /// + /// **Note**: Requires the [Create Instant Invite] permission. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// + /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE + #[cfg(feature = "http")] + #[inline] + pub async fn execute( + self, + cache_http: impl CacheHttp, + channel_id: ChannelId, + #[cfg(feature = "cache")] guild_id: Option, + ) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + crate::utils::user_has_perms_cache( + cache, + channel_id, + guild_id, + Permissions::CREATE_INSTANT_INVITE, + )?; + } + } + + self._execute(cache_http.http(), channel_id).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http, channel_id: ChannelId) -> Result { + http.create_invite(channel_id.into(), &self, None).await + } + /// The duration that the invite will be valid for. /// /// Set to `0` for an invite which does not expire after an amount of time. @@ -100,17 +139,19 @@ impl CreateInvite { /// # #[cfg(feature = "framework")] /// # use serenity::framework::standard::{CommandResult, macros::command}; /// # use serenity::model::id::ChannelId; + /// # use serenity::builder::CreateInvite; /// # /// # #[cfg(all(feature = "cache", feature = "client", feature = "framework", feature = "http"))] /// # #[command] /// # async fn example(context: &Context) -> CommandResult { /// # let channel = context.cache.guild_channel(81384788765712384).unwrap().clone(); /// # - /// let invite = channel.create_invite(context, |i| i.max_age(3600)).await?; + /// let builder = CreateInvite::default().max_age(3600); + /// let invite = channel.create_invite(context, builder).await?; /// # Ok(()) /// # } /// ``` - pub fn max_age(&mut self, max_age: u64) -> &mut Self { + pub fn max_age(mut self, max_age: u64) -> Self { self.max_age = Some(max_age); self } @@ -131,17 +172,19 @@ impl CreateInvite { /// # #[cfg(feature = "framework")] /// # use serenity::framework::standard::{CommandResult, macros::command}; /// # use serenity::model::id::ChannelId; + /// # use serenity::builder::CreateInvite; /// # /// # #[cfg(all(feature = "cache", feature = "client", feature = "framework", feature = "http"))] /// # #[command] /// # async fn example(context: &Context) -> CommandResult { /// # let channel = context.cache.guild_channel(81384788765712384).unwrap().clone(); /// # - /// let invite = channel.create_invite(context, |i| i.max_uses(5)).await?; + /// let builder = CreateInvite::default().max_uses(5); + /// let invite = channel.create_invite(context, builder).await?; /// # Ok(()) /// # } /// ``` - pub fn max_uses(&mut self, max_uses: u64) -> &mut Self { + pub fn max_uses(mut self, max_uses: u64) -> Self { self.max_uses = Some(max_uses); self } @@ -160,19 +203,21 @@ impl CreateInvite { /// # #[cfg(feature = "framework")] /// # use serenity::framework::standard::{CommandResult, macros::command}; /// # use serenity::model::id::ChannelId; + /// # use serenity::builder::CreateInvite; /// # /// # #[cfg(all(feature = "cache", feature = "client", feature = "framework", feature = "http"))] /// # #[command] /// # async fn example(context: &Context) -> CommandResult { /// # let channel = context.cache.guild_channel(81384788765712384).unwrap().clone(); /// # - /// let invite = channel.create_invite(context, |i| i.temporary(true)).await?; + /// let builder = CreateInvite::default().temporary(true); + /// let invite = channel.create_invite(context, builder).await?; /// # Ok(()) /// # } /// # /// # fn main() {} /// ``` - pub fn temporary(&mut self, temporary: bool) -> &mut Self { + pub fn temporary(mut self, temporary: bool) -> Self { self.temporary = Some(temporary); self } @@ -191,23 +236,25 @@ impl CreateInvite { /// # #[cfg(feature = "framework")] /// # use serenity::framework::standard::{CommandResult, macros::command}; /// # use serenity::model::id::ChannelId; + /// # use serenity::builder::CreateInvite; /// # /// # #[cfg(all(feature = "cache", feature = "client", feature = "framework", feature = "http"))] /// # #[command] /// # async fn example(context: &Context) -> CommandResult { /// # let channel = context.cache.guild_channel(81384788765712384).unwrap().clone(); /// # - /// let invite = channel.create_invite(context, |i| i.unique(true)).await?; + /// let builder = CreateInvite::default().unique(true); + /// let invite = channel.create_invite(context, builder).await?; /// # Ok(()) /// # } /// ``` - pub fn unique(&mut self, unique: bool) -> &mut Self { + pub fn unique(mut self, unique: bool) -> Self { self.unique = Some(unique); self } /// The type of target for this voice channel invite. - pub fn target_type(&mut self, target_type: InviteTargetType) -> &mut Self { + pub fn target_type(mut self, target_type: InviteTargetType) -> Self { self.target_type = Some(target_type); self } @@ -215,7 +262,7 @@ impl CreateInvite { /// The ID of the user whose stream to display for this invite, required if `target_type` is /// `Stream` /// The user must be streaming in the channel. - pub fn target_user_id(&mut self, target_user_id: UserId) -> &mut Self { + pub fn target_user_id(mut self, target_user_id: UserId) -> Self { self.target_user_id = Some(target_user_id); self } @@ -234,7 +281,7 @@ impl CreateInvite { /// fishing: `814288819477020702` /// poker: `755827207812677713` /// chess: `832012774040141894` - pub fn target_application_id(&mut self, target_application_id: ApplicationId) -> &mut Self { + pub fn target_application_id(mut self, target_application_id: ApplicationId) -> Self { self.target_application_id = Some(target_application_id); self } diff --git a/src/builder/create_message.rs b/src/builder/create_message.rs index 28872913120..0d4f3807fcb 100644 --- a/src/builder/create_message.rs +++ b/src/builder/create_message.rs @@ -1,52 +1,52 @@ -#[cfg(not(feature = "model"))] -use std::marker::PhantomData; +use super::{CreateAllowedMentions, CreateComponents, CreateEmbed}; +#[cfg(feature = "http")] +use crate::constants; +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; -use super::{CreateAllowedMentions, CreateEmbed}; -use crate::builder::CreateComponents; -#[cfg(feature = "model")] -use crate::model::channel::AttachmentType; -use crate::model::channel::{MessageFlags, MessageReference, ReactionType}; -use crate::model::id::StickerId; - -/// A builder to specify the contents of an [`Http::send_message`] request, -/// primarily meant for use through [`ChannelId::send_message`]. +/// A builder to specify the contents of an [`Http::send_message`] request, primarily meant for use +/// through [`ChannelId::send_message`]. /// /// There are three situations where different field requirements are present: /// -/// 1. When sending a message without embeds or stickers, [`Self::content`] is -/// the only required field that is required to be set. +/// 1. When sending a message without embeds or stickers, [`Self::content`] is the only required +/// field that is required to be set. /// 2. When sending an [`Self::embed`], no other field is required. -/// 3. When sending stickers with [`Self::sticker_id`] or other sticker methods, -/// no other field is required. +/// 3. When sending stickers with [`Self::sticker_id`] or other sticker methods, no other field is +/// required. /// -/// Note that if you only need to send the content of a message, without -/// specifying other fields, then [`ChannelId::say`] may be a more preferable -/// option. +/// Note that if you only need to send the content of a message, without specifying other fields, +/// then [`ChannelId::say`] may be a more preferable option. /// /// # Examples /// /// Sending a message with a content of `"test"` and applying text-to-speech: /// /// ```rust,no_run +/// use serenity::builder::{CreateEmbed, CreateMessage}; /// use serenity::model::id::ChannelId; /// # use serenity::http::Http; /// # use std::sync::Arc; /// # +/// # async fn run() { /// # let http = Arc::new(Http::new("token")); /// /// let channel_id = ChannelId::new(7); /// -/// let _ = channel_id.send_message(&http, |m| { -/// m.content("test") -/// .tts(true) -/// .embed(|e| e.title("This is an embed").description("With a description")) -/// }); +/// let embed = CreateEmbed::default().title("This is an embed").description("With a description"); +/// let builder = CreateMessage::default().content("test").tts(true).embed(embed); +/// let _ = channel_id.send_message(&http, builder).await; +/// # } /// ``` /// /// [`ChannelId::say`]: crate::model::id::ChannelId::say /// [`ChannelId::send_message`]: crate::model::id::ChannelId::send_message /// [`Http::send_message`]: crate::http::client::Http::send_message #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateMessage<'a> { tts: bool, embeds: Vec, @@ -62,86 +62,135 @@ pub struct CreateMessage<'a> { #[serde(skip_serializing_if = "Option::is_none")] flags: Option, - // Following fields are not sent to discord, and are - // instead handled seperately. + // The following fields are handled seperately. #[serde(skip)] - #[cfg(feature = "model")] - pub(crate) files: Vec>, - #[cfg(not(feature = "model"))] - pub(crate) files: PhantomData<&'a ()>, - + files: Vec>, #[serde(skip)] - pub(crate) reactions: Vec, + reactions: Vec, } impl<'a> CreateMessage<'a> { + /// Send a message to the channel. + /// + /// **Note**: Requires the [Send Messages] permission. Additionally, attaching files requires the + /// [Attach Files] permission. + /// + /// **Note**: Message contents must be under 2000 unicode code points, and embeds must be under + /// 6000 code points. + /// + /// # Errors + /// + /// Returns a [`ModelError::MessageTooLong`] if the message contents are over the above limits. + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// + /// [Send Messages]: Permissions::SEND_MESSAGES + /// [Attach Files]: Permissions::ATTACH_FILES + #[cfg(feature = "http")] + #[inline] + pub async fn execute( + self, + cache_http: impl CacheHttp, + channel_id: ChannelId, + #[cfg(feature = "cache")] guild_id: Option, + ) -> Result { + #[cfg(feature = "cache")] + { + let mut req = Permissions::SEND_MESSAGES; + if !self.files.is_empty() { + req |= Permissions::ATTACH_FILES; + } + if let Some(cache) = cache_http.cache() { + crate::utils::user_has_perms_cache(cache, channel_id, guild_id, req)?; + } + } + + self._execute(cache_http.http(), channel_id).await + } + + #[cfg(feature = "http")] + async fn _execute(mut self, http: &Http, channel_id: ChannelId) -> Result { + self.check_length()?; + let files = std::mem::take(&mut self.files); + + let message = if files.is_empty() { + http.send_message(channel_id.into(), &self).await? + } else { + http.send_files(channel_id.into(), files, &self).await? + }; + + for reaction in self.reactions { + channel_id.create_reaction(&http, message.id, reaction).await?; + } + + Ok(message) + } + + #[cfg(feature = "http")] + fn check_length(&self) -> Result<()> { + if let Some(content) = &self.content { + let length = content.chars().count(); + let max_length = constants::MESSAGE_CODE_LIMIT; + if length > max_length { + return Err(Error::Model(ModelError::MessageTooLong(length - max_length))); + } + } + + if self.embeds.len() > constants::EMBED_MAX_COUNT { + return Err(Error::Model(ModelError::EmbedAmount)); + } + for embed in &self.embeds { + embed.check_length()?; + } + + if self.sticker_ids.len() > constants::STICKER_MAX_COUNT { + return Err(Error::Model(ModelError::StickerAmount)); + } + + Ok(()) + } + /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. #[inline] - pub fn content(&mut self, content: impl Into) -> &mut Self { + pub fn content(mut self, content: impl Into) -> Self { self.content = Some(content.into()); self } - fn _add_embed(&mut self, embed: CreateEmbed) -> &mut Self { - self.embeds.push(embed); - self - } - /// Add an embed for the message. /// - /// **Note**: This will keep all existing embeds. Use [`Self::set_embed()`] to replace existing + /// **Note**: This will keep all existing embeds. Use [`Self::embed()`] to replace existing /// embeds. - pub fn add_embed(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, - { - let mut embed = CreateEmbed::default(); - f(&mut embed); - self._add_embed(embed) + pub fn add_embed(mut self, embed: CreateEmbed) -> Self { + self.embeds.push(embed); + self } /// Add multiple embeds for the message. /// - /// **Note**: This will keep all existing embeds. Use [`Self::set_embeds()`] to replace existing + /// **Note**: This will keep all existing embeds. Use [`Self::embeds()`] to replace existing /// embeds. - pub fn add_embeds(&mut self, embeds: Vec) -> &mut Self { + pub fn add_embeds(mut self, embeds: Vec) -> Self { self.embeds.extend(embeds); self } /// Set an embed for the message. /// - /// Equivalent to [`Self::set_embed()`]. - /// - /// **Note**: This will replace all existing embeds. Use - /// [`Self::add_embed()`] to add an additional embed. - pub fn embed(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, - { - let mut embed = CreateEmbed::default(); - f(&mut embed); - - self.set_embed(embed) - } - - /// Set an embed for the message. - /// - /// Equivalent to [`Self::embed()`]. - /// - /// **Note**: This will replace all existing embeds. - /// Use [`Self::add_embed()`] to add an additional embed. - pub fn set_embed(&mut self, embed: CreateEmbed) -> &mut Self { - self.set_embeds(vec![embed]) + /// **Note**: This will replace all existing embeds. Use [`Self::add_embed()`] to keep existing + /// embeds. + pub fn embed(self, embed: CreateEmbed) -> Self { + self.embeds(vec![embed]) } /// Set multiple embeds for the message. /// /// **Note**: This will replace all existing embeds. Use [`Self::add_embeds()`] to keep existing /// embeds. - pub fn set_embeds(&mut self, embeds: Vec) -> &mut Self { + pub fn embeds(mut self, embeds: Vec) -> Self { self.embeds = embeds; self } @@ -151,7 +200,7 @@ impl<'a> CreateMessage<'a> { /// Think carefully before setting this to `true`. /// /// Defaults to `false`. - pub fn tts(&mut self, tts: bool) -> &mut Self { + pub fn tts(mut self, tts: bool) -> Self { self.tts = tts; self } @@ -159,133 +208,123 @@ impl<'a> CreateMessage<'a> { /// Adds a list of reactions to create after the message's sent. #[inline] pub fn reactions, It: IntoIterator>( - &mut self, + mut self, reactions: It, - ) -> &mut Self { + ) -> Self { self.reactions = reactions.into_iter().map(Into::into).collect(); self } /// Appends a file to the message. - #[cfg(feature = "model")] - pub fn add_file>>(&mut self, file: T) -> &mut Self { + /// + /// **Note**: Requres the [Attach Files] permission. + /// + /// [Attach Files]: Permissions::ATTACH_FILES + pub fn add_file>>(mut self, file: T) -> Self { self.files.push(file.into()); self } /// Appends a list of files to the message. - #[cfg(feature = "model")] + /// + /// **Note**: Requres the [Attach Files] permission. + /// + /// [Attach Files]: Permissions::ATTACH_FILES pub fn add_files>, It: IntoIterator>( - &mut self, + mut self, files: It, - ) -> &mut Self { + ) -> Self { self.files.extend(files.into_iter().map(Into::into)); self } /// Sets a list of files to include in the message. /// - /// Calling this multiple times will overwrite the file list. - /// To append files, call [`Self::add_file`] or [`Self::add_files`] instead. - #[cfg(feature = "model")] + /// Calling this multiple times will overwrite the file list. To append files, call + /// [`Self::add_file`] or [`Self::add_files`] instead. + /// + /// **Note**: Requres the [Attach Files] permission. + /// + /// [Attach Files]: Permissions::ATTACH_FILES pub fn files>, It: IntoIterator>( - &mut self, + mut self, files: It, - ) -> &mut Self { + ) -> Self { self.files = files.into_iter().map(Into::into).collect(); self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, - { - let mut allowed_mentions = CreateAllowedMentions::default(); - f(&mut allowed_mentions); - + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { self.allowed_mentions = Some(allowed_mentions); self } /// Set the reference message this message is a reply to. #[allow(clippy::unwrap_used)] // allowing unwrap here because serializing MessageReference should never error - pub fn reference_message(&mut self, reference: impl Into) -> &mut Self { + pub fn reference_message(mut self, reference: impl Into) -> Self { self.message_reference = Some(reference.into()); self } - /// Creates components for this message. - pub fn components(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, - { - let mut components = CreateComponents::default(); - f(&mut components); - - self.set_components(components) - } - /// Sets the components of this message. - pub fn set_components(&mut self, components: CreateComponents) -> &mut Self { + pub fn components(mut self, components: CreateComponents) -> Self { self.components = Some(components); self } /// Sets the flags for the message. - pub fn flags(&mut self, flags: MessageFlags) -> &mut Self { + pub fn flags(mut self, flags: MessageFlags) -> Self { self.flags = Some(flags); self } /// Sets a single sticker ID to include in the message. /// - /// **Note**: This will replace all existing stickers. Use - /// [`Self::add_sticker_id()`] to add an additional sticker. - pub fn sticker_id(&mut self, sticker_id: impl Into) -> &mut Self { - self.set_sticker_ids(vec![sticker_id.into()]) + /// **Note**: This will replace all existing stickers. Use [`Self::add_sticker_id()`] to keep + /// existing stickers. + pub fn sticker_id(self, sticker_id: impl Into) -> Self { + self.sticker_ids(vec![sticker_id.into()]) } - /// Add a sticker ID for the message. + /// Sets a list of sticker IDs to include in the message. /// /// **Note**: There can be a maximum of 3 stickers in a message. /// - /// **Note**: This will keep all existing stickers. Use - /// [`Self::set_sticker_ids()`] to replace existing stickers. - pub fn add_sticker_id(&mut self, sticker_id: impl Into) -> &mut Self { - self.sticker_ids.push(sticker_id.into()); + /// **Note**: This will replace all existing stickers. Use [`Self::add_sticker_id()`] or + /// [`Self::add_sticker_ids()`] to keep existing stickers. + pub fn sticker_ids, It: IntoIterator>( + mut self, + sticker_ids: It, + ) -> Self { + self.sticker_ids = sticker_ids.into_iter().map(Into::into).collect(); self } - /// Add multiple sticker IDs for the message. + /// Add a sticker ID for the message. /// /// **Note**: There can be a maximum of 3 stickers in a message. /// - /// **Note**: This will keep all existing stickers. Use - /// [`Self::set_sticker_ids()`] to replace existing stickers. - pub fn add_sticker_ids, It: IntoIterator>( - &mut self, - sticker_ids: It, - ) -> &mut Self { - for sticker_id in sticker_ids { - self.add_sticker_id(sticker_id); - } - + /// **Note**: This will keep all existing stickers. Use [`Self::sticker_id()`] to replace + /// existing sticker. + pub fn add_sticker_id(mut self, sticker_id: impl Into) -> Self { + self.sticker_ids.push(sticker_id.into()); self } - /// Sets a list of sticker IDs to include in the message. + /// Add multiple sticker IDs for the message. /// /// **Note**: There can be a maximum of 3 stickers in a message. /// - /// **Note**: This will replace all existing stickers. Use - /// [`Self::add_sticker_id()`] or [`Self::add_sticker_ids()`] to keep + /// **Note**: This will keep all existing stickers. Use [`Self::sticker_ids()`] to replace /// existing stickers. - pub fn set_sticker_ids, It: IntoIterator>( - &mut self, + pub fn add_sticker_ids, It: IntoIterator>( + mut self, sticker_ids: It, - ) -> &mut Self { - self.sticker_ids = sticker_ids.into_iter().map(Into::into).collect(); + ) -> Self { + for sticker_id in sticker_ids { + self = self.add_sticker_id(sticker_id); + } self } } diff --git a/src/builder/create_scheduled_event.rs b/src/builder/create_scheduled_event.rs index 74ae08e7842..067bc92e38e 100644 --- a/src/builder/create_scheduled_event.rs +++ b/src/builder/create_scheduled_event.rs @@ -1,16 +1,13 @@ -#[cfg(feature = "model")] -use crate::http::Http; -#[cfg(feature = "model")] +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] use crate::internal::prelude::*; -#[cfg(feature = "model")] -use crate::model::channel::AttachmentType; -use crate::model::guild::{ScheduledEventMetadata, ScheduledEventType}; -use crate::model::id::ChannelId; -use crate::model::Timestamp; -#[cfg(feature = "model")] +use crate::model::prelude::*; +#[cfg(feature = "http")] use crate::utils::encode_image; #[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateScheduledEvent { #[serde(skip_serializing_if = "Option::is_none")] channel_id: Option, @@ -33,63 +30,90 @@ pub struct CreateScheduledEvent { } impl CreateScheduledEvent { - /// Sets the channel id of the scheduled event. Required if the [`kind`] of the event is - /// [`StageInstance`] or [`Voice`]. + /// Creates a new scheduled event in the guild with the data set, if any. /// - /// [`kind`]: CreateScheduledEvent::kind - /// [`StageInstance`]: ScheduledEventType::StageInstance - /// [`Voice`]: ScheduledEventType::Voice - pub fn channel_id>(&mut self, channel_id: C) -> &mut Self { + /// **Note**: Requires the [Manage Events] permission. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// + /// [Manage Events]: Permissions::MANAGE_EVENTS + #[cfg(feature = "http")] + #[inline] + pub async fn execute( + self, + cache_http: impl CacheHttp, + guild_id: GuildId, + ) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(guild) = cache.guild(guild_id) { + let req = Permissions::MANAGE_EVENTS; + + if !guild.has_perms(&cache_http, req).await { + return Err(Error::Model(ModelError::InvalidPermissions(req))); + } + } + } + } + + self._execute(cache_http.http(), guild_id).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http, guild_id: GuildId) -> Result { + http.create_scheduled_event(guild_id.into(), &self, None).await + } + + /// Sets the channel id of the scheduled event. Required if [`Self::kind`] is + /// [`ScheduledEventType::StageInstance`] or [`ScheduledEventType::Voice`]. + pub fn channel_id>(mut self, channel_id: C) -> Self { self.channel_id = Some(channel_id.into()); self } /// Sets the name of the scheduled event. Required to be set for event creation. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } /// Sets the description of the scheduled event. - pub fn description(&mut self, description: impl Into) -> &mut Self { + pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); self } /// Sets the start time of the scheduled event. Required to be set for event creation. - #[inline] - pub fn start_time>(&mut self, timestamp: T) -> &mut Self { + pub fn start_time>(mut self, timestamp: T) -> Self { self.scheduled_start_time = Some(timestamp.into().to_string()); self } - /// Sets the end time of the scheduled event. Required if the [`kind`] of the event is - /// [`External`]. - /// - /// [`kind`]: CreateScheduledEvent::kind - /// [`External`]: ScheduledEventType::External - #[inline] - pub fn end_time>(&mut self, timestamp: T) -> &mut Self { + /// Sets the end time of the scheduled event. Required if [`Self::kind`] is + /// [`ScheduledEventType::External`]. + pub fn end_time>(mut self, timestamp: T) -> Self { self.scheduled_end_time = Some(timestamp.into().to_string()); self } /// Sets the entity type of the scheduled event. Required to be set for event creation. - pub fn kind(&mut self, kind: ScheduledEventType) -> &mut Self { + pub fn kind(mut self, kind: ScheduledEventType) -> Self { self.entity_type = Some(kind); self } - /// Sets the location of the scheduled event. Required to be set and non-empty if the - /// [`kind`] of the event is [`External`]. + /// Sets the location of the scheduled event. Required to be set and non-empty if + /// [`Self::kind`] is [`ScheduledEventType::External`]. /// - /// [`kind`]: CreateScheduledEvent::kind /// [`External`]: ScheduledEventType::External - pub fn location(&mut self, location: impl Into) -> &mut Self { + pub fn location(mut self, location: impl Into) -> Self { self.entity_metadata = Some(ScheduledEventMetadata { location: location.into(), }); - self } @@ -97,18 +121,26 @@ impl CreateScheduledEvent { /// /// # Errors /// - /// May error if the icon is a URL and the HTTP request fails, or if the image is a file - /// on a path that doesn't exist. - #[cfg(feature = "model")] + /// May error if the input is a URL and the HTTP request fails, or if it is a path to a file + /// that does not exist. + #[cfg(feature = "http")] pub async fn image<'a>( - &mut self, + mut self, http: impl AsRef, image: impl Into>, - ) -> Result<&mut Self> { + ) -> Result { let image_data = image.into().data(&http.as_ref().client).await?; self.image = Some(encode_image(&image_data)); Ok(self) } + + /// Sets the cover image for the scheduled event. Requires the input be a base64-encoded image + /// that is in either JPG, GIF, or PNG format. + #[cfg(not(feature = "http"))] + pub fn image(mut self, image: String) -> Self { + self.image = Some(image); + self + } } impl Default for CreateScheduledEvent { diff --git a/src/builder/create_stage_instance.rs b/src/builder/create_stage_instance.rs index 9fd362b4c23..b2a85bff5ff 100644 --- a/src/builder/create_stage_instance.rs +++ b/src/builder/create_stage_instance.rs @@ -1,9 +1,12 @@ -use crate::model::id::ChannelId; +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; -/// Creates a [`StageInstance`]. -/// -/// [`StageInstance`]: crate::model::channel::StageInstance +/// Builder for creating a [`StageInstance`]. #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateStageInstance { #[serde(skip_serializing_if = "Option::is_none")] channel_id: Option, @@ -12,14 +15,45 @@ pub struct CreateStageInstance { } impl CreateStageInstance { - // Sets the stage channel id of the stage channel instance. - pub fn channel_id(&mut self, id: impl Into) -> &mut Self { + /// Creates the stage instance. + /// + /// # Errors + /// + /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. + /// + /// Returns [`Error::Http`] if there is already a stage instance currently. + #[cfg(feature = "http")] + #[inline] + pub async fn execute(self, cache_http: impl CacheHttp) -> Result { + #[cfg(feature = "cache")] + { + if let Some(channel_id) = self.channel_id { + if let Some(cache) = cache_http.cache() { + if let Some(channel) = cache.guild_channel(channel_id) { + if channel.kind != ChannelType::Stage { + return Err(Error::Model(ModelError::InvalidChannelType)); + } + } + } + } + } + + self._execute(cache_http.http()).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http) -> Result { + http.create_stage_instance(&self).await + } + + /// Sets the stage channel id of the stage channel instance. + pub fn channel_id(mut self, id: impl Into) -> Self { self.channel_id = Some(id.into()); self } /// Sets the topic of the stage channel instance. - pub fn topic(&mut self, topic: impl Into) -> &mut Self { + pub fn topic(mut self, topic: impl Into) -> Self { self.topic = Some(topic.into()); self } diff --git a/src/builder/create_sticker.rs b/src/builder/create_sticker.rs index 3fa099efd9e..7aaedc30d39 100644 --- a/src/builder/create_sticker.rs +++ b/src/builder/create_sticker.rs @@ -1,35 +1,78 @@ -use std::borrow::Cow; +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; -use crate::model::channel::AttachmentType; - -type Field = (Cow<'static, str>, Cow<'static, str>); - -/// A builder to create or edit a [`Sticker`] for use via a number of model methods. +/// A builder to create a [`Sticker`] for use via a number of model methods. /// /// These are: /// /// - [`PartialGuild::create_sticker`] /// - [`Guild::create_sticker`] /// - [`GuildId::create_sticker`] -/// -/// [`Sticker`]: crate::model::sticker::Sticker -/// [`PartialGuild::create_sticker`]: crate::model::guild::PartialGuild::create_sticker -/// [`Guild::create_sticker`]: crate::model::guild::Guild::create_sticker -/// [`GuildId::create_sticker`]: crate::model::id::GuildId::create_sticker #[derive(Clone, Debug, Default)] +#[must_use] pub struct CreateSticker<'a> { name: Option, tags: Option, description: Option, - pub(crate) file: Option>, + file: Option>, } impl<'a> CreateSticker<'a> { + /// Creates a new sticker in the guild with the data set, if any. + /// + /// **Note**: Requires the [Manage Emojis and Stickers] permission. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// + /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS + #[cfg(feature = "http")] + #[inline] + pub async fn execute(self, cache_http: impl CacheHttp, guild_id: GuildId) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(guild) = cache.guild(guild_id) { + let req = Permissions::MANAGE_EMOJIS_AND_STICKERS; + + if !guild.has_perms(&cache_http, req).await { + return Err(Error::Model(ModelError::InvalidPermissions(req))); + } + } + } + } + + self._execute(cache_http.http(), guild_id).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http, guild_id: GuildId) -> Result { + let file = self.file.ok_or(Error::Model(ModelError::NoStickerFileSet))?; + + let mut map = Vec::with_capacity(3); + if let Some(name) = self.name { + map.push(("name".to_string(), name)); + } + if let Some(tags) = self.tags { + map.push(("tags".to_string(), tags)); + } + if let Some(description) = self.description { + map.push(("description".to_string(), description)); + } + + http.create_sticker(guild_id.into(), map, file, None).await + } + /// The name of the sticker to set. /// /// **Note**: Must be between 2 and 30 characters long. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } @@ -37,7 +80,7 @@ impl<'a> CreateSticker<'a> { /// The description of the sticker. /// /// **Note**: If not empty, must be between 2 and 100 characters long. - pub fn description(&mut self, description: impl Into) -> &mut Self { + pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); self } @@ -45,7 +88,7 @@ impl<'a> CreateSticker<'a> { /// The Discord name of a unicode emoji representing the sticker's expression. /// /// **Note**: Must be between 2 and 200 characters long. - pub fn tags(&mut self, tags: impl Into) -> &mut Self { + pub fn tags(mut self, tags: impl Into) -> Self { self.tags = Some(tags.into()); self } @@ -53,28 +96,8 @@ impl<'a> CreateSticker<'a> { /// The sticker file. /// /// **Note**: Must be a PNG, APNG, or Lottie JSON file, max 500 KB. - pub fn file>>(&mut self, file: T) -> &mut Self { + pub fn file>>(mut self, file: T) -> Self { self.file = Some(file.into()); self } - - #[must_use] - pub fn build(self) -> Option<(Vec, AttachmentType<'a>)> { - let file = self.file?; - let mut buf = Vec::with_capacity(3); - - if let Some(name) = self.name { - buf.push(("name".into(), name.into())); - } - - if let Some(description) = self.description { - buf.push(("description".into(), description.into())); - } - - if let Some(tags) = self.tags { - buf.push(("tags".into(), tags.into())); - } - - Some((buf, file)) - } } diff --git a/src/builder/create_thread.rs b/src/builder/create_thread.rs index 8c53a085f5f..6d936cc2b67 100644 --- a/src/builder/create_thread.rs +++ b/src/builder/create_thread.rs @@ -1,6 +1,11 @@ -use crate::model::channel::ChannelType; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateThread { #[serde(skip_serializing_if = "Option::is_none")] name: Option, @@ -15,10 +20,31 @@ pub struct CreateThread { } impl CreateThread { + /// Creates a thread, either private or public. Public threads require a message to connect the + /// thread to. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + channel_id: ChannelId, + message_id: Option, + ) -> Result { + match message_id { + Some(msg_id) => { + http.as_ref().create_public_thread(channel_id.into(), msg_id.into(), &self).await + }, + None => http.as_ref().create_private_thread(channel_id.into(), &self).await, + } + } + /// The name of the thread. /// /// **Note**: Must be between 2 and 100 characters long. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self @@ -27,7 +53,7 @@ impl CreateThread { /// Duration in minutes to automatically archive the thread after recent activity. /// /// **Note**: Can only be set to 60, 1440, 4320, 10080 currently. - pub fn auto_archive_duration(&mut self, duration: u16) -> &mut Self { + pub fn auto_archive_duration(mut self, duration: u16) -> Self { self.auto_archive_duration = Some(duration); self @@ -35,7 +61,8 @@ impl CreateThread { /// How many seconds must a user wait before sending another message. /// - /// Bots, or users with the [`MANAGE_MESSAGES`] and/or [`MANAGE_CHANNELS`] permissions are exempt + /// Bots, or users with the [`MANAGE_MESSAGES`] and/or [`MANAGE_CHANNELS`] permissions are + /// exempt from this restriction. /// from this restriction. /// /// **Note**: Must be between 0 and 21600 seconds (360 minutes or 6 hours). @@ -43,7 +70,7 @@ impl CreateThread { /// [`MANAGE_MESSAGES`]: crate::model::permissions::Permissions::MANAGE_MESSAGES /// [`MANAGE_CHANNELS`]: crate::model::permissions::Permissions::MANAGE_CHANNELS #[doc(alias = "slowmode")] - pub fn rate_limit_per_user(&mut self, seconds: u16) -> &mut Self { + pub fn rate_limit_per_user(mut self, seconds: u16) -> Self { self.rate_limit_per_user = Some(seconds); self @@ -55,7 +82,7 @@ impl CreateThread { /// when thread documentation was first published. This is a bit of a weird default though, /// and thus is highly likely to change in the future, so it is recommended to always /// explicitly setting it to avoid any breaking change. - pub fn kind(&mut self, kind: ChannelType) -> &mut Self { + pub fn kind(mut self, kind: ChannelType) -> Self { self.kind = Some(kind); self diff --git a/src/builder/create_webhook.rs b/src/builder/create_webhook.rs index 54b91ff0657..8f9ca2b561a 100644 --- a/src/builder/create_webhook.rs +++ b/src/builder/create_webhook.rs @@ -1,22 +1,97 @@ +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::prelude::*; + #[derive(Debug, Default, Clone, Serialize)] +#[must_use] pub struct CreateWebhook { #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) name: Option, + name: Option, #[serde(skip_serializing_if = "Option::is_none")] - avatar: Option>, + avatar: Option, } impl CreateWebhook { - /// Set default name of the Webhook. + /// Creates the webhook. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns [`ModelError::InvalidChannelType`] if the + /// corresponding channel is not of type [`Text`] or [`News`]. + /// + /// If the provided name is less than 2 characters, returns [`ModelError::NameTooShort`]. If it + /// is more than 100 characters, returns [`ModelError::NameTooLong`]. + /// + /// Returns a [`Error::Http`] if the current user lacks permission, or if invalid data is + /// given. + /// + /// [`Text`]: ChannelType::Text + /// [`News`]: ChannelType::News + #[cfg(feature = "http")] + pub async fn execute( + self, + cache_http: impl CacheHttp, + channel_id: ChannelId, + ) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(channel) = cache.guild_channel(channel_id) { + if !channel.is_text_based() { + return Err(Error::Model(ModelError::InvalidChannelType)); + } + } + } + } + + self._execute(cache_http.http(), channel_id).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http, channel_id: ChannelId) -> Result { + if let Some(name) = &self.name { + if name.len() < 2 { + return Err(Error::Model(ModelError::NameTooShort)); + } else if name.len() > 100 { + return Err(Error::Model(ModelError::NameTooLong)); + } + } + + http.create_webhook(channel_id.into(), &self, None).await + } + + /// Set the webhook's name. /// /// This must be between 1-80 characters. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } - /// Set default avatar of the webhook. - pub fn avatar(&mut self, avatar: Option) -> &mut Self { + /// Set the webhook's default avatar. + /// + /// # Errors + /// + /// May error if the input is a URL and the HTTP request fails, or if it is a path to a file + /// that does not exist. + #[cfg(feature = "http")] + pub async fn avatar<'a>( + mut self, + http: impl AsRef, + avatar: impl Into>, + ) -> Result { + let avatar_data = avatar.into().data(&http.as_ref().client).await?; + self.avatar = Some(crate::utils::encode_image(&avatar_data)); + Ok(self) + } + + #[cfg(not(feature = "http"))] + /// Set the webhook's default avatar. Requires the input be a base64-encoded image that is in + /// either JPG, GIF, or PNG format. + pub fn avatar(mut self, avatar: String) -> Self { self.avatar = Some(avatar); self } diff --git a/src/builder/edit_automod_rule.rs b/src/builder/edit_automod_rule.rs index 7fc4581d67b..d5ccdb64371 100644 --- a/src/builder/edit_automod_rule.rs +++ b/src/builder/edit_automod_rule.rs @@ -1,7 +1,19 @@ +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::guild::automod::Rule; use crate::model::guild::automod::{Action, EventType, Trigger}; -use crate::model::id::{ChannelId, RoleId}; +use crate::model::prelude::*; #[derive(Clone, Debug, Serialize)] +#[must_use] +/// A builder for creating or editing guild automoderation rules. +/// +/// # Examples +/// +/// See [`GuildId::create_automod_rule`] for details. pub struct EditAutoModRule { event_type: EventType, #[serde(skip_serializing_if = "Option::is_none")] @@ -19,14 +31,38 @@ pub struct EditAutoModRule { } impl EditAutoModRule { + /// Creates or edits an automoderation [`Rule`] in a guild. Passing `Some(rule_id)` will edit + /// that corresponding rule, otherwise a new rule will be created. + /// + /// **Note**: Requires the [Manage Guild] permission. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + /// + /// [Manage Guild]: Permissions::MANAGE_GUILD + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + guild_id: GuildId, + rule_id: Option, + ) -> Result { + let http = http.as_ref(); + match rule_id { + Some(rule_id) => http.edit_automod_rule(guild_id.into(), rule_id.into(), &self).await, + None => http.create_automod_rule(guild_id.into(), &self).await, + } + } + /// The display name of the rule. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } /// Set the event context the rule should be checked. - pub fn event_type(&mut self, event_type: EventType) -> &mut Self { + pub fn event_type(mut self, event_type: EventType) -> Self { self.event_type = event_type; self } @@ -34,19 +70,19 @@ impl EditAutoModRule { /// Set the type of content which can trigger the rule. /// /// **None**: The trigger type can't be edited after creation. Only its values. - pub fn trigger(&mut self, trigger: Trigger) -> &mut Self { + pub fn trigger(mut self, trigger: Trigger) -> Self { self.trigger = Some(trigger); self } /// Set the actions which will execute when the rule is triggered. - pub fn actions(&mut self, actions: Vec) -> &mut Self { + pub fn actions(mut self, actions: Vec) -> Self { self.actions = Some(actions); self } /// Set whether the rule is enabled. - pub fn enabled(&mut self, enabled: bool) -> &mut Self { + pub fn enabled(mut self, enabled: bool) -> Self { self.enabled = Some(enabled); self } @@ -54,10 +90,7 @@ impl EditAutoModRule { /// Set roles that should not be affected by the rule. /// /// Maximum of 20. - pub fn exempt_roles( - &mut self, - roles: impl IntoIterator>, - ) -> &mut Self { + pub fn exempt_roles(mut self, roles: impl IntoIterator>) -> Self { self.exempt_roles = Some(roles.into_iter().map(Into::into).collect()); self } @@ -66,9 +99,9 @@ impl EditAutoModRule { /// /// Maximum of 50. pub fn exempt_channels( - &mut self, + mut self, channels: impl IntoIterator>, - ) -> &mut Self { + ) -> Self { self.exempt_channels = Some(channels.into_iter().map(Into::into).collect()); self } diff --git a/src/builder/edit_channel.rs b/src/builder/edit_channel.rs index d84796f4c17..e633604aa90 100644 --- a/src/builder/edit_channel.rs +++ b/src/builder/edit_channel.rs @@ -1,31 +1,32 @@ -use crate::model::channel::{PermissionOverwrite, PermissionOverwriteData, VideoQualityMode}; -use crate::model::id::ChannelId; +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; -/// A builder to edit a [`GuildChannel`] for use via [`GuildChannel::edit`] -/// -/// Defaults are not directly provided by the builder itself. +/// A builder to edit a [`GuildChannel`] for use via [`GuildChannel::edit`]. /// /// # Examples /// /// Edit a channel, providing a new name and topic: /// /// ```rust,no_run -/// # use serenity::{http::Http, model::id::ChannelId}; +/// # use serenity::builder::EditChannel; +/// # use serenity::http::Http; +/// # use serenity::model::id::ChannelId; /// # /// # async fn run() -> Result<(), Box> { /// # let http = Http::new("token"); /// # let mut channel = ChannelId::new(1); -/// // assuming a channel has already been bound -/// if let Err(why) = channel.edit(&http, |c| c.name("new name").topic("a test topic")).await { +/// let builder = EditChannel::default().name("new name").topic("a test topic"); +/// if let Err(why) = channel.edit(&http, builder).await { /// // properly handle the error /// } /// # Ok(()) /// # } /// ``` -/// -/// [`GuildChannel`]: crate::model::channel::GuildChannel -/// [`GuildChannel::edit`]: crate::model::channel::GuildChannel::edit #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditChannel { #[serde(skip_serializing_if = "Option::is_none")] bitrate: Option, @@ -52,12 +53,59 @@ pub struct EditChannel { } impl EditChannel { + /// Edits the channel's settings. + /// + /// **Note**: Requires the [Manage Channels] permission. Modifying permissions via + /// [`Self::permissions`] also requires the [Manage Roles] permission. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// + /// [Manage Channels]: Permissions::MANAGE_CHANNELS + /// [Manage Roles]: Permissions::MANAGE_ROLES + #[cfg(feature = "http")] + pub async fn execute( + self, + cache_http: impl CacheHttp, + channel_id: ChannelId, + #[cfg(feature = "cache")] guild_id: Option, + ) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + crate::utils::user_has_perms_cache( + cache, + channel_id, + guild_id, + Permissions::MANAGE_CHANNELS, + )?; + if self.permission_overwrites.is_some() { + crate::utils::user_has_perms_cache( + cache, + channel_id, + guild_id, + Permissions::MANAGE_ROLES, + )?; + } + } + } + + self._execute(cache_http.http(), channel_id).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http, channel_id: ChannelId) -> Result { + http.edit_channel(channel_id.into(), &self, None).await + } + /// The bitrate of the channel in bits. /// /// This is for [voice] channels only. /// - /// [voice]: crate::model::channel::ChannelType::Voice - pub fn bitrate(&mut self, bitrate: u32) -> &mut Self { + /// [voice]: ChannelType::Voice + pub fn bitrate(mut self, bitrate: u32) -> Self { self.bitrate = Some(bitrate); self } @@ -66,19 +114,18 @@ impl EditChannel { /// /// This is for [voice] channels only. /// - /// [voice]: crate::model::channel::ChannelType::Voice - pub fn video_quality_mode(&mut self, quality: VideoQualityMode) -> &mut Self { + /// [voice]: ChannelType::Voice + pub fn video_quality_mode(mut self, quality: VideoQualityMode) -> Self { self.video_quality_mode = Some(quality); self } - /// The voice region of the channel. - /// It is automatic when `None`. + /// The voice region of the channel. It is automatic when `None`. /// /// This is for [voice] channels only. /// - /// [voice]: crate::model::channel::ChannelType::Voice - pub fn voice_region(&mut self, id: Option) -> &mut Self { + /// [voice]: ChannelType::Voice + pub fn voice_region(mut self, id: Option) -> Self { self.rtc_region = id; self } @@ -86,13 +133,13 @@ impl EditChannel { /// The name of the channel. /// /// Must be between 2 and 100 characters long. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } /// The position of the channel in the channel list. - pub fn position(&mut self, position: u32) -> &mut Self { + pub fn position(mut self, position: u32) -> Self { self.position = Some(position); self } @@ -103,8 +150,8 @@ impl EditChannel { /// /// This is for [text] channels only. /// - /// [text]: crate::model::channel::ChannelType::Text - pub fn topic(&mut self, topic: impl Into) -> &mut Self { + /// [text]: ChannelType::Text + pub fn topic(mut self, topic: impl Into) -> Self { self.topic = Some(topic.into()); self } @@ -113,8 +160,8 @@ impl EditChannel { /// /// This is for [text] channels only. /// - /// [text]: crate::model::channel::ChannelType::Text - pub fn nsfw(&mut self, nsfw: bool) -> &mut Self { + /// [text]: ChannelType::Text + pub fn nsfw(mut self, nsfw: bool) -> Self { self.nsfw = Some(nsfw); self } @@ -123,8 +170,8 @@ impl EditChannel { /// /// This is for [voice] channels only. /// - /// [voice]: crate::model::channel::ChannelType::Voice - pub fn user_limit(&mut self, user_limit: u32) -> &mut Self { + /// [voice]: ChannelType::Voice + pub fn user_limit(mut self, user_limit: u32) -> Self { self.user_limit = Some(user_limit); self } @@ -133,39 +180,39 @@ impl EditChannel { /// /// This is for [text] and [voice] channels only. /// - /// [text]: crate::model::channel::ChannelType::Text - /// [voice]: crate::model::channel::ChannelType::Voice + /// [text]: ChannelType::Text + /// [voice]: ChannelType::Voice #[inline] - pub fn category>>(&mut self, category: C) -> &mut Self { + pub fn category>>(mut self, category: C) -> Self { self.parent_id = category.into(); self } /// How many seconds must a user wait before sending another message. /// - /// Bots, or users with the [`MANAGE_MESSAGES`] and/or [`MANAGE_CHANNELS`] permissions are exempt - /// from this restriction. + /// Bots, or users with the [`MANAGE_MESSAGES`] and/or [`MANAGE_CHANNELS`] permissions are + /// exempt from this restriction. /// /// **Note**: Must be between 0 and 21600 seconds (360 minutes or 6 hours). /// - /// [`MANAGE_MESSAGES`]: crate::model::permissions::Permissions::MANAGE_MESSAGES - /// [`MANAGE_CHANNELS`]: crate::model::permissions::Permissions::MANAGE_CHANNELS + /// [`MANAGE_MESSAGES`]: Permissions::MANAGE_MESSAGES + /// [`MANAGE_CHANNELS`]: Permissions::MANAGE_CHANNELS #[doc(alias = "slowmode")] - pub fn rate_limit_per_user(&mut self, seconds: u64) -> &mut Self { + pub fn rate_limit_per_user(mut self, seconds: u64) -> Self { self.rate_limit_per_user = Some(seconds); - self } - /// A set of overwrites defining what a user or a user carrying a certain role can - /// and cannot do. + /// A set of overwrites defining what a user or a user carrying a certain role can or can't do. /// /// # Example /// /// Inheriting permissions from an existing channel: /// /// ```rust,no_run - /// # use serenity::{http::Http, model::id::ChannelId}; + /// # use serenity::builder::EditChannel; + /// # use serenity::http::Http; + /// # use serenity::model::id::ChannelId; /// # use std::sync::Arc; /// # /// # async fn run() -> Result<(), Box> { @@ -182,11 +229,12 @@ impl EditChannel { /// kind: PermissionOverwriteType::Member(UserId::new(1234)), /// }]; /// - /// channel.edit(http, |c| c.name("my_edited_cool_channel").permissions(permissions)).await?; + /// let builder = EditChannel::default().name("my_edited_cool_channel").permissions(permissions); + /// channel.edit(http, builder).await?; /// # Ok(()) /// # } /// ``` - pub fn permissions(&mut self, perms: I) -> &mut Self + pub fn permissions(mut self, perms: I) -> Self where I: IntoIterator, { diff --git a/src/builder/edit_guild.rs b/src/builder/edit_guild.rs index daa3c10e3b0..fd539d1ba5c 100644 --- a/src/builder/edit_guild.rs +++ b/src/builder/edit_guild.rs @@ -1,15 +1,12 @@ +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; use crate::model::prelude::*; -/// A builder to optionally edit certain fields of a [`Guild`]. This is meant -/// for usage with [`Guild::edit`]. -/// -/// **Note**: Editing a guild requires that the current user have the -/// [Manage Guild] permission. -/// -/// [`Guild::edit`]: crate::model::guild::Guild::edit -/// [`Guild`]: crate::model::guild::Guild -/// [Manage Guild]: crate::model::permissions::Permissions::MANAGE_GUILD +/// A builder to optionally edit certain fields of a [`Guild`]. #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditGuild { #[serde(skip_serializing_if = "Option::is_none")] afk_channel_id: Option>, @@ -50,21 +47,54 @@ pub struct EditGuild { } impl EditGuild { - /// Set the "AFK voice channel" that users are to move to if they have been - /// AFK for an amount of time, configurable by [`Self::afk_timeout`]. - /// - /// The given channel must be either some valid voice channel, or [`None`] to - /// not set an AFK channel. The library does not check if a channel is - /// valid. + /// Edits the given guild. + /// + /// **Note**: Requires the [Manage Guild] permission. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// + /// [Manage Guild]: Permissions::MANAGE_GUILD + #[cfg(feature = "http")] + pub async fn execute( + self, + cache_http: impl CacheHttp, + guild_id: GuildId, + ) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(guild) = cache.guild(guild_id) { + let req = Permissions::MANAGE_GUILD; + + if !guild.has_perms(&cache_http, req).await { + return Err(Error::Model(ModelError::InvalidPermissions(req))); + } + } + } + } + + self._execute(cache_http.http(), guild_id).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http, guild_id: GuildId) -> Result { + http.as_ref().edit_guild(guild_id.into(), &self, None).await + } + + /// Set the "AFK voice channel" that users are to move to if they have been AFK for an amount + /// of time, configurable by [`Self::afk_timeout`]. Pass [`None`] to unset the current value. #[inline] - pub fn afk_channel>(&mut self, channel: Option) -> &mut Self { - self.afk_channel_id = Some(channel.map(Into::into)); + pub fn afk_channel(mut self, channel: Option) -> Self { + self.afk_channel_id = Some(channel); self } - /// Set the amount of time a user is to be moved to the AFK channel - - /// configured via [`Self::afk_channel`] - after being AFK. - pub fn afk_timeout(&mut self, timeout: u64) -> &mut Self { + /// Set the amount of time a user is to be moved to the AFK channel - configured via + /// [`Self::afk_channel`] - after being AFK. + pub fn afk_timeout(mut self, timeout: u64) -> Self { self.afk_timeout = Some(timeout); self } @@ -73,58 +103,63 @@ impl EditGuild { /// /// # Examples /// - /// Using the utility function - [`utils::read_image`] - to read an image - /// from the cwd and encode it in base64 to send to Discord. + /// Using the utility function - [`utils::read_image`] - to read an image and encode it in + /// base64, to then set as the guild icon. /// /// ```rust,no_run + /// # use serenity::builder::EditGuild; /// # use serenity::{http::Http, model::id::GuildId}; /// # /// # async fn run() -> Result<(), Box> { /// # let http = Http::new("token"); /// # let mut guild = GuildId::new(1).to_partial_guild(&http).await?; - /// use serenity::utils; + /// let base64_icon = serenity::utils::read_image("./guild_icon.png")?; /// /// // assuming a `guild` has already been bound - /// - /// let base64_icon = utils::read_image("./guild_icon.png")?; - /// - /// guild.edit(&http, |g| g.icon(Some(base64_icon))).await?; + /// let builder = EditGuild::default().icon(Some(base64_icon)); + /// guild.edit(&http, builder).await?; /// # Ok(()) /// # } /// ``` /// /// [`utils::read_image`]: crate::utils::read_image - pub fn icon(&mut self, icon: Option) -> &mut Self { + pub fn icon(mut self, icon: Option) -> Self { self.icon = Some(icon); self } + /// Clear the current guild icon, resetting it to the default logo. + pub fn delete_icon(mut self) -> Self { + self.icon = Some(None); + self + } + /// Set the name of the guild. /// /// **Note**: Must be between (and including) 2-100 characters. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } /// Set the description of the guild. /// - /// **Note**: Requires that the guild have the `DISCOVERABLE` feature enabled. - /// You can check this through a guild's [`features`] list. + /// **Note**: Requires that the guild have the `DISCOVERABLE` feature enabled. You can check + /// this through a guild's [`features`] list. /// - /// [`features`]: crate::model::guild::Guild::features - pub fn description(&mut self, name: impl Into) -> &mut Self { + /// [`features`]: Guild::features + pub fn description(mut self, name: impl Into) -> Self { self.description = Some(name.into()); self } /// Set the features of the guild. /// - /// **Note**: Requires that the guild have the `DISCOVERABLE` feature enabled. - /// You can check this through a guild's [`features`] list. + /// **Note**: Requires that the guild have the `DISCOVERABLE` feature enabled. You can check + /// this through a guild's [`features`] list. /// - /// [`features`]: crate::model::guild::Guild::features - pub fn features(&mut self, features: Vec) -> &mut Self { + /// [`features`]: Guild::features + pub fn features(mut self, features: Vec) -> Self { self.features = Some(features); self } @@ -133,7 +168,7 @@ impl EditGuild { /// /// **Note**: The current user must be the owner of the guild. #[inline] - pub fn owner>(&mut self, user_id: U) -> &mut Self { + pub fn owner(mut self, user_id: impl Into) -> Self { self.owner_id = Some(user_id.into()); self } @@ -142,11 +177,11 @@ impl EditGuild { /// /// The `splash` must be base64-encoded 1024x1024 png/jpeg/gif image-data. /// - /// Requires that the guild have the `INVITE_SPLASH` feature enabled. - /// You can check this through a guild's [`features`] list. + /// Requires that the guild have the `INVITE_SPLASH` feature enabled. You can check this + /// through a guild's [`features`] list. /// - /// [`features`]: crate::model::guild::Guild::features - pub fn splash(&mut self, splash: Option) -> &mut Self { + /// [`features`]: Guild::features + pub fn splash(mut self, splash: Option) -> Self { self.splash = Some(splash); self } @@ -155,11 +190,11 @@ impl EditGuild { /// /// The `splash` must be base64-encoded 1024x1024 png/jpeg/gif image-data. /// - /// Requires that the guild have the `DISCOVERABLE` feature enabled. - /// You can check this through a guild's [`features`] list. + /// Requires that the guild have the `DISCOVERABLE` feature enabled. You can check this through + /// a guild's [`features`] list. /// - /// [`features`]: crate::model::guild::Guild::features - pub fn discovery_splash(&mut self, splash: Option) -> &mut Self { + /// [`features`]: Guild::features + pub fn discovery_splash(mut self, splash: Option) -> Self { self.discovery_splash = Some(splash); self } @@ -168,18 +203,17 @@ impl EditGuild { /// /// The `banner` must be base64-encoded 16:9 png/jpeg image data. /// - /// Requires that the guild have the `BANNER` feature enabled. - /// You can check this through a guild's [`features`] list. + /// Requires that the guild have the `BANNER` feature enabled. You can check this through a + /// guild's [`features`] list. /// - /// [`features`]: crate::model::guild::Guild::features - pub fn banner(&mut self, banner: Option) -> &mut Self { + /// [`features`]: Guild::features + pub fn banner(mut self, banner: Option) -> Self { self.banner = Some(banner); self } - /// Set the channel ID where welcome messages and boost events will be - /// posted. - pub fn system_channel_id(&mut self, channel_id: Option) -> &mut Self { + /// Set the channel ID where welcome messages and boost events will be posted. + pub fn system_channel_id(mut self, channel_id: Option) -> Self { self.system_channel_id = Some(channel_id); self } @@ -188,60 +222,57 @@ impl EditGuild { /// /// **Note**: /// This feature is for Community guilds only. - pub fn rules_channel_id(&mut self, channel_id: Option) -> &mut Self { + pub fn rules_channel_id(mut self, channel_id: Option) -> Self { self.rules_channel_id = Some(channel_id); self } - /// Set the channel ID where admins and moderators receive update messages - /// from Discord. + /// Set the channel ID where admins and moderators receive update messages from Discord. /// /// **Note**: /// This feature is for Community guilds only. - pub fn public_updates_channel_id(&mut self, channel_id: Option) -> &mut Self { + pub fn public_updates_channel_id(mut self, channel_id: Option) -> Self { self.public_updates_channel_id = Some(channel_id); self } - /// Set the preferred locale used in Server Discovery and update messages - /// from Discord. + /// Set the preferred locale used in Server Discovery and update messages from Discord. /// /// If this is not set, the locale will default to "en-US"; /// /// **Note**: /// This feature is for Community guilds only. - pub fn preferred_locale(&mut self, locale: Option) -> &mut Self { + pub fn preferred_locale(mut self, locale: Option) -> Self { self.preferred_locale = Some(locale); self } /// Set the content filter level. - pub fn explicit_content_filter(&mut self, level: Option) -> &mut Self { + pub fn explicit_content_filter(mut self, level: Option) -> Self { self.explicit_content_filter = Some(level); self } /// Set the default message notification level. pub fn default_message_notifications( - &mut self, + mut self, level: Option, - ) -> &mut Self { + ) -> Self { self.default_message_notifications = Some(level); self } - /// Set the verification level of the guild. This can restrict what a - /// user must have prior to being able to send messages in a guild. - /// - /// Refer to the documentation for [`VerificationLevel`] for more - /// information on each variant. + /// Set the verification level of the guild. This can restrict what a user must have prior to + /// being able to send messages in a guild. /// + /// Refer to the documentation for [`VerificationLevel`] for more information on each variant. /// /// # Examples /// /// Setting the verification level to [`High`][`VerificationLevel::High`]: /// /// ```rust,no_run + /// # use serenity::builder::EditGuild; /// # use serenity::{http::Http, model::id::GuildId}; /// # /// # async fn run() -> Result<(), Box> { @@ -249,10 +280,10 @@ impl EditGuild { /// # let mut guild = GuildId::new(1).to_partial_guild(&http).await?; /// use serenity::model::guild::VerificationLevel; /// - /// // assuming a `guild` has already been bound - /// - /// let edit = guild.edit(&http, |g| g.verification_level(VerificationLevel::High)).await; + /// let builder = EditGuild::default().verification_level(VerificationLevel::High); /// + /// // assuming a `guild` has already been bound + /// let edit = guild.edit(&http, builder).await; /// if let Err(why) = edit { /// println!("Error setting verification level: {:?}", why); /// } @@ -260,10 +291,7 @@ impl EditGuild { /// # } /// ``` #[inline] - pub fn verification_level(&mut self, verification_level: V) -> &mut Self - where - V: Into, - { + pub fn verification_level(mut self, verification_level: impl Into) -> Self { self.verification_level = Some(verification_level.into()); self } @@ -271,6 +299,7 @@ impl EditGuild { /// Modifies the notifications that are sent by discord to the configured system channel. /// /// ```rust,no_run + /// # use serenity::builder::EditGuild; /// # use serenity::{http::Http, model::id::GuildId}; /// # /// # async fn run() -> Result<(), Box> { @@ -278,24 +307,20 @@ impl EditGuild { /// # let mut guild = GuildId::new(1).to_partial_guild(&http).await?; /// use serenity::model::guild::SystemChannelFlags; /// - /// // assuming a `guild` has already been bound - /// - /// let edit = guild - /// .edit(&http, |g| { - /// g.system_channel_flags( - /// SystemChannelFlags::SUPPRESS_JOIN_NOTIFICATIONS - /// | SystemChannelFlags::SUPPRESS_GUILD_REMINDER_NOTIFICATIONS, - /// ) - /// }) - /// .await; + /// let builder = EditGuild::default().system_channel_flags( + /// SystemChannelFlags::SUPPRESS_JOIN_NOTIFICATIONS + /// | SystemChannelFlags::SUPPRESS_GUILD_REMINDER_NOTIFICATIONS, + /// ); /// + /// // assuming a `guild` has already been bound + /// let edit = guild.edit(&http, builder).await; /// if let Err(why) = edit { - /// println!("Error setting verification level: {:?}", why); + /// println!("Error setting system channel flags: {:?}", why); /// } /// # Ok(()) /// # } /// ``` - pub fn system_channel_flags(&mut self, system_channel_flags: SystemChannelFlags) -> &mut Self { + pub fn system_channel_flags(mut self, system_channel_flags: SystemChannelFlags) -> Self { self.system_channel_flags = Some(system_channel_flags); self } diff --git a/src/builder/edit_guild_welcome_screen.rs b/src/builder/edit_guild_welcome_screen.rs index 2b066dc743f..f7911beafc6 100644 --- a/src/builder/edit_guild_welcome_screen.rs +++ b/src/builder/edit_guild_welcome_screen.rs @@ -1,10 +1,14 @@ -use crate::model::guild::GuildWelcomeChannelEmoji; -use crate::model::id::{ChannelId, EmojiId}; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; /// A builder to specify the fields to edit in a [`GuildWelcomeScreen`]. /// /// [`GuildWelcomeScreen`]: crate::model::guild::GuildWelcomeScreen #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditGuildWelcomeScreen { #[serde(skip_serializing_if = "Option::is_none")] enabled: Option, @@ -15,36 +19,42 @@ pub struct EditGuildWelcomeScreen { } impl EditGuildWelcomeScreen { + /// Edits the guild's welcome screen. + /// + /// **Note**: Requires the [Manage Guild] permission. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Guild]: Permissions::MANAGE_GUILD + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + guild_id: GuildId, + ) -> Result { + http.as_ref().edit_guild_welcome_screen(guild_id.into(), &self).await + } + /// Whether the welcome screen is enabled or not. - pub fn enabled(&mut self, enabled: bool) -> &mut Self { + pub fn enabled(mut self, enabled: bool) -> Self { self.enabled = Some(enabled); - self } /// The server description shown in the welcome screen. - pub fn description(&mut self, description: impl Into) -> &mut Self { + pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); - self } - pub fn create_welcome_channel(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateGuildWelcomeChannel) -> &mut CreateGuildWelcomeChannel, - { - let mut data = CreateGuildWelcomeChannel::default(); - f(&mut data); - - self.add_welcome_channel(data) - } - - pub fn add_welcome_channel(&mut self, channel: CreateGuildWelcomeChannel) -> &mut Self { + pub fn add_welcome_channel(mut self, channel: CreateGuildWelcomeChannel) -> Self { self.welcome_channels.push(channel); self } - pub fn set_welcome_channels(&mut self, channels: Vec) -> &mut Self { + pub fn set_welcome_channels(mut self, channels: Vec) -> Self { self.welcome_channels = channels; self } @@ -67,21 +77,19 @@ pub struct CreateGuildWelcomeChannel { impl CreateGuildWelcomeChannel { /// The Id of the channel to show. It is required. - pub fn id(&mut self, id: impl Into) -> &mut Self { + pub fn id(mut self, id: impl Into) -> Self { self.channel_id = Some(id.into()); - self } /// The description shown for the channel. It is required. - pub fn description(&mut self, description: impl Into) -> &mut Self { + pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); - self } /// The emoji shown for the channel. - pub fn emoji(&mut self, emoji: GuildWelcomeChannelEmoji) -> &mut Self { + pub fn emoji(mut self, emoji: GuildWelcomeChannelEmoji) -> Self { match emoji { GuildWelcomeChannelEmoji::Unicode(name) => { self.emoji_name = Some(name); diff --git a/src/builder/edit_guild_widget.rs b/src/builder/edit_guild_widget.rs index 894030790a3..05770a8785a 100644 --- a/src/builder/edit_guild_widget.rs +++ b/src/builder/edit_guild_widget.rs @@ -1,28 +1,45 @@ -use crate::model::id::ChannelId; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; /// A builder to specify the fields to edit in a [`GuildWidget`]. /// /// [`GuildWidget`]: crate::model::guild::GuildWidget #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditGuildWidget { #[serde(skip_serializing_if = "Option::is_none")] - pub enabled: Option, + enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub channel_id: Option, + channel_id: Option, } impl EditGuildWidget { + /// Edits the guild's widget. + /// + /// **Note**: Requires the [Manage Guild] permission. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Guild]: Permissions::MANAGE_GUILD + #[cfg(feature = "http")] + pub async fn execute(self, http: impl AsRef, guild_id: GuildId) -> Result { + http.as_ref().edit_guild_widget(guild_id.into(), &self).await + } + /// Whether the widget is enabled or not. - pub fn enabled(&mut self, enabled: bool) -> &mut Self { + pub fn enabled(mut self, enabled: bool) -> Self { self.enabled = Some(enabled); - self } /// The server description shown in the welcome screen. - pub fn channel_id(&mut self, id: impl Into) -> &mut Self { + pub fn channel_id(mut self, id: impl Into) -> Self { self.channel_id = Some(id.into()); - self } } diff --git a/src/builder/edit_interaction_response.rs b/src/builder/edit_interaction_response.rs index d16a1482348..eaa89b0dec4 100644 --- a/src/builder/edit_interaction_response.rs +++ b/src/builder/edit_interaction_response.rs @@ -1,92 +1,112 @@ use super::{CreateAllowedMentions, CreateComponents, CreateEmbed}; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::prelude::*; #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditInteractionResponse { + embeds: Vec, #[serde(skip_serializing_if = "Option::is_none")] content: Option, #[serde(skip_serializing_if = "Option::is_none")] - embeds: Option>, - #[serde(skip_serializing_if = "Option::is_none")] allowed_mentions: Option, #[serde(skip_serializing_if = "Option::is_none")] components: Option, } impl EditInteractionResponse { + /// Edits the initial interaction response. Does not work for ephemeral messages. + /// + /// The `application_id` used will usually be the bot's [`UserId`], except if the bot is very + /// old. + /// + /// **Note**: Message contents must be under 2000 unicode code points, and embeds must be under + /// 6000 code points. + /// + /// # Errors + /// + /// Returns an [`Error::Model`] if the message content is too long. May also return an + /// [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is an error in + /// deserializing the API response. + #[cfg(feature = "http")] + pub async fn execute(self, http: impl AsRef, token: &str) -> Result { + self.check_length()?; + http.as_ref().edit_original_interaction_response(token, &self).await + } + + #[cfg(feature = "http")] + fn check_length(&self) -> Result<()> { + if let Some(content) = &self.content { + let length = content.chars().count(); + let max_length = crate::constants::MESSAGE_CODE_LIMIT; + if length > max_length { + return Err(Error::Model(ModelError::MessageTooLong(length - max_length))); + } + } + + if self.embeds.len() > crate::constants::EMBED_MAX_COUNT { + return Err(Error::Model(ModelError::EmbedAmount)); + } + for embed in &self.embeds { + embed.check_length()?; + } + Ok(()) + } + /// Sets the `InteractionApplicationCommandCallbackData` for the message. /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. #[inline] - pub fn content(&mut self, content: impl Into) -> &mut Self { + pub fn content(mut self, content: impl Into) -> Self { self.content = Some(content.into()); self } - /// Creates an embed for the message. - pub fn embed(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, - { - let mut embed = CreateEmbed::default(); - f(&mut embed); - self.add_embed(embed) - } - - fn embeds(&mut self) -> &mut Vec { - self.embeds.get_or_insert_with(Vec::new) - } - /// Adds an embed for the message. - pub fn add_embed(&mut self, embed: CreateEmbed) -> &mut Self { - self.embeds().push(embed); + pub fn add_embed(mut self, embed: CreateEmbed) -> Self { + self.embeds.push(embed); self } /// Adds multiple embeds to the message. - pub fn add_embeds(&mut self, embeds: Vec) -> &mut Self { - self.embeds().extend(embeds); + pub fn add_embeds(mut self, embeds: Vec) -> Self { + self.embeds.extend(embeds); self } /// Sets a single embed to include in the message /// - /// Calling this will overwrite the embed list. - /// To append embeds, call [`Self::add_embed`] instead. - pub fn set_embed(&mut self, embed: CreateEmbed) -> &mut Self { - self.set_embeds(vec![embed]); - self + /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embed`] + /// instead. + pub fn embed(self, embed: CreateEmbed) -> Self { + self.embeds(vec![embed]) } /// Sets the embeds for the message. /// /// **Note**: You can only have up to 10 embeds per message. - pub fn set_embeds(&mut self, embeds: Vec) -> &mut Self { - self.embeds = Some(embeds); + /// + /// Calling this will overwrite the embed list. To append embeds, call [`Self::add_embeds`] + /// instead. + pub fn embeds(mut self, embeds: Vec) -> Self { + self.embeds = embeds; self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, - { - let mut allowed_mentions = CreateAllowedMentions::default(); - f(&mut allowed_mentions); - + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { self.allowed_mentions = Some(allowed_mentions); self } /// Sets the components of this message. - pub fn components(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, - { - let mut components = CreateComponents::default(); - f(&mut components); - + pub fn components(mut self, components: CreateComponents) -> Self { self.components = Some(components); self } diff --git a/src/builder/edit_member.rs b/src/builder/edit_member.rs index 082e674f7d1..09b279da531 100644 --- a/src/builder/edit_member.rs +++ b/src/builder/edit_member.rs @@ -1,12 +1,13 @@ -use crate::model::id::{ChannelId, RoleId}; -use crate::model::Timestamp; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; -/// A builder which edits the properties of a [`Member`], to be used in -/// conjunction with [`Member::edit`]. -/// -/// [`Member`]: crate::model::guild::Member -/// [`Member::edit`]: crate::model::guild::Member::edit +/// A builder which edits the properties of a [`Member`], to be used in conjunction with +/// [`Member::edit`]. #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditMember { #[serde(skip_serializing_if = "Option::is_none")] deaf: Option, @@ -23,64 +24,80 @@ pub struct EditMember { } impl EditMember { + /// Edits the properties of the guild member. + /// + /// For details on permissions requirements, refer to each specific method. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + guild_id: GuildId, + user_id: UserId, + ) -> Result { + http.as_ref().edit_member(guild_id.into(), user_id.into(), &self, None).await + } + /// Whether to deafen the member. /// - /// Requires the [Deafen Members] permission. + /// **Note**: Requires the [Deafen Members] permission. /// - /// [Deafen Members]: crate::model::permissions::Permissions::DEAFEN_MEMBERS - pub fn deafen(&mut self, deafen: bool) -> &mut Self { + /// [Deafen Members]: Permissions::DEAFEN_MEMBERS + pub fn deafen(mut self, deafen: bool) -> Self { self.deaf = Some(deafen); self } /// Whether to mute the member. /// - /// Requires the [Mute Members] permission. + /// **Note**: Requires the [Mute Members] permission. /// - /// [Mute Members]: crate::model::permissions::Permissions::MUTE_MEMBERS - pub fn mute(&mut self, mute: bool) -> &mut Self { + /// [Mute Members]: Permissions::MUTE_MEMBERS + pub fn mute(mut self, mute: bool) -> Self { self.mute = Some(mute); self } - /// Changes the member's nickname. Pass an empty string to reset the - /// nickname. + /// Changes the member's nickname. Pass an empty string to reset the nickname. /// - /// Requires the [Manage Nicknames] permission. + /// **Note**: Requires the [Manage Nicknames] permission. /// - /// [Manage Nicknames]: crate::model::permissions::Permissions::MANAGE_NICKNAMES - pub fn nickname(&mut self, nickname: impl Into) -> &mut Self { + /// [Manage Nicknames]: Permissions::MANAGE_NICKNAMES + pub fn nickname(mut self, nickname: impl Into) -> Self { self.nick = Some(nickname.into()); self } /// Set the list of roles that the member should have. /// - /// Requires the [Manage Roles] permission to modify. + /// **Note**: Requires the [Manage Roles] permission to modify. /// - /// [Manage Roles]: crate::model::permissions::Permissions::MANAGE_ROLES - pub fn roles(&mut self, roles: impl IntoIterator>) -> &mut Self { + /// [Manage Roles]: Permissions::MANAGE_ROLES + pub fn roles(mut self, roles: impl IntoIterator>) -> Self { self.roles = Some(roles.into_iter().map(Into::into).collect()); self } - /// The Id of the voice channel to move the member to. + /// Move the member into a voice channel. /// - /// Requires the [Move Members] permission. + /// **Note**: Requires the [Move Members] permission. /// - /// [Move Members]: crate::model::permissions::Permissions::MOVE_MEMBERS + /// [Move Members]: Permissions::MOVE_MEMBERS #[inline] - pub fn voice_channel>(&mut self, channel_id: C) -> &mut Self { + pub fn voice_channel(mut self, channel_id: impl Into) -> Self { self.channel_id = Some(Some(channel_id.into())); self } - /// Disconnects the user from their voice channel if any + /// Disconnects the user from their voice channel, if any. /// - /// Requires the [Move Members] permission. + /// **Note**: Requires the [Move Members] permission. /// - /// [Move Members]: crate::model::permissions::Permissions::MOVE_MEMBERS - pub fn disconnect_member(&mut self) -> &mut Self { + /// [Move Members]: Permissions::MOVE_MEMBERS + pub fn disconnect_member(mut self) -> Self { self.channel_id = Some(None); self } @@ -90,11 +107,11 @@ impl EditMember { /// `time` is considered invalid if it is not a valid ISO8601 timestamp or if it is greater /// than 28 days from the current time. /// - /// Requires the [Moderate Members] permission. + /// **Note**: Requires the [Moderate Members] permission. /// - /// [Moderate Members]: crate::model::permissions::Permissions::MODERATE_MEMBERS + /// [Moderate Members]: Permissions::MODERATE_MEMBERS #[doc(alias = "timeout")] - pub fn disable_communication_until(&mut self, time: String) -> &mut Self { + pub fn disable_communication_until(mut self, time: String) -> Self { self.communication_disabled_until = Some(Some(time)); self } @@ -102,22 +119,22 @@ impl EditMember { /// Times the user out until `time`. /// /// `time` is considered invalid if it is greater than 28 days from the current time. - /// Requires the [Moderate Members] permission. /// - /// [Moderate Members]: crate::model::permissions::Permissions::MODERATE_MEMBERS + /// **Note**: Requires the [Moderate Members] permission. + /// + /// [Moderate Members]: Permissions::MODERATE_MEMBERS #[doc(alias = "timeout")] - pub fn disable_communication_until_datetime(&mut self, time: Timestamp) -> &mut Self { - self.disable_communication_until(time.to_string()); - self + pub fn disable_communication_until_datetime(self, time: Timestamp) -> Self { + self.disable_communication_until(time.to_string()) } /// Allow a user to communicate, removing their timeout, if there is one. /// - /// Requires the [Moderate Members] permission. + /// **Note**: Requires the [Moderate Members] permission. /// - /// [Moderate Members]: crate::model::permissions::Permissions::MODERATE_MEMBERS + /// [Moderate Members]: Permissions::MODERATE_MEMBERS #[doc(alias = "timeout")] - pub fn enable_communication(&mut self) -> &mut Self { + pub fn enable_communication(mut self) -> Self { self.communication_disabled_until = Some(None); self } diff --git a/src/builder/edit_message.rs b/src/builder/edit_message.rs index 2d184f088be..7e9d2b200ce 100644 --- a/src/builder/edit_message.rs +++ b/src/builder/edit_message.rs @@ -1,6 +1,9 @@ use super::{CreateAllowedMentions, CreateComponents, CreateEmbed}; -use crate::model::channel::{AttachmentType, MessageFlags}; -use crate::model::id::AttachmentId; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; /// A builder to specify the fields to edit in an existing message. /// @@ -9,6 +12,7 @@ use crate::model::id::AttachmentId; /// Editing the content of a [`Message`] to `"hello"`: /// /// ```rust,no_run +/// # use serenity::builder::EditMessage; /// # use serenity::model::id::{ChannelId, MessageId}; /// # #[cfg(feature = "client")] /// # use serenity::client::Context; @@ -19,16 +23,18 @@ use crate::model::id::AttachmentId; /// # #[command] /// # async fn example(ctx: &Context) -> CommandResult { /// # let mut message = ChannelId::new(7).message(&ctx, MessageId::new(8)).await?; -/// message.edit(ctx, |m| m.content("hello")).await?; +/// let builder = EditMessage::default().content("hello"); +/// message.edit(ctx, builder).await?; /// # Ok(()) /// # } /// ``` /// /// [`Message`]: crate::model::channel::Message #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditMessage<'a> { #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) content: Option, + content: Option, #[serde(skip_serializing_if = "Option::is_none")] embeds: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -41,90 +47,123 @@ pub struct EditMessage<'a> { attachments: Option>, #[serde(skip)] - pub(crate) files: Vec>, + files: Vec>, } impl<'a> EditMessage<'a> { - /// Set the content of the message. + /// Edits a message in the channel. /// - /// **Note**: Message contents must be under 2000 unicode code points. - #[inline] - pub fn content(&mut self, content: impl Into) -> &mut Self { - self.content = Some(content.into()); - self + /// **Note**: Message contents must be under 2000 unicode code points, and embeds must be under + /// 6000 code points. + /// + /// **Note**: Requires that the current user be the author of the message. Other users can + /// only call [`Self::suppress_embeds`], but additionally require the [Manage Messages] + /// permission to do so. + /// + /// **Note**: If any embeds or attachments are set, they will overwrite the existing contents + /// of the message, deleting existing embeds and attachments. Preserving them requires calling + /// [`Self::add_existing_attachment`] in the case of attachments. In the case of embeds, + /// duplicate copies of the existing embeds must be sent. Luckily, [`CreateEmbed`] implements + /// [`From`], so one can simply call `embed.into()`. + /// + /// # Errors + /// + /// Returns a [`ModelError::MessageTooLong`] if the message contents are over the above limits. + /// + /// Returns [`Error::Http`] if the user lacks permission, as well as if invalid data is given. + /// + /// [Manage Messages]: Permissions::MANAGE_MESSAGES + /// [`From`]: CreateEmbed#impl-From + #[cfg(feature = "http")] + pub async fn execute( + mut self, + http: impl AsRef, + channel_id: ChannelId, + message_id: MessageId, + ) -> Result { + self.check_length()?; + let files = std::mem::take(&mut self.files); + + if files.is_empty() { + http.as_ref().edit_message(channel_id.into(), message_id.into(), &self).await + } else { + http.as_ref() + .edit_message_and_attachments(channel_id.into(), message_id.into(), &self, files) + .await + } } - fn embeds(&mut self) -> &mut Vec { - self.embeds.get_or_insert_with(Vec::new) - } + #[cfg(feature = "http")] + fn check_length(&self) -> Result<()> { + if let Some(content) = &self.content { + let length = content.chars().count(); + let max_length = crate::constants::MESSAGE_CODE_LIMIT; + if length > max_length { + return Err(Error::Model(ModelError::MessageTooLong(length - max_length))); + } + } - fn _add_embed(&mut self, embed: CreateEmbed) -> &mut Self { - self.embeds().push(embed); + if let Some(embeds) = &self.embeds { + if embeds.len() > crate::constants::EMBED_MAX_COUNT { + return Err(Error::Model(ModelError::EmbedAmount)); + } + for embed in embeds { + embed.check_length()?; + } + } + Ok(()) + } + + /// Set the content of the message. + /// + /// **Note**: Message contents must be under 2000 unicode code points. + #[inline] + pub fn content(mut self, content: impl Into) -> Self { + self.content = Some(content.into()); self } /// Add an embed for the message. /// - /// **Note**: This will keep all existing embeds. Use [`Self::set_embed()`] to replace existing + /// **Note**: This will keep all existing embeds. Use [`Self::embed()`] to replace existing /// embeds. - pub fn add_embed(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, - { - let mut embed = CreateEmbed::default(); - f(&mut embed); - self._add_embed(embed) + pub fn add_embed(mut self, embed: CreateEmbed) -> Self { + self.embeds.get_or_insert_with(Vec::new).push(embed); + self } /// Add multiple embeds for the message. /// - /// **Note**: This will keep all existing embeds. Use [`Self::set_embeds()`] to replace existing + /// **Note**: This will keep all existing embeds. Use [`Self::embeds()`] to replace existing /// embeds. - pub fn add_embeds(&mut self, embeds: Vec) -> &mut Self { - self.embeds().extend(embeds); + pub fn add_embeds(mut self, embeds: Vec) -> Self { + self.embeds.get_or_insert_with(Vec::new).extend(embeds); self } /// Set an embed for the message. /// - /// Equivalent to [`Self::set_embed()`]. - /// - /// **Note**: This will replace all existing embeds. Use - /// [`Self::add_embed()`] to add an additional embed. - pub fn embed(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, - { - let mut embed = CreateEmbed::default(); - f(&mut embed); - self.set_embed(embed) - } - - /// Set an embed for the message. - /// - /// Equivalent to [`Self::embed()`]. - /// - /// **Note**: This will replace all existing embeds. - /// Use [`Self::add_embed()`] to add an additional embed. - pub fn set_embed(&mut self, embed: CreateEmbed) -> &mut Self { - self.set_embeds(vec![embed]) + /// **Note**: This will replace all existing embeds. Use [`Self::add_embed()`] to keep existing + /// embeds. + pub fn embed(self, embed: CreateEmbed) -> Self { + self.embeds(vec![embed]) } /// Set multiple embeds for the message. /// /// **Note**: This will replace all existing embeds. Use [`Self::add_embeds()`] to keep existing /// embeds. - pub fn set_embeds(&mut self, embeds: Vec) -> &mut Self { + pub fn embeds(mut self, embeds: Vec) -> Self { self.embeds = Some(embeds); self } /// Suppress or unsuppress embeds in the message, this includes those generated by Discord /// themselves. - pub fn suppress_embeds(&mut self, suppress: bool) -> &mut Self { - // `1 << 2` is defined by the API to be the SUPPRESS_EMBEDS flag. - // At the time of writing, the only accepted value in "flags" is `SUPPRESS_EMBEDS` for editing messages. + pub fn suppress_embeds(mut self, suppress: bool) -> Self { + // At time of writing, only `SUPPRESS_EMBEDS` can be set/unset when editing messages. See + // for details: https://discord.com/developers/docs/resources/channel#edit-message-jsonform-params let flags = suppress.then(|| MessageFlags::SUPPRESS_EMBEDS).unwrap_or_else(MessageFlags::empty); @@ -133,36 +172,19 @@ impl<'a> EditMessage<'a> { } /// Set the allowed mentions for the message. - pub fn allowed_mentions(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, - { - let mut allowed_mentions = CreateAllowedMentions::default(); - f(&mut allowed_mentions); - + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { self.allowed_mentions = Some(allowed_mentions); self } - /// Creates components for this message. - pub fn components(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, - { - let mut components = CreateComponents::default(); - f(&mut components); - - self.set_components(components) - } - /// Sets the components of this message. - pub fn set_components(&mut self, components: CreateComponents) -> &mut Self { + pub fn components(mut self, components: CreateComponents) -> Self { self.components = Some(components); self } /// Sets the flags for the message. - pub fn flags(&mut self, flags: MessageFlags) -> &mut Self { + pub fn flags(mut self, flags: MessageFlags) -> Self { self.flags = Some(flags); self } @@ -170,29 +192,24 @@ impl<'a> EditMessage<'a> { /// Add a new attachment for the message. /// /// This can be called multiple times. - pub fn attachment(&mut self, attachment: impl Into>) -> &mut Self { + pub fn attachment(mut self, attachment: impl Into>) -> Self { self.files.push(attachment.into()); self } - fn attachments(&mut self) -> &mut Vec { - self.attachments.get_or_insert_with(Vec::new) - } - /// Add an existing attachment by id. - pub fn add_existing_attachment(&mut self, attachment: AttachmentId) -> &mut Self { - self.attachments().push(attachment); + pub fn add_existing_attachment(mut self, attachment: AttachmentId) -> Self { + self.attachments.get_or_insert_with(Vec::new).push(attachment); self } /// Remove an existing attachment by id. - pub fn remove_existing_attachment(&mut self, attachment: AttachmentId) -> &mut Self { + pub fn remove_existing_attachment(mut self, attachment: AttachmentId) -> Self { if let Some(attachments) = &mut self.attachments { if let Some(attachment_index) = attachments.iter().position(|a| *a == attachment) { attachments.remove(attachment_index); }; } - self } } diff --git a/src/builder/edit_profile.rs b/src/builder/edit_profile.rs index d667b93c44d..9c1cd48a551 100644 --- a/src/builder/edit_profile.rs +++ b/src/builder/edit_profile.rs @@ -1,8 +1,14 @@ -/// A builder to edit the current user's settings, to be used in conjunction -/// with [`CurrentUser::edit`]. -/// -/// [`CurrentUser::edit`]: crate::model::user::CurrentUser::edit +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::{channel::AttachmentType, user::CurrentUser}; + +/// A builder to edit the current user's settings, to be used in conjunction with +/// [`CurrentUser::edit`]. #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditProfile { #[serde(skip_serializing_if = "Option::is_none")] avatar: Option>, @@ -11,19 +17,25 @@ pub struct EditProfile { } impl EditProfile { - /// Sets the avatar of the current user. [`None`] can be passed to remove an - /// avatar. + /// Edit the current user's profile with the fields set. /// - /// A base64-encoded string is accepted as the avatar content. + /// # Errors /// - /// # Examples + /// Returns an [`Error::Http`] if an invalid value is set. May also return an [`Error::Json`] + /// if there is an error in deserializing the API response. + #[cfg(feature = "http")] + pub async fn execute(self, http: impl AsRef) -> Result { + http.as_ref().edit_profile(&self).await + } + + /// Set the avatar of the current user. /// - /// A utility method - [`utils::read_image`] - is provided to read an - /// image from a file and return its contents in base64-encoded form: + /// # Examples /// /// ```rust,no_run /// # #[cfg(all(feature = "client", feature = "cache", feature = "gateway"))] /// # { + /// # use serenity::builder::EditProfile; /// # use serenity::prelude::*; /// # use serenity::model::prelude::*; /// # @@ -32,32 +44,54 @@ impl EditProfile { /// # #[serenity::async_trait] /// # impl EventHandler for Handler { /// # async fn message(&self, context: Context, _: Message) { - /// use serenity::utils; - /// /// // assuming a `context` has been bound - /// - /// let base64 = utils::read_image("./my_image.jpg").expect("Failed to read image"); - /// /// let mut user = context.cache.current_user().clone(); - /// let _ = user.edit(&context, |p| p.avatar(Some(base64))).await; + /// + /// let builder = EditProfile::default() + /// .avatar(&context, "./my_image.jpg") + /// .await + /// .expect("Failed to read image."); + /// let _ = user.edit(&context, builder).await; /// # } /// # } /// # } /// ``` /// - /// [`utils::read_image`]: crate::utils::read_image - pub fn avatar(&mut self, avatar: Option) -> &mut Self { - self.avatar = Some(avatar); + /// # Errors + /// + /// May error if the input is a URL and the HTTP request fails, or if it is a path to a file + /// that does not exist. + #[cfg(feature = "http")] + pub async fn avatar<'a>( + mut self, + http: impl AsRef, + avatar: impl Into>, + ) -> Result { + let avatar_data = avatar.into().data(&http.as_ref().client).await?; + self.avatar = Some(Some(crate::utils::encode_image(&avatar_data))); + Ok(self) + } + + #[cfg(not(feature = "http"))] + /// Set the current user's avatar. Requires the input be a base64-encoded image that is in + /// either JPG, GIF, or PNG format. + pub fn avatar(mut self, avatar: String) -> Self { + self.avatar = Some(Some(avatar)); + self + } + + /// Delete the current user's avatar, resetting it to the default logo. + pub fn delete_avatar(mut self) -> Self { + self.avatar = Some(None); self } /// Modifies the current user's username. /// - /// When modifying the username, if another user has the same _new_ username - /// and current discriminator, a new unique discriminator will be assigned. - /// If there are no available discriminators with the requested username, - /// an error will occur. - pub fn username(&mut self, username: impl Into) -> &mut Self { + /// When modifying the username, if another user has the same _new_ username and current + /// discriminator, a new unique discriminator will be assigned. If there are no available + /// discriminators with the requested username, an error will occur. + pub fn username(mut self, username: impl Into) -> Self { self.username = Some(username.into()); self } diff --git a/src/builder/edit_role.rs b/src/builder/edit_role.rs index f305aad4040..bb9cec0dca4 100644 --- a/src/builder/edit_role.rs +++ b/src/builder/edit_role.rs @@ -1,12 +1,9 @@ -#[cfg(feature = "model")] -use crate::http::Http; -#[cfg(feature = "model")] +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] use crate::internal::prelude::*; -#[cfg(feature = "model")] -use crate::model::channel::AttachmentType; -use crate::model::guild::Role; -use crate::model::Permissions; -#[cfg(feature = "model")] +use crate::model::prelude::*; +#[cfg(feature = "http")] use crate::utils::encode_image; /// A builder to create or edit a [`Role`] for use via a number of model methods. @@ -14,6 +11,7 @@ use crate::utils::encode_image; /// These are: /// /// - [`PartialGuild::create_role`] +/// - [`PartialGuild::edit_role`] /// - [`Guild::create_role`] /// - [`Guild::edit_role`] /// - [`GuildId::create_role`] @@ -27,23 +25,23 @@ use crate::utils::encode_image; /// Create a hoisted, mentionable role named `"a test role"`: /// /// ```rust,no_run -/// # use serenity::{model::id::{ChannelId, GuildId}, http::Http}; +/// # use serenity::builder::EditRole; +/// # use serenity::http::Http; +/// # use serenity::model::id::GuildId; /// # use std::sync::Arc; /// # +/// # async fn run() -> Result<(), Box> { /// # let http = Arc::new(Http::new("token")); -/// # let (channel_id, guild_id) = (ChannelId::new(1), GuildId::new(2)); +/// # let guild_id = GuildId::new(2); /// # -/// // assuming a `channel_id` and `guild_id` has been bound -/// -/// let role = guild_id.create_role(&http, |r| r.hoist(true).mentionable(true).name("a test role")); +/// // assuming a `guild_id` has been bound +/// let builder = EditRole::default().name("a test role").hoist(true).mentionable(true); +/// let role = guild_id.create_role(&http, builder).await?; +/// # Ok(()) +/// # } /// ``` -/// -/// [`PartialGuild::create_role`]: crate::model::guild::PartialGuild::create_role -/// [`Guild::create_role`]: crate::model::guild::Guild::create_role -/// [`Guild::edit_role`]: crate::model::guild::Guild::edit_role -/// [`GuildId::create_role`]: crate::model::id::GuildId::create_role -/// [`GuildId::edit_role`]: crate::model::id::GuildId::edit_role #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditRole { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "color")] @@ -57,7 +55,7 @@ pub struct EditRole { #[serde(skip_serializing_if = "Option::is_none")] permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) position: Option, + position: Option, #[serde(skip_serializing_if = "Option::is_none")] unicode_emoji: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -65,8 +63,58 @@ pub struct EditRole { } impl EditRole { + /// Edits the role. + /// + /// **Note**: Requires the [Manage Roles] permission. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// + /// [Manage Roles]: Permissions::MANAGE_ROLES + #[cfg(feature = "http")] + pub async fn execute( + self, + cache_http: impl CacheHttp, + guild_id: GuildId, + role_id: Option, + ) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(guild) = cache.guild(guild_id) { + let req = Permissions::MANAGE_ROLES; + + if !guild.has_perms(&cache_http, req).await { + return Err(Error::Model(ModelError::InvalidPermissions(req))); + } + } + } + } + + self._execute(cache_http.http(), guild_id, role_id).await + } + + #[cfg(feature = "http")] + async fn _execute( + self, + http: &Http, + guild_id: GuildId, + role_id: Option, + ) -> Result { + let role = match role_id { + Some(role_id) => http.edit_role(guild_id.into(), role_id.into(), &self, None).await?, + None => http.create_role(guild_id.into(), &self, None).await?, + }; + + if let Some(position) = self.position { + guild_id.edit_role_position(http, role.id, position as u64).await?; + } + Ok(role) + } + /// Creates a new builder with the values of the given [`Role`]. - #[must_use] pub fn new(role: &Role) -> Self { let colour; @@ -92,66 +140,63 @@ impl EditRole { } } - /// Sets the colour of the role. - pub fn colour(&mut self, colour: u32) -> &mut Self { + /// Set the colour of the role. + pub fn colour(mut self, colour: u32) -> Self { self.colour = Some(colour); - self } - /// Whether or not to hoist the role above lower-positioned role in the user - /// list. - pub fn hoist(&mut self, hoist: bool) -> &mut Self { + /// Whether or not to hoist the role above lower-positioned roles in the user list. + pub fn hoist(mut self, hoist: bool) -> Self { self.hoist = Some(hoist); - self } - /// Whether or not to make the role mentionable, notifying its users. - pub fn mentionable(&mut self, mentionable: bool) -> &mut Self { + /// Whether or not to make the role mentionable, upon which users with that role will be + /// notified. + pub fn mentionable(mut self, mentionable: bool) -> Self { self.mentionable = Some(mentionable); self } - /// The name of the role to set. - pub fn name(&mut self, name: impl Into) -> &mut Self { + /// Set the role's name. + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } - /// The set of permissions to assign the role. - pub fn permissions(&mut self, permissions: Permissions) -> &mut Self { + /// Set the role's permissions. + pub fn permissions(mut self, permissions: Permissions) -> Self { self.permissions = Some(permissions.bits()); self } - /// The position to assign the role in the role list. This correlates to the - /// role's position in the user list. - pub fn position(&mut self, position: i64) -> &mut Self { + /// Set the role's position in the role list. This correlates to the role's position in the + /// user list. + pub fn position(mut self, position: i64) -> Self { self.position = Some(position); self } - /// The unicode emoji to set as the role image. - pub fn unicode_emoji(&mut self, unicode_emoji: impl Into) -> &mut Self { + /// Set the role icon to a unicode emoji. + pub fn unicode_emoji(mut self, unicode_emoji: impl Into) -> Self { self.unicode_emoji = Some(unicode_emoji.into()); self.icon = None; - self } - /// The image to set as the role icon. + /// Set the role icon to a custom image. /// /// # Errors /// /// May error if the icon is a URL and the HTTP request fails, or if the icon is a file /// on a path that doesn't exist. - #[cfg(feature = "model")] + #[cfg(feature = "http")] pub async fn icon<'a>( - &mut self, + mut self, http: impl AsRef, icon: impl Into>, - ) -> Result<&mut Self> { + ) -> Result { let icon_data = icon.into().data(&http.as_ref().client).await?; self.icon = Some(encode_image(&icon_data)); @@ -159,4 +204,13 @@ impl EditRole { Ok(self) } + + /// Set the role icon to custom image. Requires the input be a base64-encoded image that is in + /// either JPG, GIF, or PNG format. + #[cfg(not(feature = "http"))] + pub fn icon(mut self, icon: String) -> Self { + self.icon = Some(icon); + self.unicode_emoji = None; + self + } } diff --git a/src/builder/edit_scheduled_event.rs b/src/builder/edit_scheduled_event.rs index 86233ee00e6..65baff29e8a 100644 --- a/src/builder/edit_scheduled_event.rs +++ b/src/builder/edit_scheduled_event.rs @@ -1,16 +1,13 @@ -#[cfg(feature = "model")] -use crate::http::Http; -#[cfg(feature = "model")] +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] use crate::internal::prelude::*; -#[cfg(feature = "model")] -use crate::model::channel::AttachmentType; -use crate::model::guild::{ScheduledEventMetadata, ScheduledEventStatus, ScheduledEventType}; -use crate::model::id::ChannelId; -use crate::model::Timestamp; -#[cfg(feature = "model")] +use crate::model::prelude::*; +#[cfg(feature = "http")] use crate::utils::encode_image; #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditScheduledEvent { #[serde(skip_serializing_if = "Option::is_none")] channel_id: Option>, @@ -33,33 +30,75 @@ pub struct EditScheduledEvent { } impl EditScheduledEvent { + /// Modifies a scheduled event in the guild with the data set, if any. + /// + /// **Note**: Requires the [Manage Events] permission. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. + /// + /// [Manage Events]: Permissions::MANAGE_EVENTS + #[cfg(feature = "http")] + pub async fn execute( + self, + cache_http: impl CacheHttp, + guild_id: GuildId, + event_id: ScheduledEventId, + ) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(guild) = cache.guild(guild_id) { + let req = Permissions::MANAGE_EVENTS; + + if !guild.has_perms(&cache_http, req).await { + return Err(Error::Model(ModelError::InvalidPermissions(req))); + } + } + } + } + + self._execute(cache_http.http(), guild_id, event_id).await + } + + #[cfg(feature = "http")] + async fn _execute( + self, + http: &Http, + guild_id: GuildId, + event_id: ScheduledEventId, + ) -> Result { + http.as_ref().edit_scheduled_event(guild_id.into(), event_id.into(), &self, None).await + } + /// Sets the channel id of the scheduled event. If the [`kind`] of the event is changed from /// [`External`] to either [`StageInstance`] or [`Voice`], then this field is also required. /// /// [`kind`]: EditScheduledEvent::kind - /// [`StageInstance`]: ScheduledEventType::StageInstance /// [`Voice`]: ScheduledEventType::Voice /// [`External`]: ScheduledEventType::External - pub fn channel_id>(&mut self, channel_id: C) -> &mut Self { + pub fn channel_id(mut self, channel_id: impl Into) -> Self { self.channel_id = Some(Some(channel_id.into())); self } /// Sets the name of the scheduled event. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } /// Sets the description of the scheduled event. - pub fn description(&mut self, description: impl Into) -> &mut Self { + pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); self } /// Sets the start time of the scheduled event. #[inline] - pub fn start_time>(&mut self, timestamp: T) -> &mut Self { + pub fn start_time(mut self, timestamp: impl Into) -> Self { self.scheduled_start_time = Some(timestamp.into().to_string()); self } @@ -71,7 +110,7 @@ impl EditScheduledEvent { /// [`kind`]: EditScheduledEvent::kind /// [`External`]: ScheduledEventType::External #[inline] - pub fn end_time>(&mut self, timestamp: T) -> &mut Self { + pub fn end_time(mut self, timestamp: impl Into) -> Self { self.scheduled_end_time = Some(timestamp.into().to_string()); self } @@ -90,7 +129,7 @@ impl EditScheduledEvent { /// [`StageInstance`]: ScheduledEventType::StageInstance /// [`Voice`]: ScheduledEventType::Voice /// [`External`]: ScheduledEventType::External - pub fn kind(&mut self, kind: ScheduledEventType) -> &mut Self { + pub fn kind(mut self, kind: ScheduledEventType) -> Self { if let ScheduledEventType::External = kind { self.channel_id = Some(None); } else { @@ -118,7 +157,7 @@ impl EditScheduledEvent { /// [`Active`]: ScheduledEventStatus::Active /// [`Completed`]: ScheduledEventStatus::Completed /// [`Canceled`]: ScheduledEventStatus::Canceled - pub fn status(&mut self, status: ScheduledEventStatus) -> &mut Self { + pub fn status(mut self, status: ScheduledEventStatus) -> Self { self.status = Some(status); self } @@ -130,7 +169,7 @@ impl EditScheduledEvent { /// /// [`kind`]: EditScheduledEvent::kind /// [`External`]: ScheduledEventType::External - pub fn location(&mut self, location: impl Into) -> &mut Self { + pub fn location(mut self, location: impl Into) -> Self { self.entity_metadata = Some(Some(ScheduledEventMetadata { location: location.into(), })); @@ -141,16 +180,24 @@ impl EditScheduledEvent { /// /// # Errors /// - /// May error if the icon is a URL and the HTTP request fails, or if the image is a file - /// on a path that doesn't exist. - #[cfg(feature = "model")] + /// May error if the input is a URL and the HTTP request fails, or if it is a path to a file + /// that does not exist. + #[cfg(feature = "http")] pub async fn image<'a>( - &mut self, + mut self, http: impl AsRef, image: impl Into>, - ) -> Result<&mut Self> { + ) -> Result { let image_data = image.into().data(&http.as_ref().client).await?; self.image = Some(encode_image(&image_data)); Ok(self) } + + /// Sets the cover image for the scheduled event. Requires the input be a base64-encoded image + /// that is in either JPG, GIF, or PNG format. + #[cfg(not(feature = "http"))] + pub fn image(mut self, image: String) -> Self { + self.image = Some(image); + self + } } diff --git a/src/builder/edit_stage_instance.rs b/src/builder/edit_stage_instance.rs index 303b85ed789..3453dd0ed99 100644 --- a/src/builder/edit_stage_instance.rs +++ b/src/builder/edit_stage_instance.rs @@ -1,15 +1,55 @@ +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::prelude::*; + /// Edits a [`StageInstance`]. -/// -/// [`StageInstance`]: crate::model::channel::StageInstance #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditStageInstance { #[serde(skip_serializing_if = "Option::is_none")] topic: Option, } impl EditStageInstance { + /// Edits the stage instance + /// + /// # Errors + /// + /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. + /// + /// Returns [`Error::Http`] if the channel is not a stage channel, or there is no stage + /// instance currently. + #[cfg(feature = "http")] + #[inline] + pub async fn execute( + self, + cache_http: impl CacheHttp, + channel_id: ChannelId, + ) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(channel) = cache.guild_channel(channel_id) { + if channel.kind != ChannelType::Stage { + return Err(Error::Model(ModelError::InvalidChannelType)); + } + } + } + } + + self._execute(cache_http.http(), channel_id).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http, channel_id: ChannelId) -> Result { + http.edit_stage_instance(channel_id.into(), &self).await + } + /// Sets the topic of the stage channel instance. - pub fn topic(&mut self, topic: impl Into) -> &mut Self { + pub fn topic(mut self, topic: impl Into) -> Self { self.topic = Some(topic.into()); self } diff --git a/src/builder/edit_sticker.rs b/src/builder/edit_sticker.rs index afd62d10acb..c2dd1f9db4c 100644 --- a/src/builder/edit_sticker.rs +++ b/src/builder/edit_sticker.rs @@ -1,3 +1,10 @@ +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::prelude::*; + /// A builder to create or edit a [`Sticker`] for use via a number of model methods. /// /// These are: @@ -13,6 +20,7 @@ /// [`GuildId::edit_sticker`]: crate::model::id::GuildId::edit_sticker /// [`Sticker::edit`]: crate::model::sticker::Sticker::edit #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditSticker { #[serde(skip_serializing_if = "Option::is_none")] name: Option, @@ -23,10 +31,29 @@ pub struct EditSticker { } impl EditSticker { + /// Edits the sticker. + /// + /// **Note**: Requires the [Manage Emojis and Stickers] permission. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + /// + /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + guild_id: GuildId, + sticker_id: StickerId, + ) -> Result { + http.as_ref().edit_sticker(guild_id.into(), sticker_id.into(), &self, None).await + } + /// The name of the sticker to set. /// /// **Note**: Must be between 2 and 30 characters long. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } @@ -34,7 +61,7 @@ impl EditSticker { /// The description of the sticker. /// /// **Note**: If not empty, must be between 2 and 100 characters long. - pub fn description(&mut self, description: impl Into) -> &mut Self { + pub fn description(mut self, description: impl Into) -> Self { self.description = Some(description.into()); self } @@ -42,7 +69,7 @@ impl EditSticker { /// The Discord name of a unicode emoji representing the sticker's expression. /// /// **Note**: Must be between 2 and 200 characters long. - pub fn tags(&mut self, tags: impl Into) -> &mut Self { + pub fn tags(mut self, tags: impl Into) -> Self { self.tags = Some(tags.into()); self } diff --git a/src/builder/edit_thread.rs b/src/builder/edit_thread.rs index f5897869412..a620b43ca58 100644 --- a/src/builder/edit_thread.rs +++ b/src/builder/edit_thread.rs @@ -1,4 +1,12 @@ +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::prelude::*; + #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditThread { #[serde(skip_serializing_if = "Option::is_none")] name: Option, @@ -13,46 +21,56 @@ pub struct EditThread { } impl EditThread { + /// Edits the thread. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + channel_id: ChannelId, + ) -> Result { + http.as_ref().edit_thread(channel_id.into(), &self).await + } + /// The name of the thread. /// /// **Note**: Must be between 2 and 100 characters long. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); - self } /// Duration in minutes to automatically archive the thread after recent activity. /// /// **Note**: Can only be set to 60, 1440, 4320, 10080 currently. - pub fn auto_archive_duration(&mut self, duration: u16) -> &mut Self { + pub fn auto_archive_duration(mut self, duration: u16) -> Self { self.auto_archive_duration = Some(duration); - self } /// The archive status of the thread. /// - /// **Note**: A thread that is `locked` can only be unarchived if the user has the `MANAGE_THREADS` permission. - pub fn archived(&mut self, archived: bool) -> &mut Self { + /// **Note**: A thread that is `locked` can only be unarchived if the user has the + /// `MANAGE_THREADS` permission. + pub fn archived(mut self, archived: bool) -> Self { self.archived = Some(archived); - self } /// The lock status of the thread. - pub fn locked(&mut self, lock: bool) -> &mut Self { + pub fn locked(mut self, lock: bool) -> Self { self.locked = Some(lock); - self } /// Whether non-moderators can add other non-moderators to a thread. /// /// **Note**: Only available on private threads. - pub fn invitable(&mut self, invitable: bool) -> &mut Self { + pub fn invitable(mut self, invitable: bool) -> Self { self.invitable = Some(invitable); - self } } diff --git a/src/builder/edit_voice_state.rs b/src/builder/edit_voice_state.rs index bc39b1a6d87..92a8f11c5a4 100644 --- a/src/builder/edit_voice_state.rs +++ b/src/builder/edit_voice_state.rs @@ -1,13 +1,15 @@ -use crate::model::id::ChannelId; -use crate::model::Timestamp; +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; /// A builder which edits a user's voice state, to be used in conjunction with /// [`GuildChannel::edit_voice_state`]. -/// -/// [`GuildChannel::edit_voice_state`]: crate::model::channel::GuildChannel::edit_voice_state #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditVoiceState { - pub(crate) channel_id: Option, + channel_id: Option, #[serde(skip_serializing_if = "Option::is_none")] suppress: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -15,45 +17,84 @@ pub struct EditVoiceState { } impl EditVoiceState { - /// Whether to suppress the user. Setting this to false will invite a user - /// to speak. + /// Edits the given user's voice state in a stage channel. Pass [`None`] for `user_id` to edit + /// the current user's voice state. /// - /// Requires the [Mute Members] permission to suppress another user or - /// unsuppress the current user. + /// **Note**: Requires the [Request to Speak] permission. Also requires the [Mute Members] + /// permission to suppress another user or unsuppress the current user. This is not required if + /// suppressing the current user. /// - /// [Mute Members]: crate::model::permissions::Permissions::MUTE_MEMBERS - pub fn suppress(&mut self, deafen: bool) -> &mut Self { + /// # Errors + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidChannelType`] if the channel is + /// not a stage channel. + /// + /// Returns [`Error::Http`] if the user lacks permission, or if invalid data is given. + /// + /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK + /// [Mute Members]: Permissions::MUTE_MEMBERS + #[cfg(feature = "http")] + pub async fn execute( + mut self, + cache_http: impl CacheHttp, + guild_id: GuildId, + channel_id: ChannelId, + user_id: Option, + ) -> Result<()> { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(channel) = cache.guild_channel(channel_id) { + if channel.kind != ChannelType::Stage { + return Err(Error::from(ModelError::InvalidChannelType)); + } + } + } + } + + self.channel_id = Some(channel_id); + self._execute(cache_http.http(), guild_id, user_id).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http, guild_id: GuildId, user_id: Option) -> Result<()> { + if let Some(user_id) = user_id { + http.edit_voice_state(guild_id.into(), user_id.into(), &self).await + } else { + http.edit_voice_state_me(guild_id.into(), &self).await + } + } + + /// Whether to suppress the user. Setting this to false will invite a user to speak. + /// + /// **Note**: Requires the [Mute Members] permission to suppress another user or unsuppress the + /// current user. This is not required if suppressing the current user. + /// + /// [Mute Members]: Permissions::MUTE_MEMBERS + pub fn suppress(mut self, deafen: bool) -> Self { self.suppress = Some(deafen); self } - /// Requests or clears a request to speak. This is equivalent to passing the - /// current time to [`Self::request_to_speak_timestamp`]. + /// Requests or clears a request to speak. Passing `true` is equivalent to passing the current + /// time to [`Self::request_to_speak_timestamp`]. /// - /// Requires the [Request to Speak] permission. + /// **Note**: Requires the [Request to Speak] permission. /// - /// [Request to Speak]: crate::model::permissions::Permissions::REQUEST_TO_SPEAK - pub fn request_to_speak(&mut self, request: bool) -> &mut Self { - if request { - self.request_to_speak_timestamp(Some(Timestamp::now())); - } else { - self.request_to_speak_timestamp(None::); - } - + /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK + pub fn request_to_speak(mut self, request: bool) -> Self { + self.request_to_speak_timestamp = Some(request.then(Timestamp::now)); self } - /// Sets the current bot user's request to speak timestamp. This can be any - /// present or future time. Set this to [`None`] to clear a request to speak. + /// Sets the current bot user's request to speak timestamp. This can be any present or future + /// time. /// - /// Requires the [Request to Speak] permission. + /// **Note**: Requires the [Request to Speak] permission. /// - /// [Request to Speak]: crate::model::permissions::Permissions::REQUEST_TO_SPEAK - pub fn request_to_speak_timestamp>( - &mut self, - timestamp: Option, - ) -> &mut Self { - self.request_to_speak_timestamp = Some(timestamp.map(Into::into)); + /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK + pub fn request_to_speak_timestamp(mut self, timestamp: impl Into) -> Self { + self.request_to_speak_timestamp = Some(Some(timestamp.into())); self } } diff --git a/src/builder/edit_webhook.rs b/src/builder/edit_webhook.rs index bb292d897d3..205ade0a72f 100644 --- a/src/builder/edit_webhook.rs +++ b/src/builder/edit_webhook.rs @@ -1,6 +1,11 @@ -use crate::model::id::ChannelId; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; #[derive(Debug, Default, Clone, Serialize)] +#[must_use] pub struct EditWebhook { #[serde(skip_serializing_if = "Option::is_none")] name: Option, @@ -11,23 +16,66 @@ pub struct EditWebhook { } impl EditWebhook { - /// Set default name of the Webhook. + /// Edits the webhook corresponding to the provided Id and token, and returns the resulting ne + /// [`Webhook`]. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the content is malformed, or if the token is invalid. + /// + /// Returns [`Error::Json`] if there is an error in deserialising Discord's response. + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + webhook_id: WebhookId, + token: &str, + ) -> Result { + http.as_ref().edit_webhook_with_token(webhook_id.into(), token, &self).await + } + + /// Set the webhook's name. /// /// This must be between 1-80 characters. - pub fn name(&mut self, name: impl Into) -> &mut Self { + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } /// Set the channel to move the webhook to. - pub fn channel_id(&mut self, channel_id: impl Into) -> &mut Self { + pub fn channel_id(mut self, channel_id: impl Into) -> Self { self.channel_id = Some(channel_id.into()); self } - /// Set default avatar of the webhook. - pub fn avatar(&mut self, avatar: Option) -> &mut Self { - self.avatar = Some(avatar); + /// Set the webhook's default avatar. + /// + /// # Errors + /// + /// May error if the input is a URL and the HTTP request fails, or if it is a path to a file + /// that does not exist. + #[cfg(feature = "http")] + pub async fn avatar<'a>( + mut self, + http: impl AsRef, + avatar: impl Into>, + ) -> Result { + let avatar_data = avatar.into().data(&http.as_ref().client).await?; + self.avatar = Some(Some(crate::utils::encode_image(&avatar_data))); + Ok(self) + } + + #[cfg(not(feature = "http"))] + /// Set the webhook's default avatar. Requires the input be a base64-encoded image that is in + /// either JPG, GIF, or PNG format. + pub fn avatar(mut self, avatar: String) -> Self { + self.avatar = Some(Some(avatar)); + self + } + + /// Delete the webhook's avatar, resetting it to the default logo. + pub fn delete_avatar(mut self) -> Self { + self.avatar = Some(None); self } } diff --git a/src/builder/edit_webhook_message.rs b/src/builder/edit_webhook_message.rs index c3b2562d06a..d4f4dba3310 100644 --- a/src/builder/edit_webhook_message.rs +++ b/src/builder/edit_webhook_message.rs @@ -1,9 +1,16 @@ use super::{CreateAllowedMentions, CreateComponents, CreateEmbed}; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::prelude::*; /// A builder to specify the fields to edit in an existing [`Webhook`]'s message. /// /// [`Webhook`]: crate::model::webhook::Webhook #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct EditWebhookMessage { #[serde(skip_serializing_if = "Option::is_none")] content: Option, @@ -16,11 +23,58 @@ pub struct EditWebhookMessage { } impl EditWebhookMessage { + /// Edits the webhook's message. + /// + /// **Note**: Message contents must be under 2000 unicode code points, and embeds must be under + /// 6000 code points. + /// + /// # Errors + /// + /// Returns an [`Error::Model`] if the message content is too long. + /// + /// May also return an [`Error::Http`] if the content is malformed, the webhook's token is + /// invalid, or the given message Id does not belong to the webhook. + /// + /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response. + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + message_id: MessageId, + webhook_id: WebhookId, + token: &str, + ) -> Result { + self.check_length()?; + http.as_ref().edit_webhook_message(webhook_id.into(), token, message_id.into(), &self).await + } + + #[cfg(feature = "http")] + fn check_length(&self) -> Result<()> { + if let Some(content) = &self.content { + let length = content.chars().count(); + let max_length = crate::constants::MESSAGE_CODE_LIMIT; + if length > max_length { + return Err(Error::Model(ModelError::MessageTooLong(length - max_length))); + } + } + + if let Some(embeds) = &self.embeds { + if embeds.len() > crate::constants::EMBED_MAX_COUNT { + return Err(Error::Model(ModelError::EmbedAmount)); + } + for embed in embeds { + embed.check_length()?; + } + } + + Ok(()) + } + /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. #[inline] - pub fn content(&mut self, content: impl Into) -> &mut Self { + pub fn content(mut self, content: impl Into) -> Self { self.content = Some(content.into()); self } @@ -34,36 +88,24 @@ impl EditWebhookMessage { /// /// [struct-level documentation of `ExecuteWebhook`]: crate::builder::ExecuteWebhook#examples #[inline] - pub fn embeds(&mut self, embeds: Vec) -> &mut Self { + pub fn embeds(mut self, embeds: Vec) -> Self { self.embeds = Some(embeds); self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, - { - let mut allowed_mentions = CreateAllowedMentions::default(); - f(&mut allowed_mentions); - + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { self.allowed_mentions = Some(allowed_mentions); self } - /// Creates components for this message. Requires an application-owned webhook, meaning either + /// Sets the components for this message. Requires an application-owned webhook, meaning either /// the webhook's `kind` field is set to [`WebhookType::Application`], or it was created by an /// application (and has kind [`WebhookType::Incoming`]). /// /// [`WebhookType::Application`]: crate::model::webhook::WebhookType /// [`WebhookType::Incoming`]: crate::model::webhook::WebhookType - pub fn components(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, - { - let mut components = CreateComponents::default(); - f(&mut components); - + pub fn components(mut self, components: CreateComponents) -> Self { self.components = Some(components); self } diff --git a/src/builder/execute_webhook.rs b/src/builder/execute_webhook.rs index 05c6301fbf2..67f272e1328 100644 --- a/src/builder/execute_webhook.rs +++ b/src/builder/execute_webhook.rs @@ -1,27 +1,21 @@ -#[cfg(not(feature = "model"))] -use std::marker::PhantomData; - use super::{CreateAllowedMentions, CreateComponents, CreateEmbed}; -#[cfg(feature = "model")] -use crate::model::channel::AttachmentType; -use crate::model::channel::MessageFlags; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; -/// A builder to create the inner content of a [`Webhook`]'s execution. -/// -/// This is a structured way of cleanly creating the inner execution payload, -/// to reduce potential argument counts. +/// A builder to create the content for a [`Webhook`]'s execution. /// -/// Refer to the documentation for [`execute_webhook`] on restrictions with -/// execution payloads and its fields. +/// Refer to [`Http::execute_webhook`] for restrictions and requirements on the execution payload. /// /// # Examples /// -/// Creating two embeds, and then sending them as part of the delivery -/// payload of [`Webhook::execute`]: +/// Creating two embeds, and then sending them as part of the payload using [`Webhook::execute`]: /// /// ```rust,no_run +/// use serenity::builder::{CreateEmbed, ExecuteWebhook}; /// use serenity::http::Http; -/// use serenity::model::channel::Embed; /// use serenity::model::webhook::Webhook; /// use serenity::utils::Colour; /// @@ -30,33 +24,27 @@ use crate::model::channel::MessageFlags; /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// let webhook = Webhook::from_url(&http, url).await?; /// -/// let website = Embed::fake(|e| { -/// e.title("The Rust Language Website") -/// .description("Rust is a systems programming language.") -/// .colour(Colour::from_rgb(222, 165, 132)) -/// }); +/// let website = CreateEmbed::default() +/// .title("The Rust Language Website") +/// .description("Rust is a systems programming language.") +/// .colour(Colour::from_rgb(222, 165, 132)); /// -/// let resources = Embed::fake(|e| { -/// e.title("Rust Resources") -/// .description("A few resources to help with learning Rust") -/// .colour(0xDEA584) -/// .field("The Rust Book", "A comprehensive resource for Rust.", false) -/// .field("Rust by Example", "A collection of Rust examples", false) -/// }); +/// let resources = CreateEmbed::default() +/// .title("Rust Resources") +/// .description("A few resources to help with learning Rust") +/// .colour(0xDEA584) +/// .field("The Rust Book", "A comprehensive resource for Rust.", false) +/// .field("Rust by Example", "A collection of Rust examples", false); /// -/// webhook -/// .execute(&http, false, |w| { -/// w.content("Here's some information on Rust:").embeds(vec![website, resources]) -/// }) -/// .await?; +/// let builder = ExecuteWebhook::default() +/// .content("Here's some information on Rust:") +/// .embeds(vec![website, resources]); +/// webhook.execute(&http, false, builder).await?; /// # Ok(()) /// # } /// ``` -/// -/// [`Webhook`]: crate::model::webhook::Webhook -/// [`Webhook::execute`]: crate::model::webhook::Webhook::execute -/// [`execute_webhook`]: crate::http::client::Http::execute_webhook #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct ExecuteWebhook<'a> { tts: bool, embeds: Vec, @@ -74,13 +62,65 @@ pub struct ExecuteWebhook<'a> { flags: Option, #[serde(skip)] - #[cfg(feature = "model")] - pub(crate) files: Vec>, - #[cfg(not(feature = "model"))] - files: PhantomData<&'a ()>, + thread_id: Option, + #[serde(skip)] + files: Vec>, } impl<'a> ExecuteWebhook<'a> { + /// Executes the webhook with the given content. + /// + /// If `wait` is set to false, this function will return `Ok(None)` on success. Otherwise, + /// Discord will wait for confirmation that the message was sent, and this function will + /// instead return `Ok(Some(Message))`. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the content is malformed, if the token is invalid, or if + /// execution is attempted in a thread not belonging to the webhook's [`Channel`]. + /// + /// Returns [`Error::Json`] if there is an error in deserialising Discord's response. + #[cfg(feature = "http")] + pub async fn execute( + mut self, + http: impl AsRef, + webhook_id: WebhookId, + token: &str, + wait: bool, + ) -> Result> { + self.check_length()?; + let webhook_id = webhook_id.into(); + let thread_id = self.thread_id.map(Into::into); + let files = std::mem::take(&mut self.files); + + if files.is_empty() { + http.as_ref().execute_webhook(webhook_id, thread_id, token, wait, &self).await + } else { + http.as_ref() + .execute_webhook_with_files(webhook_id, thread_id, token, wait, files, &self) + .await + } + } + + #[cfg(feature = "http")] + fn check_length(&self) -> Result<()> { + if let Some(content) = &self.content { + let length = content.chars().count(); + let max_length = crate::constants::MESSAGE_CODE_LIMIT; + if length > max_length { + return Err(Error::Model(ModelError::MessageTooLong(length - max_length))); + } + } + + if self.embeds.len() > crate::constants::EMBED_MAX_COUNT { + return Err(Error::Model(ModelError::EmbedAmount)); + } + for embed in &self.embeds { + embed.check_length()?; + } + Ok(()) + } + /// Override the default avatar of the webhook with an image URL. /// /// # Examples @@ -88,6 +128,7 @@ impl<'a> ExecuteWebhook<'a> { /// Overriding the default avatar: /// /// ```rust,no_run + /// # use serenity::builder::ExecuteWebhook; /// # use serenity::http::Http; /// # use serenity::model::webhook::Webhook; /// # @@ -95,13 +136,14 @@ impl<'a> ExecuteWebhook<'a> { /// # let http = Http::new("token"); /// # let webhook = Webhook::from_id_with_token(&http, 0, "").await?; /// # - /// let avatar_url = "https://i.imgur.com/KTs6whd.jpg"; - /// - /// webhook.execute(&http, false, |w| w.avatar_url(avatar_url).content("Here's a webhook")).await?; + /// let builder = ExecuteWebhook::default() + /// .avatar_url("https://i.imgur.com/KTs6whd.jpg") + /// .content("Here's a webhook"); + /// webhook.execute(&http, false, builder).await?; /// # Ok(()) /// # } /// ``` - pub fn avatar_url(&mut self, avatar_url: impl Into) -> &mut Self { + pub fn avatar_url(mut self, avatar_url: impl Into) -> Self { self.avatar_url = Some(avatar_url.into()); self } @@ -116,6 +158,7 @@ impl<'a> ExecuteWebhook<'a> { /// Sending a webhook with a content of `"foo"`: /// /// ```rust,no_run + /// # use serenity::builder::ExecuteWebhook; /// # use serenity::http::Http; /// # use serenity::model::webhook::Webhook; /// # @@ -123,7 +166,8 @@ impl<'a> ExecuteWebhook<'a> { /// # let http = Http::new("token"); /// # let webhook = Webhook::from_id_with_token(&http, 0, "").await?; /// # - /// let execution = webhook.execute(&http, false, |w| w.content("foo")).await; + /// let builder = ExecuteWebhook::default().content("foo"); + /// let execution = webhook.execute(&http, false, builder).await; /// /// if let Err(why) = execution { /// println!("Err sending webhook: {:?}", why); @@ -131,87 +175,95 @@ impl<'a> ExecuteWebhook<'a> { /// # Ok(()) /// # } /// ``` - pub fn content(&mut self, content: impl Into) -> &mut Self { + pub fn content(mut self, content: impl Into) -> Self { self.content = Some(content.into()); self } + /// Execute within a given thread. If the provided thread Id doesn't belong to the current + /// webhook, the API will return an error. + /// + /// **Note**: If the given thread is archived, it will automatically be unarchived. + /// + /// # Examples + /// + /// Execute a webhook with message content of `test`, in a thread with Id `12345678`: + /// + /// ```rust,no_run + /// # use serenity::builder::ExecuteWebhook; + /// # use serenity::http::Http; + /// # use serenity::model::webhook::Webhook; + /// # + /// # async fn run() -> Result<(), Box> { + /// # let http = Http::new("token"); + /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// let mut webhook = Webhook::from_url(&http, url).await?; + /// + /// let builder = ExecuteWebhook::default().in_thread(12345678).content("test"); + /// webhook.execute(&http, false, builder).await?; + /// # Ok(()) + /// # } + /// ``` + pub fn in_thread(mut self, thread_id: impl Into) -> Self { + self.thread_id = Some(thread_id.into()); + self + } + /// Appends a file to the webhook message. - #[cfg(feature = "model")] - pub fn add_file>>(&mut self, file: T) -> &mut Self { + pub fn add_file(mut self, file: impl Into>) -> Self { self.files.push(file.into()); self } /// Appends a list of files to the webhook message. - #[cfg(feature = "model")] pub fn add_files>, It: IntoIterator>( - &mut self, + mut self, files: It, - ) -> &mut Self { + ) -> Self { self.files.extend(files.into_iter().map(Into::into)); self } /// Sets a list of files to include in the webhook message. /// - /// Calling this multiple times will overwrite the file list. - /// To append files, call [`Self::add_file`] or [`Self::add_files`] instead. - #[cfg(feature = "model")] + /// Calling this multiple times will overwrite the file list. To append files, call + /// [`Self::add_file`] or [`Self::add_files`] instead. pub fn files>, It: IntoIterator>( - &mut self, + mut self, files: It, - ) -> &mut Self { + ) -> Self { self.files = files.into_iter().map(Into::into).collect(); self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, - { - let mut allowed_mentions = CreateAllowedMentions::default(); - f(&mut allowed_mentions); - + pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self { self.allowed_mentions = Some(allowed_mentions); self } - /// Creates components for this message. Requires an application-owned webhook, meaning either + /// Sets the components for this message. Requires an application-owned webhook, meaning either /// the webhook's `kind` field is set to [`WebhookType::Application`], or it was created by an /// application (and has kind [`WebhookType::Incoming`]). /// /// [`WebhookType::Application`]: crate::model::webhook::WebhookType /// [`WebhookType::Incoming`]: crate::model::webhook::WebhookType - pub fn components(&mut self, f: F) -> &mut Self - where - F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, - { - let mut components = CreateComponents::default(); - f(&mut components); - - self.set_components(components) - } - - /// Sets the components of this message. Requires an application-owned webhook. See - /// [`components`] for details. - /// - /// [`components`]: crate::builder::ExecuteWebhook::components - pub fn set_components(&mut self, components: CreateComponents) -> &mut Self { + pub fn components(mut self, components: CreateComponents) -> Self { self.components = Some(components); self } - /// Set the embeds associated with the message. - /// - /// # Examples + /// Set an embed for the message. /// - /// Refer to the [struct-level documentation] for an example on how to use - /// embeds. + /// Refer to the [struct-level documentation] for an example on how to use embeds. /// /// [struct-level documentation]: #examples - pub fn embeds(&mut self, embeds: Vec) -> &mut Self { + pub fn embed(self, embed: CreateEmbed) -> Self { + self.embeds(vec![embed]) + } + + /// Set multiple embeds for the message. + pub fn embeds(mut self, embeds: Vec) -> Self { self.embeds = embeds; self } @@ -223,6 +275,7 @@ impl<'a> ExecuteWebhook<'a> { /// Sending a webhook with text-to-speech enabled: /// /// ```rust,no_run + /// # use serenity::builder::ExecuteWebhook; /// # use serenity::http::Http; /// # use serenity::model::webhook::Webhook; /// # @@ -230,7 +283,8 @@ impl<'a> ExecuteWebhook<'a> { /// # let http = Http::new("token"); /// # let webhook = Webhook::from_id_with_token(&http, 0, "").await?; /// # - /// let execution = webhook.execute(&http, false, |w| w.content("hello").tts(true)).await; + /// let builder = ExecuteWebhook::default().content("hello").tts(true); + /// let execution = webhook.execute(&http, false, builder).await; /// /// if let Err(why) = execution { /// println!("Err sending webhook: {:?}", why); @@ -238,7 +292,7 @@ impl<'a> ExecuteWebhook<'a> { /// # Ok(()) /// # } /// ``` - pub fn tts(&mut self, tts: bool) -> &mut Self { + pub fn tts(mut self, tts: bool) -> Self { self.tts = tts; self } @@ -250,6 +304,7 @@ impl<'a> ExecuteWebhook<'a> { /// Overriding the username to `"hakase"`: /// /// ```rust,no_run + /// # use serenity::builder::ExecuteWebhook; /// # use serenity::http::Http; /// # use serenity::model::webhook::Webhook; /// # @@ -257,7 +312,8 @@ impl<'a> ExecuteWebhook<'a> { /// # let http = Http::new("token"); /// # let webhook = Webhook::from_id_with_token(&http, 0, "").await?; /// # - /// let execution = webhook.execute(&http, false, |w| w.content("hello").username("hakase")).await; + /// let builder = ExecuteWebhook::default().content("hello").username("hakase"); + /// let execution = webhook.execute(&http, false, builder).await; /// /// if let Err(why) = execution { /// println!("Err sending webhook: {:?}", why); @@ -265,7 +321,7 @@ impl<'a> ExecuteWebhook<'a> { /// # Ok(()) /// # } /// ``` - pub fn username(&mut self, username: impl Into) -> &mut Self { + pub fn username(mut self, username: impl Into) -> Self { self.username = Some(username.into()); self } @@ -277,6 +333,7 @@ impl<'a> ExecuteWebhook<'a> { /// Suppressing an embed on the message. /// /// ```rust,no_run + /// # use serenity::builder::ExecuteWebhook; /// # use serenity::http::Http; /// # use serenity::model::channel::MessageFlags; /// # use serenity::model::webhook::Webhook; @@ -285,12 +342,10 @@ impl<'a> ExecuteWebhook<'a> { /// # let http = Http::new("token"); /// # let webhook = Webhook::from_id_with_token(&http, 0, "").await?; /// # - /// let execution = webhook - /// .execute(&http, false, |w| { - /// w.content("https://docs.rs/serenity/latest/serenity/") - /// .flags(MessageFlags::SUPPRESS_EMBEDS) - /// }) - /// .await; + /// let builder = ExecuteWebhook::default() + /// .content("https://docs.rs/serenity/latest/serenity/") + /// .flags(MessageFlags::SUPPRESS_EMBEDS); + /// let execution = webhook.execute(&http, false, builder).await; /// /// if let Err(why) = execution { /// println!("Err sending webhook: {:?}", why); @@ -298,7 +353,7 @@ impl<'a> ExecuteWebhook<'a> { /// # Ok(()) /// # } /// ``` - pub fn flags(&mut self, flags: MessageFlags) -> &mut Self { + pub fn flags(mut self, flags: MessageFlags) -> Self { self.flags = Some(flags); self } diff --git a/src/builder/get_messages.rs b/src/builder/get_messages.rs index 6aa7da2b0e7..9ecde79e0a8 100644 --- a/src/builder/get_messages.rs +++ b/src/builder/get_messages.rs @@ -1,97 +1,116 @@ -use crate::model::id::MessageId; +#[cfg(feature = "http")] +use std::fmt::Write; + +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; /// Builds a request to the API to retrieve messages. /// -/// This can have 2 different sets of parameters. The first set is around where -/// to get the messages: +/// This accepts 2 types of parameters. The first type filters messages based on Id, and is set by +/// one of the following: /// /// - [`Self::after`] /// - [`Self::around`] /// - [`Self::before`] /// -/// These can not be mixed, and the first in the list alphabetically will be -/// used. If one is not specified, `most_recent` will be used. +/// These are mutually exclusive, and override each other if called sequentially. If one is not +/// specified, messages are simply sorted by most recent. /// -/// The fourth parameter is to specify the number of messages to retrieve. This -/// does not _need_ to be called and defaults to a value of 50. +/// The other parameter specifies the number of messages fo retrieve. This is _optional_, and +/// defaults to 50 if not specified. /// -/// This should be used only for retrieving messages; see -/// [`GuildChannel::messages`] for examples. +/// See [`GuildChannel::messages`] for more examples. /// /// # Examples /// -/// Creating a [`GetMessages`] builder to retrieve the first 25 messages after the -/// message with an Id of `158339864557912064`: +/// Creating a [`GetMessages`] builder to retrieve the first 25 messages after the message with an +/// Id of `158339864557912064`: /// /// ```rust,no_run /// # use serenity::http::Http; /// # /// # async fn run() -> Result<(), Box> { /// # let http = Http::new("token"); +/// use serenity::builder::GetMessages; /// use serenity::model::id::{ChannelId, MessageId}; /// /// // you can then pass it into a function which retrieves messages: /// let channel_id = ChannelId::new(81384788765712384); /// -/// let _messages = channel_id -/// .messages(&http, |retriever| retriever.after(MessageId::new(158339864557912064)).limit(25)) -/// .await?; +/// let builder = GetMessages::default().after(MessageId::new(158339864557912064)).limit(25); +/// let _messages = channel_id.messages(&http, builder).await?; /// # Ok(()) /// # } /// ``` -/// -/// [`GuildChannel::messages`]: crate::model::channel::GuildChannel::messages #[derive(Clone, Copy, Debug, Default)] +#[must_use] pub struct GetMessages { pub search_filter: Option, pub limit: Option, } impl GetMessages { - /// Indicates to retrieve the messages after a specific message, given by - /// its Id. - #[inline] - pub fn after>(&mut self, message_id: M) -> &mut Self { - self._after(message_id.into()); - self - } + /// Gets messages from the channel. + /// + /// **Note**: If the user does not have the [Read Message History] permission, returns an empty + /// [`Vec`]. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY + #[cfg(feature = "http")] + pub async fn execute( + self, + http: impl AsRef, + channel_id: ChannelId, + ) -> Result> { + let mut query = "?".to_string(); + if let Some(limit) = self.limit { + write!(query, "limit={}", limit)?; + } - fn _after(&mut self, message_id: MessageId) { - self.search_filter = Some(SearchFilter::After(message_id)); - } + if let Some(filter) = self.search_filter { + match filter { + SearchFilter::After(after) => write!(query, "&after={}", after)?, + SearchFilter::Around(around) => write!(query, "&around={}", around)?, + SearchFilter::Before(before) => write!(query, "&before={}", before)?, + } + } - /// Indicates to retrieve the messages _around_ a specific message in either - /// direction (before+after) the given message. - #[inline] - pub fn around>(&mut self, message_id: M) -> &mut Self { - self._around(message_id.into()); - self + http.as_ref().get_messages(channel_id.into(), &query).await } - fn _around(&mut self, message_id: MessageId) { - self.search_filter = Some(SearchFilter::Around(message_id)); + /// Indicates to retrieve the messages after a specific message, given its Id. + pub fn after(mut self, message_id: impl Into) -> Self { + self.search_filter = Some(SearchFilter::After(message_id.into())); + self } - /// Indicates to retrieve the messages before a specific message, given by - /// its Id. - #[inline] - pub fn before>(&mut self, message_id: M) -> &mut Self { - self._before(message_id.into()); + /// Indicates to retrieve the messages _around_ a specific message, in other words in either + /// direction from the message in time. + pub fn around(mut self, message_id: impl Into) -> Self { + self.search_filter = Some(SearchFilter::Around(message_id.into())); self } - fn _before(&mut self, message_id: MessageId) { - self.search_filter = Some(SearchFilter::Before(message_id)); + /// Indicates to retrieve the messages before a specific message, given its Id. + pub fn before(mut self, message_id: impl Into) -> Self { + self.search_filter = Some(SearchFilter::Before(message_id.into())); + self } /// The maximum number of messages to retrieve for the query. /// /// If this is not specified, a default value of 50 is used. /// - /// **Note**: This field is capped to 100 messages due to a Discord - /// limitation. If an amount larger than 100 is supplied, it will be - /// reduced. - pub fn limit(&mut self, limit: u8) -> &mut Self { + /// **Note**: This field is capped to 100 messages due to a Discord limitation. If an amount + /// larger than 100 is supplied, it will be truncated. + pub fn limit(mut self, limit: u8) -> Self { self.limit = Some(limit.min(100)); self } diff --git a/src/framework/standard/help_commands.rs b/src/framework/standard/help_commands.rs index 3587c5cf4a7..cae475984cd 100644 --- a/src/framework/standard/help_commands.rs +++ b/src/framework/standard/help_commands.rs @@ -76,11 +76,11 @@ use super::{ }; #[cfg(all(feature = "cache", feature = "http"))] use crate::{ - builder, + builder::{CreateEmbed, CreateMessage}, cache::Cache, client::Context, framework::standard::CommonOptions, - http::Http, + http::CacheHttp, model::channel::Message, model::id::{ChannelId, UserId}, utils::Colour, @@ -1012,7 +1012,7 @@ fn flatten_group_to_plain_string( /// Sends an embed listing all groups with their commands. #[cfg(all(feature = "cache", feature = "http"))] async fn send_grouped_commands_embed( - http: impl AsRef, + cache_http: impl CacheHttp, help_options: &HelpOptions, channel_id: ChannelId, help_description: &str, @@ -1022,105 +1022,94 @@ async fn send_grouped_commands_embed( // creating embed outside message builder since flatten_group_to_string // may return an error. - let mut embed = builder::CreateEmbed::default(); - embed.colour(colour); - embed.description(help_description); + let mut embed = CreateEmbed::default().colour(colour).description(help_description); for group in groups { let mut embed_text = String::default(); flatten_group_to_string(&mut embed_text, group, 0, help_options)?; - embed.field(group.name, &embed_text, true); + embed = embed.field(group.name, &embed_text, true); } - channel_id.send_message(&http, |m| m.set_embed(embed)).await + let builder = CreateMessage::default().embed(embed); + channel_id.send_message(cache_http, builder).await } /// Sends embed showcasing information about a single command. #[cfg(all(feature = "cache", feature = "http"))] async fn send_single_command_embed( - http: impl AsRef, + cache_http: impl CacheHttp, help_options: &HelpOptions, channel_id: ChannelId, command: &Command<'_>, colour: Colour, ) -> Result { - channel_id - .send_message(&http, |m| { - m.embed(|embed| { - embed.title(command.name); - embed.colour(colour); - - if let Some(desc) = command.description { - embed.description(desc); - } + let mut embed = CreateEmbed::default().title(command.name).colour(colour); - if let Some(usage) = command.usage { - let full_usage_text = if let Some(first_prefix) = command.group_prefixes.first() - { - format!("`{} {} {}`", first_prefix, command.name, usage) - } else { - format!("`{} {}`", command.name, usage) - }; + if let Some(desc) = command.description { + embed = embed.description(desc); + } - embed.field(help_options.usage_label, &full_usage_text, true); - } + if let Some(usage) = command.usage { + let full_usage_text = if let Some(first_prefix) = command.group_prefixes.first() { + format!("`{} {} {}`", first_prefix, command.name, usage) + } else { + format!("`{} {}`", command.name, usage) + }; - if !command.usage_sample.is_empty() { - let full_example_text = if let Some(first_prefix) = - command.group_prefixes.first() - { - let format_example = - |example| format!("`{} {} {}`\n", first_prefix, command.name, example); - command.usage_sample.iter().map(format_example).collect::() - } else { - let format_example = |example| format!("`{} {}`\n", command.name, example); - command.usage_sample.iter().map(format_example).collect::() - }; - embed.field(help_options.usage_sample_label, &full_example_text, true); - } + embed = embed.field(help_options.usage_label, &full_usage_text, true); + } + + if !command.usage_sample.is_empty() { + let full_example_text = if let Some(first_prefix) = command.group_prefixes.first() { + let format_example = + |example| format!("`{} {} {}`\n", first_prefix, command.name, example); + command.usage_sample.iter().map(format_example).collect::() + } else { + let format_example = |example| format!("`{} {}`\n", command.name, example); + command.usage_sample.iter().map(format_example).collect::() + }; + embed = embed.field(help_options.usage_sample_label, &full_example_text, true); + } - embed.field(help_options.grouped_label, command.group_name, true); + embed = embed.field(help_options.grouped_label, command.group_name, true); - if !command.aliases.is_empty() { - embed.field( - help_options.aliases_label, - &format!("`{}`", command.aliases.join("`, `")), - true, - ); - } + if !command.aliases.is_empty() { + embed = embed.field( + help_options.aliases_label, + &format!("`{}`", command.aliases.join("`, `")), + true, + ); + } - if !help_options.available_text.is_empty() && !command.availability.is_empty() { - embed.field(help_options.available_text, command.availability, true); - } + if !help_options.available_text.is_empty() && !command.availability.is_empty() { + embed = embed.field(help_options.available_text, command.availability, true); + } - if !command.checks.is_empty() { - embed.field( - help_options.checks_label, - &format!("`{}`", command.checks.join("`, `")), - true, - ); - } + if !command.checks.is_empty() { + embed = embed.field( + help_options.checks_label, + &format!("`{}`", command.checks.join("`, `")), + true, + ); + } - if !command.sub_commands.is_empty() { - embed.field( - help_options.sub_commands_label, - &format!("`{}`", command.sub_commands.join("`, `")), - true, - ); - } + if !command.sub_commands.is_empty() { + embed = embed.field( + help_options.sub_commands_label, + &format!("`{}`", command.sub_commands.join("`, `")), + true, + ); + } - embed - }); - m - }) - .await + let builder = CreateMessage::default().embed(embed); + channel_id.send_message(cache_http, builder).await } /// Sends embed listing commands that are similar to the sent one. #[cfg(all(feature = "cache", feature = "http"))] async fn send_suggestion_embed( - http: impl AsRef, + cache_http: impl CacheHttp, channel_id: ChannelId, help_description: &str, suggestions: &Suggestions, @@ -1128,18 +1117,22 @@ async fn send_suggestion_embed( ) -> Result { let text = help_description.replace("{}", &suggestions.join("`, `")); - channel_id.send_message(&http, |m| m.embed(|e| e.colour(colour).description(text))).await + let embed = CreateEmbed::default().colour(colour).description(text); + let builder = CreateMessage::default().embed(embed); + channel_id.send_message(cache_http, builder).await } /// Sends an embed explaining fetching commands failed. #[cfg(all(feature = "cache", feature = "http"))] async fn send_error_embed( - http: impl AsRef, + cache_http: impl CacheHttp, channel_id: ChannelId, input: &str, colour: Colour, ) -> Result { - channel_id.send_message(&http, |m| m.embed(|e| e.colour(colour).description(input))).await + let embed = CreateEmbed::default().colour(colour).description(input); + let builder = CreateMessage::default().embed(embed); + channel_id.send_message(cache_http, builder).await } /// Posts an embed showing each individual command group and its commands. diff --git a/src/http/client.rs b/src/http/client.rs index b6eea26f43c..0d27b91c3f0 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -865,19 +865,21 @@ impl Http { /// **Note**: Requires the [Manage Emojis and Stickers] permission. /// /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS - // Manual async fn to decorate the lifetime manually, avoiding a compiler bug - pub fn create_sticker<'a>( - &'a self, + // We take a `Vec<(String, String)>` rather than `Vec<(&'static str, String)>` to avoid a compiler + // bug around `async fn` and lifetime unification. TODO: change this back once MSRV is on 1.58. + // Relevant issue: https://github.com/rust-lang/rust/issues/63033 + pub async fn create_sticker<'a>( + &self, guild_id: u64, - map: Vec<(Cow<'static, str>, Cow<'static, str>)>, + map: Vec<(String, String)>, file: impl Into>, audit_log_reason: Option<&str>, - ) -> impl std::future::Future> + 'a { + ) -> Result { self.fire(Request { body: None, multipart: Some(Multipart { files: vec![file.into()], - fields: map, + fields: map.into_iter().map(|(k, v)| (k.into(), v.into())).collect(), payload_json: None, }), headers: audit_log_reason.map(reason_into_header), @@ -885,6 +887,7 @@ impl Http { guild_id, }, }) + .await } /// Creates a webhook for the given [channel][`GuildChannel`]'s Id, passing in @@ -2173,20 +2176,19 @@ impl Http { .await } - /// Executes a webhook, posting a [`Message`] in the webhook's associated - /// [`Channel`]. + /// Executes a webhook, posting a [`Message`] in the webhook's associated [`Channel`]. /// /// This method does _not_ require authentication. /// /// If `thread_id` is not `None`, then the message will be sent to the thread in the webhook's /// associated [`Channel`] with the corresponding Id, which will be automatically unarchived. /// - /// Pass `true` to `wait` to wait for server confirmation of the message sending - /// before receiving a response. From the [Discord docs]: + /// Pass `true` to `wait` to wait for server confirmation of the message sending before + /// receiving a response. From the [Discord docs]: /// - /// > waits for server confirmation of message send before response, and returns - /// > the created message body (defaults to false; when false a message that is - /// > not saved does not return an error) + /// > waits for server confirmation of message send before response, and returns the created + /// > message body (defaults to false; when false a message that is not saved does not return + /// > an error) /// /// The map can _optionally_ contain the following data: /// @@ -2199,9 +2201,9 @@ impl Http { /// - `content`: The content of the message. /// - `embeds`: An array of rich embeds. /// - /// **Note**: For embed objects, all fields are registered by Discord except for - /// `height`, `provider`, `proxy_url`, `type` (it will always be `rich`), - /// `video`, and `width`. The rest will be determined by Discord. + /// **Note**: For embed objects, all fields are registered by Discord except for `height`, + /// `provider`, `proxy_url`, `type` (it will always be `rich`), `video`, and `width`. The rest + /// will be determined by Discord. /// /// # Examples /// @@ -2259,9 +2261,7 @@ impl Http { /// /// # Errors /// - /// Returns an - /// [`HttpError::UnsuccessfulRequest(ErrorResponse)`][`HttpError::UnsuccessfulRequest`] - /// if the files are too large to send. + /// Returns an [`HttpError::UnsuccessfulRequest`] if the files are too large to send. pub async fn execute_webhook_with_files<'a>( &self, webhook_id: u64, diff --git a/src/model/application/command.rs b/src/model/application/command.rs index 2df52f809b9..cccc1c2731a 100644 --- a/src/model/application/command.rs +++ b/src/model/application/command.rs @@ -81,8 +81,7 @@ pub struct Command { #[cfg(feature = "http")] impl Command { - /// Creates a global [`Command`], - /// overriding an existing one with the same name if it exists. + /// Create a global [`Command`], overriding an existing one with the same name if it exists. /// /// When a created [`Command`] is used, the [`InteractionCreate`] event will be emitted. /// @@ -102,13 +101,13 @@ impl Command { /// # /// # async fn run() { /// # let http = Arc::new(Http::new("token")); + /// use serenity::builder::CreateApplicationCommand; /// use serenity::model::application::command::Command; /// use serenity::model::id::ApplicationId; /// - /// let _ = Command::create_global_application_command(&http, |command| { - /// command.name("ping").description("A simple ping command") - /// }) - /// .await; + /// let builder = + /// CreateApplicationCommand::default().name("ping").description("A simple ping command"); + /// let _ = Command::create_global_application_command(&http, builder).await; /// # } /// ``` /// @@ -120,80 +119,62 @@ impl Command { /// # /// # async fn run() { /// # let http = Arc::new(Http::new("token")); + /// use serenity::builder::{ + /// CreateApplicationCommand, + /// CreateApplicationCommandOption as CreateOption, + /// }; /// use serenity::model::application::command::{Command, CommandOptionType}; /// use serenity::model::id::ApplicationId; /// - /// let _ = Command::create_global_application_command(&http, |command| { - /// command.name("echo").description("Makes the bot send a message").create_option(|option| { - /// option + /// let builder = CreateApplicationCommand::default() + /// .name("echo") + /// .description("Makes the bot send a message") + /// .add_option( + /// CreateOption::default() /// .name("message") /// .description("The message to send") /// .kind(CommandOptionType::String) - /// .required(true) - /// }) - /// }) - /// .await; + /// .required(true), + /// ); + /// let _ = Command::create_global_application_command(&http, builder).await; /// # } /// ``` /// /// # Errors /// - /// May return an [`Error::Http`] if the [`Command`] is illformed, - /// such as if more than 10 [`choices`] are set. See the [API Docs] for further details. - /// - /// Can also return an [`Error::Json`] if there is an error in deserializing - /// the response. + /// See [`CreateApplicationCommand::execute`] for a list of possible errors. /// /// [`InteractionCreate`]: crate::client::EventHandler::interaction_create - /// [API Docs]: https://discord.com/developers/docs/interactions/slash-commands - /// [`choices`]: CommandOption::choices - pub async fn create_global_application_command( + pub async fn create_global_application_command( http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, - { - let map = Command::build_application_command(f); - http.as_ref().create_global_application_command(&map).await + builder: CreateApplicationCommand, + ) -> Result { + builder.execute(http, None, None).await } - /// Overrides all global application commands. - /// - /// [`create_global_application_command`]: Self::create_global_application_command + /// Override all global application commands. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn set_global_application_commands( + /// See [`CreateApplicationCommands::execute`] for a list of possible errors. + pub async fn set_global_application_commands( http: impl AsRef, - f: F, - ) -> Result> - where - F: FnOnce(&mut CreateApplicationCommands) -> &mut CreateApplicationCommands, - { - let mut array = CreateApplicationCommands::default(); - - f(&mut array); - - http.as_ref().create_global_application_commands(&array).await + builder: CreateApplicationCommands, + ) -> Result> { + builder.execute(http, None).await } - /// Edits a global command by its Id. + /// Edit a global command, given its Id. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn edit_global_application_command( + /// See [`CreateApplicationCommand::execute`] for a list of possible errors. + pub async fn edit_global_application_command( http: impl AsRef, command_id: CommandId, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, - { - let map = Command::build_application_command(f); - http.as_ref().edit_global_application_command(command_id.into(), &map).await + builder: CreateApplicationCommand, + ) -> Result { + builder.execute(http, None, Some(command_id)).await } /// Gets all global commands. @@ -230,19 +211,6 @@ impl Command { } } -#[cfg(feature = "http")] -impl Command { - #[inline] - pub(crate) fn build_application_command(f: F) -> CreateApplicationCommand - where - F: FnOnce(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, - { - let mut create_application_command = CreateApplicationCommand::default(); - f(&mut create_application_command); - create_application_command - } -} - enum_number! { /// The type of an application command. /// diff --git a/src/model/application/interaction/application_command.rs b/src/model/application/interaction/application_command.rs index 0556550795d..ff0de8c9200 100644 --- a/src/model/application/interaction/application_command.rs +++ b/src/model/application/interaction/application_command.rs @@ -10,6 +10,7 @@ use serde_value::{DeserializerError, Value}; use crate::builder::{ CreateAutocompleteResponse, CreateInteractionResponse, + CreateInteractionResponseData, CreateInteractionResponseFollowup, EditInteractionResponse, }; @@ -92,46 +93,15 @@ impl ApplicationCommandInteraction { /// /// # Errors /// - /// Returns an [`Error::Model`] if the message content is too long. - /// May also return an [`Error::Http`] if the API returns an error, - /// or an [`Error::Json`] if there is an error in deserializing the - /// API response. - pub async fn create_interaction_response<'a, F>( + /// Returns an [`Error::Model`] if the message content is too long. May also return an + /// [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is an error in + /// deserializing the API response. + pub async fn create_interaction_response<'a>( &self, http: impl AsRef, - f: F, - ) -> Result<()> - where - for<'b> F: - FnOnce(&'b mut CreateInteractionResponse<'a>) -> &'b mut CreateInteractionResponse<'a>, - { - let mut interaction_response = CreateInteractionResponse::default(); - f(&mut interaction_response); - self._create_interaction_response(http.as_ref(), interaction_response).await - } - - async fn _create_interaction_response<'a>( - &self, - http: &Http, - mut interaction_response: CreateInteractionResponse<'a>, + builder: CreateInteractionResponse<'a>, ) -> Result<()> { - let files = interaction_response - .data - .as_mut() - .map_or_else(Vec::new, |d| std::mem::take(&mut d.files)); - - if files.is_empty() { - http.create_interaction_response(self.id.get(), &self.token, &interaction_response) - .await - } else { - http.create_interaction_response_with_files( - self.id.get(), - &self.token, - &interaction_response, - files, - ) - .await - } + builder.execute(http, self.id, &self.token).await } /// Creates a response to an autocomplete interaction. @@ -139,55 +109,29 @@ impl ApplicationCommandInteraction { /// # Errors /// /// Returns an [`Error::Http`] if the API returns an error. - /// - /// [`Error::Http`]: crate::error::Error::Http - pub async fn create_autocomplete_response(&self, http: impl AsRef, f: F) -> Result<()> - where - F: FnOnce(&mut CreateAutocompleteResponse) -> &mut CreateAutocompleteResponse, - { - #[derive(Serialize)] - struct AutocompleteResponse { - data: CreateAutocompleteResponse, - #[serde(rename = "type")] - kind: InteractionResponseType, - } - - let mut response = CreateAutocompleteResponse::default(); - f(&mut response); - - let map = AutocompleteResponse { - data: response, - kind: InteractionResponseType::Autocomplete, - }; - - http.as_ref().create_interaction_response(self.id.get(), &self.token, &map).await + pub async fn create_autocomplete_response( + &self, + http: impl AsRef, + builder: CreateAutocompleteResponse, + ) -> Result<()> { + builder.execute(http, self.id, &self.token).await } - /// Edits the initial interaction response. - /// - /// `application_id` will usually be the bot's [`UserId`], except in cases of bots being very old. - /// - /// Refer to Discord's docs for Edit Webhook Message for field information. + /// Edits the initial interaction response. Does not work for ephemeral messages. /// /// **Note**: Message contents must be under 2000 unicode code points. /// /// # Errors /// - /// Returns [`Error::Model`] if the edited content is too long. - /// May also return [`Error::Http`] if the API returns an error, - /// or an [`Error::Json`] if there is an error deserializing the response. + /// Returns an [`Error::Model`] if the message content is too long. May also return an + /// [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is an error in + /// deserializing the API response. pub async fn edit_original_interaction_response( &self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut EditInteractionResponse) -> &mut EditInteractionResponse, - { - let mut interaction_response = EditInteractionResponse::default(); - f(&mut interaction_response); - - http.as_ref().edit_original_interaction_response(&self.token, &interaction_response).await + builder: EditInteractionResponse, + ) -> Result { + builder.execute(http, &self.token).await } /// Deletes the initial interaction response. @@ -208,36 +152,15 @@ impl ApplicationCommandInteraction { /// /// # Errors /// - /// Will return [`Error::Model`] if the content is too long. - /// May also return [`Error::Http`] if the API returns an error, - /// or a [`Error::Json`] if there is an error in deserializing the response. - pub async fn create_followup_message<'a, F>( + /// Returns [`Error::Model`] if the content is too long. May also return [`Error::Http`] if the + /// API returns an error, or [`Error::Json`] if there is an error in deserializing the + /// response. + pub async fn create_followup_message<'a>( &self, http: impl AsRef, - f: F, - ) -> Result - where - for<'b> F: FnOnce( - &'b mut CreateInteractionResponseFollowup<'a>, - ) -> &'b mut CreateInteractionResponseFollowup<'a>, - { - let mut interaction_response = CreateInteractionResponseFollowup::default(); - f(&mut interaction_response); - self._create_followup_message(http.as_ref(), interaction_response).await - } - - async fn _create_followup_message<'a>( - &self, - http: &Http, - mut interaction_response: CreateInteractionResponseFollowup<'a>, + builder: CreateInteractionResponseFollowup<'a>, ) -> Result { - let files = std::mem::take(&mut interaction_response.files); - - if files.is_empty() { - http.create_followup_message(&self.token, &interaction_response).await - } else { - http.create_followup_message_with_files(&self.token, &interaction_response, files).await - } + builder.execute(http, None, &self.token).await } /// Edits a followup response to the response sent. @@ -246,33 +169,16 @@ impl ApplicationCommandInteraction { /// /// # Errors /// - /// Will return [`Error::Model`] if the content is too long. - /// May also return [`Error::Http`] if the API returns an error, - /// or a [`Error::Json`] if there is an error in deserializing the response. - pub async fn edit_followup_message<'a, F, M: Into>( + /// Returns [`Error::Model`] if the content is too long. May also return [`Error::Http`] if the + /// API returns an error, or [`Error::Json`] if there is an error in deserializing the + /// response. + pub async fn edit_followup_message<'a>( &self, http: impl AsRef, - message_id: M, - f: F, - ) -> Result - where - for<'b> F: FnOnce( - &'b mut CreateInteractionResponseFollowup<'a>, - ) -> &'b mut CreateInteractionResponseFollowup<'a>, - { - let mut builder = CreateInteractionResponseFollowup::default(); - f(&mut builder); - - let http = http.as_ref(); - let message_id = message_id.into().into(); - let files = std::mem::take(&mut builder.files); - - if files.is_empty() { - http.edit_followup_message(&self.token, message_id, &builder).await - } else { - http.edit_followup_message_and_attachments(&self.token, message_id, &builder, files) - .await - } + message_id: impl Into, + builder: CreateInteractionResponseFollowup<'a>, + ) -> Result { + builder.execute(http, Some(message_id.into()), &self.token).await } /// Deletes a followup message. @@ -303,18 +209,16 @@ impl ApplicationCommandInteraction { http.as_ref().get_followup_message(&self.token, message_id.into().into()).await } - /// Helper function to defer an interaction + /// Helper function to defer an interaction. /// /// # Errors /// - /// May also return an [`Error::Http`] if the API returns an error, - /// or an [`Error::Json`] if there is an error in deserializing the - /// API response. + /// Returns an [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is + /// an error in deserializing the API response. pub async fn defer(&self, http: impl AsRef) -> Result<()> { - self.create_interaction_response(http, |f| { - f.kind(InteractionResponseType::DeferredChannelMessageWithSource) - }) - .await + let builder = CreateInteractionResponse::default() + .kind(InteractionResponseType::DeferredChannelMessageWithSource); + self.create_interaction_response(http, builder).await } /// Helper function to defer an interaction ephemerally @@ -325,10 +229,14 @@ impl ApplicationCommandInteraction { /// or an [`Error::Json`] if there is an error in deserializing the /// API response. pub async fn defer_ephemeral(&self, http: impl AsRef) -> Result<()> { - self.create_interaction_response(http, |f| { - f.kind(InteractionResponseType::DeferredChannelMessageWithSource) - .interaction_response_data(|f| f.ephemeral(true)) - }) + self.create_interaction_response( + http, + CreateInteractionResponse::default() + .kind(InteractionResponseType::DeferredChannelMessageWithSource) + .interaction_response_data( + CreateInteractionResponseData::default().ephemeral(true), + ), + ) .await } } diff --git a/src/model/application/interaction/message_component.rs b/src/model/application/interaction/message_component.rs index e419c7ffd85..fd2a3d372c3 100644 --- a/src/model/application/interaction/message_component.rs +++ b/src/model/application/interaction/message_component.rs @@ -79,69 +79,32 @@ impl MessageComponentInteraction { /// /// # Errors /// - /// Returns an [`Error::Model`] if the message content is too long. - /// May also return an [`Error::Http`] if the API returns an error, - /// or an [`Error::Json`] if there is an error in deserializing the - /// API response. - pub async fn create_interaction_response<'a, F>( + /// Returns an [`Error::Model`] if the message content is too long. May also return an + /// [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is an error in + /// deserializing the API response. + pub async fn create_interaction_response<'a>( &self, http: impl AsRef, - f: F, - ) -> Result<()> - where - for<'b> F: - FnOnce(&'b mut CreateInteractionResponse<'a>) -> &'b mut CreateInteractionResponse<'a>, - { - let mut interaction_response = CreateInteractionResponse::default(); - f(&mut interaction_response); - - let http = http.as_ref(); - let files = interaction_response - .data - .as_mut() - .map_or_else(Vec::new, |d| std::mem::take(&mut d.files)); - - if files.is_empty() { - http.create_interaction_response(self.id.get(), &self.token, &interaction_response) - .await - } else { - http.create_interaction_response_with_files( - self.id.get(), - &self.token, - &interaction_response, - files, - ) - .await - } + builder: CreateInteractionResponse<'a>, + ) -> Result<()> { + builder.execute(http, self.id, &self.token).await } - /// Edits the initial interaction response. - /// - /// `application_id` will usually be the bot's [`UserId`], except in cases of bots being very old. - /// - /// Refer to Discord's docs for Edit Webhook Message for field information. + /// Edits the initial interaction response. Does not work for ephemeral messages. /// /// **Note**: Message contents must be under 2000 unicode code points. /// - /// [`UserId`]: crate::model::id::UserId - /// /// # Errors /// - /// Returns [`Error::Model`] if the edited content is too long. - /// May also return [`Error::Http`] if the API returns an error, - /// or an [`Error::Json`] if there is an error deserializing the response. + /// Returns an [`Error::Model`] if the message content is too long. May also return an + /// [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is an error in + /// deserializing the API response. pub async fn edit_original_interaction_response( &self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut EditInteractionResponse) -> &mut EditInteractionResponse, - { - let mut interaction_response = EditInteractionResponse::default(); - f(&mut interaction_response); - - http.as_ref().edit_original_interaction_response(&self.token, &interaction_response).await + builder: EditInteractionResponse, + ) -> Result { + builder.execute(http, &self.token).await } /// Deletes the initial interaction response. @@ -162,23 +125,15 @@ impl MessageComponentInteraction { /// /// # Errors /// - /// Will return [`Error::Model`] if the content is too long. - /// May also return [`Error::Http`] if the API returns an error, - /// or a [`Error::Json`] if there is an error in deserializing the response. - pub async fn create_followup_message<'a, F>( + /// Returns [`Error::Model`] if the content is too long. May also return [`Error::Http`] if the + /// API returns an error, or [`Error::Json`] if there is an error in deserializing the + /// response. + pub async fn create_followup_message<'a>( &self, http: impl AsRef, - f: F, - ) -> Result - where - for<'b> F: FnOnce( - &'b mut CreateInteractionResponseFollowup<'a>, - ) -> &'b mut CreateInteractionResponseFollowup<'a>, - { - let mut interaction_response = CreateInteractionResponseFollowup::default(); - f(&mut interaction_response); - - http.as_ref().create_followup_message(&self.token, &interaction_response).await + builder: CreateInteractionResponseFollowup<'a>, + ) -> Result { + builder.execute(http, None, &self.token).await } /// Edits a followup response to the response sent. @@ -187,26 +142,16 @@ impl MessageComponentInteraction { /// /// # Errors /// - /// Will return [`Error::Model`] if the content is too long. - /// May also return [`Error::Http`] if the API returns an error, - /// or a [`Error::Json`] if there is an error in deserializing the response. - pub async fn edit_followup_message<'a, F, M: Into>( + /// Returns [`Error::Model`] if the content is too long. May also return [`Error::Http`] if the + /// API returns an error, or [`Error::Json`] if there is an error in deserializing the + /// response. + pub async fn edit_followup_message<'a>( &self, http: impl AsRef, - message_id: M, - f: F, - ) -> Result - where - for<'b> F: FnOnce( - &'b mut CreateInteractionResponseFollowup<'a>, - ) -> &'b mut CreateInteractionResponseFollowup<'a>, - { - let mut interaction_response = CreateInteractionResponseFollowup::default(); - f(&mut interaction_response); - - http.as_ref() - .edit_followup_message(&self.token, message_id.into().into(), &interaction_response) - .await + message_id: impl Into, + builder: CreateInteractionResponseFollowup<'a>, + ) -> Result { + builder.execute(http, Some(message_id.into()), &self.token).await } /// Deletes a followup message. @@ -237,20 +182,16 @@ impl MessageComponentInteraction { http.as_ref().get_followup_message(&self.token, message_id.into().into()).await } - /// Helper function to defer an interaction + /// Helper function to defer an interaction. /// /// # Errors /// - /// May also return an [`Error::Http`] if the API returns an error, - /// or an [`Error::Json`] if there is an error in deserializing the - /// API response. - /// - /// # Errors + /// Returns an [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is + /// an error in deserializing the API response. pub async fn defer(&self, http: impl AsRef) -> Result<()> { - self.create_interaction_response(http, |f| { - f.kind(InteractionResponseType::DeferredUpdateMessage) - }) - .await + let builder = CreateInteractionResponse::default() + .kind(InteractionResponseType::DeferredUpdateMessage); + self.create_interaction_response(http, builder).await } } diff --git a/src/model/application/interaction/modal.rs b/src/model/application/interaction/modal.rs index d1c70a69502..c2a12d4e62f 100644 --- a/src/model/application/interaction/modal.rs +++ b/src/model/application/interaction/modal.rs @@ -81,69 +81,32 @@ impl ModalSubmitInteraction { /// /// # Errors /// - /// Returns an [`Error::Model`] if the message content is too long. - /// May also return an [`Error::Http`] if the API returns an error, - /// or an [`Error::Json`] if there is an error in deserializing the - /// API response. - pub async fn create_interaction_response<'a, F>( + /// Returns an [`Error::Model`] if the message content is too long. May also return an + /// [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is an error in + /// deserializing the API response. + pub async fn create_interaction_response<'a>( &self, http: impl AsRef, - f: F, - ) -> Result<()> - where - for<'b> F: - FnOnce(&'b mut CreateInteractionResponse<'a>) -> &'b mut CreateInteractionResponse<'a>, - { - let mut interaction_response = CreateInteractionResponse::default(); - f(&mut interaction_response); - - let http = http.as_ref(); - let files = interaction_response - .data - .as_mut() - .map_or_else(Vec::new, |d| std::mem::take(&mut d.files)); - - if files.is_empty() { - http.create_interaction_response(self.id.get(), &self.token, &interaction_response) - .await - } else { - http.create_interaction_response_with_files( - self.id.get(), - &self.token, - &interaction_response, - files, - ) - .await - } + builder: CreateInteractionResponse<'a>, + ) -> Result<()> { + builder.execute(http, self.id, &self.token).await } - /// Edits the initial interaction response. - /// - /// `application_id` will usually be the bot's [`UserId`], except in cases of bots being very old. - /// - /// Refer to Discord's docs for Edit Webhook Message for field information. + /// Edits the initial interaction response. Does not work for ephemeral messages. /// /// **Note**: Message contents must be under 2000 unicode code points. /// - /// [`UserId`]: crate::model::id::UserId - /// /// # Errors /// - /// Returns [`Error::Model`] if the edited content is too long. - /// May also return [`Error::Http`] if the API returns an error, - /// or an [`Error::Json`] if there is an error deserializing the response. + /// Returns an [`Error::Model`] if the message content is too long. May also return an + /// [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is an error in + /// deserializing the API response. pub async fn edit_original_interaction_response( &self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut EditInteractionResponse) -> &mut EditInteractionResponse, - { - let mut interaction_response = EditInteractionResponse::default(); - f(&mut interaction_response); - - http.as_ref().edit_original_interaction_response(&self.token, &interaction_response).await + builder: EditInteractionResponse, + ) -> Result { + builder.execute(http, &self.token).await } /// Deletes the initial interaction response. @@ -164,23 +127,15 @@ impl ModalSubmitInteraction { /// /// # Errors /// - /// Will return [`Error::Model`] if the content is too long. - /// May also return [`Error::Http`] if the API returns an error, - /// or a [`Error::Json`] if there is an error in deserializing the response. - pub async fn create_followup_message<'a, F>( + /// Returns [`Error::Model`] if the content is too long. May also return [`Error::Http`] if the + /// API returns an error, or [`Error::Json`] if there is an error in deserializing the + /// response. + pub async fn create_followup_message<'a>( &self, http: impl AsRef, - f: F, - ) -> Result - where - for<'b> F: FnOnce( - &'b mut CreateInteractionResponseFollowup<'a>, - ) -> &'b mut CreateInteractionResponseFollowup<'a>, - { - let mut interaction_response = CreateInteractionResponseFollowup::default(); - f(&mut interaction_response); - - http.as_ref().create_followup_message(&self.token, &interaction_response).await + builder: CreateInteractionResponseFollowup<'a>, + ) -> Result { + builder.execute(http, None, &self.token).await } /// Edits a followup response to the response sent. @@ -189,26 +144,16 @@ impl ModalSubmitInteraction { /// /// # Errors /// - /// Will return [`Error::Model`] if the content is too long. - /// May also return [`Error::Http`] if the API returns an error, - /// or a [`Error::Json`] if there is an error in deserializing the response. - pub async fn edit_followup_message<'a, F, M: Into>( + /// Returns [`Error::Model`] if the content is too long. May also return [`Error::Http`] if the + /// API returns an error, or [`Error::Json`] if there is an error in deserializing the + /// response. + pub async fn edit_followup_message<'a>( &self, http: impl AsRef, - message_id: M, - f: F, - ) -> Result - where - for<'b> F: FnOnce( - &'b mut CreateInteractionResponseFollowup<'a>, - ) -> &'b mut CreateInteractionResponseFollowup<'a>, - { - let mut interaction_response = CreateInteractionResponseFollowup::default(); - f(&mut interaction_response); - - http.as_ref() - .edit_followup_message(&self.token, message_id.into().into(), &interaction_response) - .await + message_id: impl Into, + builder: CreateInteractionResponseFollowup<'a>, + ) -> Result { + builder.execute(http, Some(message_id.into()), &self.token).await } /// Deletes a followup message. @@ -224,18 +169,17 @@ impl ModalSubmitInteraction { ) -> Result<()> { http.as_ref().delete_followup_message(&self.token, message_id.into().into()).await } - /// Helper function to defer an interaction + + /// Helper function to defer an interaction. /// /// # Errors /// - /// May also return an [`Error::Http`] if the API returns an error, - /// or an [`Error::Json`] if there is an error in deserializing the - /// API response. + /// Returns an [`Error::Http`] if the API returns an error, or an [`Error::Json`] if there is + /// an error in deserializing the API response. pub async fn defer(&self, http: impl AsRef) -> Result<()> { - self.create_interaction_response(http, |f| { - f.kind(InteractionResponseType::DeferredUpdateMessage) - }) - .await + let builder = CreateInteractionResponse::default() + .kind(InteractionResponseType::DeferredUpdateMessage); + self.create_interaction_response(http, builder).await } } diff --git a/src/model/channel/channel_category.rs b/src/model/channel/channel_category.rs index ac07431c990..d4e747152e3 100644 --- a/src/model/channel/channel_category.rs +++ b/src/model/channel/channel_category.rs @@ -89,66 +89,61 @@ impl ChannelCategory { self.id.delete(&cache_http.http()).await.map(|_| ()) } - /// Modifies the category's settings, such as its position or name. + /// Edits the category's settings. /// - /// Refer to [`EditChannel`]s documentation for a full list of methods. + /// Refer to the documentation for [`EditChannel`] for a full list of methods. /// - /// **Note**: Requires the [Manage Channels] permission, - /// also requires the [Manage Roles] permission if modifying - /// permissions for the category. + /// **Note**: Requires the [Manage Channels] permission. Modifying permissions via + /// [`EditChannel::permissions`] also requires the [Manage Roles] permission. /// /// # Examples /// - /// Change a voice channels name and bitrate: + /// Change a category's name: /// /// ```rust,no_run + /// # use serenity::builder::EditChannel; + /// # use serenity::http::Http; + /// # use serenity::model::id::ChannelId; /// # async fn run() { - /// # use serenity::http::Http; - /// # use serenity::model::id::ChannelId; /// # let http = Http::new("token"); /// # let category = ChannelId::new(1234); - /// category.edit(&http, |c| c.name("test").bitrate(86400)).await; + /// let builder = EditChannel::default().name("test"); + /// category.edit(&http, builder).await; /// # } /// ``` /// /// # Errors /// - /// Returns [`Error::Http`] if an invalid value is set, - /// or if the current user lacks the necessary permissions. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS /// [Manage Roles]: Permissions::MANAGE_ROLES - pub async fn edit(&mut self, cache_http: impl CacheHttp, f: F) -> Result<()> - where - F: FnOnce(&mut EditChannel) -> &mut EditChannel, - { - let mut edit_channel = EditChannel::default(); - f(&mut edit_channel); + pub async fn edit(&mut self, cache_http: impl CacheHttp, builder: EditChannel) -> Result<()> { + let GuildChannel { + id, + guild_id, + parent_id, + position, + kind, + name, + nsfw, + permission_overwrites, + .. + } = self.id.edit(cache_http, builder).await?; - cache_http.http().edit_channel(self.id.get(), &edit_channel, None).await.map(|channel| { - let GuildChannel { - id, - guild_id, - parent_id, - position, - kind, - name, - nsfw, - permission_overwrites, - .. - } = channel; + *self = ChannelCategory { + id, + guild_id, + parent_id, + position, + kind, + name, + nsfw, + permission_overwrites, + }; - *self = ChannelCategory { - id, - guild_id, - parent_id, - position, - kind, - name, - nsfw, - permission_overwrites, - }; - }) + Ok(()) } #[inline] diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index f5117d10a9c..5a02b43e04b 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -1,6 +1,4 @@ #[cfg(feature = "model")] -use std::fmt::Write as _; -#[cfg(feature = "model")] use std::sync::Arc; #[cfg(feature = "model")] @@ -18,7 +16,6 @@ use crate::builder::{ EditStageInstance, EditThread, GetMessages, - SearchFilter, }; #[cfg(all(feature = "cache", feature = "model"))] use crate::cache::Cache; @@ -69,23 +66,29 @@ impl ChannelId { http.as_ref().broadcast_typing(self.get()).await } - /// Creates an invite leading to the given channel. + /// Creates an invite for the given channel. /// /// **Note**: Requires the [Create Instant Invite] permission. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. + /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE - pub async fn create_invite(&self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut CreateInvite) -> &mut CreateInvite, - { - let mut invite = CreateInvite::default(); - f(&mut invite); - - http.as_ref().create_invite(self.get(), &invite, None).await + pub async fn create_invite( + self, + cache_http: impl CacheHttp, + builder: CreateInvite, + ) -> Result { + builder + .execute( + cache_http, + self, + #[cfg(feature = "cache")] + None, + ) + .await } /// Creates a [permission overwrite][`PermissionOverwrite`] for either a @@ -289,84 +292,74 @@ impl ChannelId { .await } - /// Edits the settings of a [`Channel`], optionally setting new values. + /// Edits a channel's settings. /// - /// Refer to [`EditChannel`]'s documentation for its methods. + /// Refer to the documentation for [`EditChannel`] for a full list of methods. /// - /// Requires the [Manage Channel] permission. + /// **Note**: Requires the [Manage Channels] permission. Modifying permissions via + /// [`EditChannel::permissions`] also requires the [Manage Roles] permission. /// /// # Examples /// /// Change a voice channel's name and bitrate: /// /// ```rust,no_run - /// // assuming a `channel_id` has been bound - /// + /// # use serenity::builder::EditChannel; + /// # use serenity::http::Http; + /// # use serenity::model::id::ChannelId; /// # async fn run() { - /// # use serenity::http::Http; - /// # use serenity::model::id::ChannelId; /// # let http = Http::new("token"); /// # let channel_id = ChannelId::new(1234); - /// channel_id.edit(&http, |c| c.name("test").bitrate(64000)).await; + /// let builder = EditChannel::default().name("test").bitrate(64000); + /// channel_id.edit(&http, builder).await; /// # } /// ``` /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if an invalid value is set. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// - /// [Manage Channel]: Permissions::MANAGE_CHANNELS + /// [Manage Channels]: Permissions::MANAGE_CHANNELS + /// [Manage Roles]: Permissions::MANAGE_ROLES #[inline] - pub async fn edit(self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut EditChannel) -> &mut EditChannel, - { - let mut channel = EditChannel::default(); - f(&mut channel); - - http.as_ref().edit_channel(self.get(), &channel, None).await + pub async fn edit( + self, + cache_http: impl CacheHttp, + builder: EditChannel, + ) -> Result { + builder + .execute( + cache_http, + self, + #[cfg(feature = "cache")] + None, + ) + .await } /// Edits a [`Message`] in the channel given its Id. /// - /// Message editing preserves all unchanged message data. + /// Message editing preserves all unchanged message data, with some exceptions for embeds and + /// attachments. /// - /// Refer to the documentation for [`EditMessage`] for more information - /// regarding message restrictions and requirements. + /// **Note**: In most cases requires that the current user be the author of the message. /// - /// **Note**: Requires that the current user be the author of the message. + /// Refer to the documentation for [`EditMessage`] for information regarding content + /// restrictions and requirements. /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the [`the limit`], containing the number of unicode code points - /// over the limit. - /// - /// [`the limit`]: crate::builder::EditMessage::content + /// See [`EditMessage::execute`] for a list of possible errors, and their corresponding + /// reasons. #[inline] - pub async fn edit_message<'a, F>( + pub async fn edit_message<'a>( self, http: impl AsRef, message_id: impl Into, - f: F, - ) -> Result - where - F: for<'b> FnOnce(&'b mut EditMessage<'a>) -> &'b mut EditMessage<'a>, - { - let mut msg = EditMessage::default(); - f(&mut msg); - - if let Some(content) = &msg.content { - if let Some(length_over) = Message::overflow_length(content) { - return Err(Error::Model(ModelError::MessageTooLong(length_over))); - } - } - - let files = std::mem::take(&mut msg.files); - http.as_ref() - .edit_message_and_attachments(self.get(), message_id.into().get(), &msg, files) - .await + builder: EditMessage<'_>, + ) -> Result { + builder.execute(http, self, message_id.into()).await } /// Follows the News Channel @@ -473,34 +466,20 @@ impl ChannelId { /// Gets messages from the channel. /// - /// Refer to [`GetMessages`] for more information on how to use `builder`. - /// - /// **Note**: Returns an empty [`Vec`] if the current user - /// does not have the [Read Message History] permission. + /// **Note**: If the user does not have the [Read Message History] permission, returns an empty + /// [`Vec`]. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user does not have - /// permission to view the channel. + /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY - pub async fn messages(self, http: impl AsRef, builder: F) -> Result> - where - F: FnOnce(&mut GetMessages) -> &mut GetMessages, - { - let mut get_messages = GetMessages::default(); - builder(&mut get_messages); - let mut query = format!("?limit={}", get_messages.limit.unwrap_or(50)); - - if let Some(filter) = get_messages.search_filter { - match filter { - SearchFilter::After(after) => write!(query, "&after={}", after)?, - SearchFilter::Around(around) => write!(query, "&around={}", around)?, - SearchFilter::Before(before) => write!(query, "&before={}", before)?, - } - } - - http.as_ref().get_messages(self.get(), &query).await + pub async fn messages( + self, + http: impl AsRef, + builder: GetMessages, + ) -> Result> { + builder.execute(http, self).await } /// Streams over all the messages in a channel. @@ -644,27 +623,28 @@ impl ChannelId { /// Sends a message with just the given message content in the channel. /// + /// **Note**: Message content must be under 2000 unicode code points. + /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the above limit, containing the number of unicode code points - /// over the limit. + /// Returns a [`ModelError::MessageTooLong`] if the content length is over the above limit. See + /// [`CreateMessage::execute`] for more details. #[inline] - pub async fn say(self, http: impl AsRef, content: impl Into) -> Result { - self.send_message(&http, |m| m.content(content)).await + pub async fn say( + self, + cache_http: impl CacheHttp, + content: impl Into, + ) -> Result { + let builder = CreateMessage::default().content(content); + self.send_message(cache_http, builder).await } - /// Sends file(s) along with optional message contents. The filename _must_ - /// be specified. - /// - /// Message contents may be passed by using the [`CreateMessage::content`] - /// method. + /// Sends file(s) along with optional message contents. The filename _must_ be specified. /// - /// The [Attach Files] and [Send Messages] permissions are required. - /// - /// **Note**: Message contents must be under 2000 unicode code points, and embeds must be under - /// 6000 unicode code points. + /// Message contents may be passed using the `builder` argument. /// + /// Refer to the documentation for [`CreateMessage`] for information regarding content + /// restrictions and requirements. /// /// # Examples /// @@ -676,13 +656,15 @@ impl ChannelId { /// # /// # async fn run() { /// # let http = Arc::new(Http::new("token")); + /// use serenity::builder::CreateMessage; /// use serenity::model::id::ChannelId; /// /// let channel_id = ChannelId::new(7); /// /// let paths = vec!["/path/to/file.jpg", "path/to/file2.jpg"]; /// - /// let _ = channel_id.send_files(&http, paths, |m| m.content("a file")).await; + /// let builder = CreateMessage::default().content("some files"); + /// let _ = channel_id.send_files(&http, paths, builder).await; /// # } /// ``` /// @@ -694,6 +676,7 @@ impl ChannelId { /// # /// # async fn run() -> Result<(), Box> { /// # let http = Arc::new(Http::new("token")); + /// use serenity::builder::CreateMessage; /// use serenity::model::id::ChannelId; /// use tokio::fs::File; /// @@ -704,83 +687,61 @@ impl ChannelId { /// /// let files = vec![(&f1, "my_file.jpg"), (&f2, "my_file2.jpg")]; /// - /// let _ = channel_id.send_files(&http, files, |m| m.content("a file")).await; + /// let builder = CreateMessage::default().content("some files"); + /// let _ = channel_id.send_files(&http, files, builder).await; /// # Ok(()) /// # } /// ``` /// /// # Errors /// - /// If the content of the message is over the above limit, then a - /// [`ModelError::MessageTooLong`] will be returned, containing the number - /// of unicode code points over the limit. - /// - /// Returns an - /// [`HttpError::UnsuccessfulRequest(ErrorResponse)`][`HttpError::UnsuccessfulRequest`] - /// if the file(s) are too large to send. + /// See [`CreateMessage::execute`] for a list of possible errors, and their corresponding + /// reasons. /// - /// [`HttpError::UnsuccessfulRequest`]: crate::http::HttpError::UnsuccessfulRequest - /// [`CreateMessage::content`]: crate::builder::CreateMessage::content - /// [Attach Files]: Permissions::ATTACH_FILES - /// [Send Messages]: Permissions::SEND_MESSAGES /// [`File`]: tokio::fs::File - pub async fn send_files<'a, F, T, It>( + pub async fn send_files<'a, T, It>( self, - http: impl AsRef, + cache_http: impl CacheHttp, files: It, - f: F, + builder: CreateMessage<'a>, ) -> Result where - for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>, T: Into>, It: IntoIterator, { - let mut builder = CreateMessage::default(); - http.as_ref().send_files(self.get(), files, f(&mut builder)).await + builder + .files(files) + .execute( + cache_http, + self, + #[cfg(feature = "cache")] + None, + ) + .await } /// Sends a message to the channel. /// - /// Refer to the documentation for [`CreateMessage`] for more information - /// regarding message restrictions and requirements. - /// - /// Requires the [Send Messages] permission. - /// - /// **Note**: Message contents must be under 2000 unicode code points. + /// Refer to the documentation for [`CreateMessage`] for information regarding content + /// restrictions and requirements. /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the above limit, containing the number of unicode code points - /// over the limit. - /// - /// Returns [`Error::Http`] if the current user lacks permission to - /// send a message in this channel. - /// - /// [Send Messages]: Permissions::SEND_MESSAGES - pub async fn send_message<'a, F>(self, http: impl AsRef, f: F) -> Result - where - for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>, - { - let mut create_message = CreateMessage::default(); - f(&mut create_message); - self._send_message(http.as_ref(), create_message).await - } - - async fn _send_message<'a>(self, http: &Http, mut msg: CreateMessage<'a>) -> Result { - let files = std::mem::take(&mut msg.files); - - let message = if files.is_empty() { - http.as_ref().send_message(self.get(), &msg).await? - } else { - http.as_ref().send_files(self.get(), files, &msg).await? - }; - - for reaction in msg.reactions { - self.create_reaction(&http, message.id, reaction).await?; - } - - Ok(message) + /// See [`CreateMessage::execute`] for a list of possible errors, and their corresponding + /// reasons. + pub async fn send_message<'a>( + self, + cache_http: impl CacheHttp, + builder: CreateMessage<'_>, + ) -> Result { + builder + .execute( + cache_http, + self, + #[cfg(feature = "cache")] + None, + ) + .await } /// Starts typing in the channel for an indefinite period of time. @@ -858,32 +819,17 @@ impl ChannelId { http.as_ref().get_channel_webhooks(self.get()).await } - /// Creates a webhook + /// Creates a webhook in the channel. /// /// # Errors /// - /// Returns a [`Error::Http`] if the current user lacks permission. - /// Returns a [`ModelError::NameTooShort`] if the name of the webhook is - /// under the limit of 2 characters. - /// Returns a [`ModelError::NameTooLong`] if the name of the webhook is - /// over the limit of 100 characters. - /// Returns a [`ModelError::InvalidChannelType`] if the channel type is not text. - pub async fn create_webhook(&self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut CreateWebhook) -> &mut CreateWebhook, - { - let mut builder = CreateWebhook::default(); - f(&mut builder); - - if let Some(name) = &builder.name { - if name.len() < 2 { - return Err(Error::Model(ModelError::NameTooShort)); - } else if name.len() > 100 { - return Err(Error::Model(ModelError::NameTooLong)); - } - } - - http.as_ref().create_webhook(self.get(), &builder, None).await + /// See [`CreateWebhook::execute`] for a detailed list of possible errors. + pub async fn create_webhook( + self, + cache_http: impl CacheHttp, + builder: CreateWebhook, + ) -> Result { + builder.execute(cache_http, self).await } /// Returns a builder which can be awaited to obtain a message or stream of messages in this channel. @@ -918,40 +864,31 @@ impl ChannelId { /// /// # Errors /// - /// Returns [`Error::Http`] if the channel is not a stage channel, - /// or if there is already a stage instance currently. - pub async fn create_stage_instance( - &self, - http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateStageInstance) -> &mut CreateStageInstance, - { - let mut instance = CreateStageInstance::default(); - f(&mut instance); - - http.as_ref().create_stage_instance(&instance).await + /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. + /// + /// Returns [`Error::Http`] if there is already a stage instance currently. + pub async fn create_stage_instance( + self, + cache_http: impl CacheHttp, + builder: CreateStageInstance, + ) -> Result { + builder.channel_id(self).execute(cache_http).await } - /// Edits a stage instance. + /// Edits the stage instance /// /// # Errors /// - /// Returns [`Error::Http`] if the channel is not a stage channel, - /// or if there is not stage instance currently. - pub async fn edit_stage_instance( - &self, - http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut EditStageInstance) -> &mut EditStageInstance, - { - let mut instance = EditStageInstance::default(); - f(&mut instance); - - http.as_ref().edit_stage_instance(self.get(), &instance).await + /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. + /// + /// Returns [`Error::Http`] if the channel is not a stage channel, or there is no stage + /// instance currently. + pub async fn edit_stage_instance( + self, + cache_http: impl CacheHttp, + builder: EditStageInstance, + ) -> Result { + builder.execute(cache_http, self).await } /// Edits a thread. @@ -959,14 +896,12 @@ impl ChannelId { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission. - pub async fn edit_thread(&self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut EditThread) -> &mut EditThread, - { - let mut instance = EditThread::default(); - f(&mut instance); - - http.as_ref().edit_thread(self.get(), &instance).await + pub async fn edit_thread( + self, + http: impl AsRef, + builder: EditThread, + ) -> Result { + builder.execute(http, self).await } /// Deletes a stage instance. @@ -983,40 +918,27 @@ impl ChannelId { /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. - pub async fn create_public_thread( - &self, + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + pub async fn create_public_thread( + self, http: impl AsRef, message_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateThread) -> &mut CreateThread, - { - let mut instance = CreateThread::default(); - f(&mut instance); - - http.as_ref().create_public_thread(self.get(), message_id.into().get(), &instance).await + builder: CreateThread, + ) -> Result { + builder.execute(http, self, Some(message_id.into())).await } /// Creates a private thread. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. - pub async fn create_private_thread( - &self, + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + pub async fn create_private_thread( + self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateThread) -> &mut CreateThread, - { - let mut instance = CreateThread::default(); - instance.kind(ChannelType::PrivateThread); - f(&mut instance); - - http.as_ref().create_private_thread(self.get(), &instance).await + builder: CreateThread, + ) -> Result { + builder.kind(ChannelType::PrivateThread).execute(http, self, None).await } /// Gets the thread members, if this channel is a thread. @@ -1203,16 +1125,11 @@ impl> MessagesIter { // If `self.before` is not set yet, we can use `.messages` to fetch // the last message after very first fetch from last. - self.buffer = self - .channel_id - .messages(&self.http, |b| { - if let Some(before) = self.before { - b.before(before); - } - - b.limit(grab_size) - }) - .await?; + let mut builder = GetMessages::default().limit(grab_size); + if let Some(before) = self.before { + builder = builder.before(before); + } + self.buffer = self.channel_id.messages(&self.http, builder).await?; self.buffer.reverse(); diff --git a/src/model/channel/embed.rs b/src/model/channel/embed.rs index fd970a737ab..936620e7e42 100644 --- a/src/model/channel/embed.rs +++ b/src/model/channel/embed.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "model")] -use crate::builder::CreateEmbed; use crate::model::Timestamp; #[cfg(feature = "utils")] use crate::utils::Colour; @@ -65,40 +63,6 @@ pub struct Embed { pub video: Option, } -#[cfg(feature = "model")] -impl Embed { - /// Creates a fake Embed, giving back a [`serde_json`] map. - /// - /// This should only be useful in conjunction with [`Webhook::execute`]. - /// - /// [`Webhook::execute`]: crate::model::webhook::Webhook::execute - /// - /// # Examples - /// - /// Create an embed: - /// - /// ```rust,no_run - /// use serenity::model::channel::Embed; - /// - /// let embed = Embed::fake(|e| { - /// e.title("Embed title").description("Making a basic embed").field( - /// "A field", - /// "Has some content.", - /// false, - /// ) - /// }); - /// ``` - #[inline] - pub fn fake(f: F) -> CreateEmbed - where - F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, - { - let mut create_embed = CreateEmbed::default(); - f(&mut create_embed); - create_embed - } -} - /// An author object in an embed. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure). diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index d1d4b2bf82c..2f2f4500ba5 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -157,7 +157,7 @@ impl GuildChannel { self.id.broadcast_typing(&http).await } - /// Creates an invite leading to the given channel. + /// Creates an invite for the given channel. /// /// **Note**: Requires the [Create Instant Invite] permission. /// @@ -166,36 +166,31 @@ impl GuildChannel { /// Create an invite that can only be used 5 times: /// /// ```rust,ignore - /// let invite = channel.create_invite(&context, |i| i.max_uses(5)).await; + /// let builder = CreateBuilder::default().max_uses(5); + /// let invite = channel.create_invite(&context, builder).await; /// ``` /// /// # Errors /// - /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] - /// if the current user does not have permission to create invites. - /// - /// Otherwise returns [`Error::Http`] if the current user lacks permission. + /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE #[inline] #[cfg(feature = "utils")] - pub async fn create_invite(&self, cache_http: impl CacheHttp, f: F) -> Result - where - F: FnOnce(&mut CreateInvite) -> &mut CreateInvite, - { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - utils::user_has_perms_cache( - cache, - self.id, - Some(self.guild_id), - Permissions::CREATE_INSTANT_INVITE, - )?; - } - } - - self.id.create_invite(cache_http.http(), f).await + pub async fn create_invite( + &self, + cache_http: impl CacheHttp, + builder: CreateInvite, + ) -> Result { + builder + .execute( + cache_http, + self.id, + #[cfg(feature = "cache")] + Some(self.guild_id), + ) + .await } /// Creates a [permission overwrite][`PermissionOverwrite`] for either a @@ -307,7 +302,7 @@ impl GuildChannel { #[cfg(feature = "cache")] { if let Some(cache) = cache_http.cache() { - utils::user_has_perms_cache( + crate::utils::user_has_perms_cache( cache, self.id, Some(self.guild_id), @@ -386,75 +381,70 @@ impl GuildChannel { self.id.delete_reaction(&http, message_id, user_id, reaction_type).await } - /// Modifies a channel's settings, such as its position or name. + /// Edits the channel's settings. + /// + /// Refer to the documentation for [`EditChannel`] for a full list of methods. /// - /// Refer to [`EditChannel`]s documentation for a full list of methods. + /// **Note**: Requires the [Manage Channels] permission. Modifying permissions via + /// [`EditChannel::permissions`] also requires the [Manage Roles] permission. /// /// # Examples /// /// Change a voice channels name and bitrate: /// - /// ```rust,ignore - /// channel.edit(&context, |c| c.name("test").bitrate(86400)).await; + /// ```rust,no_run + /// # use serenity::builder::EditChannel; + /// # use serenity::http::Http; + /// # use serenity::model::id::ChannelId; + /// # async fn run() { + /// # let http = Http::new("token"); + /// # let channel = ChannelId::new(1234); + /// let builder = EditChannel::default().name("test").bitrate(86400); + /// channel.edit(&http, builder).await; + /// # } /// ``` /// /// # Errors /// - /// If the `cache` is enabled, returns [ModelError::InvalidPermissions] - /// if the current user lacks permission to edit the channel. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// - /// Otherwise returns [`Error::Http`] if the current user lacks permission. - pub async fn edit(&mut self, cache_http: impl CacheHttp, f: F) -> Result<()> - where - F: FnOnce(&mut EditChannel) -> &mut EditChannel, - { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - utils::user_has_perms_cache( - cache, - self.id, - Some(self.guild_id), - Permissions::MANAGE_CHANNELS, - )?; - } - } - - let mut edit_channel = EditChannel::default(); - f(&mut edit_channel); - - *self = cache_http.http().edit_channel(self.id.get(), &edit_channel, None).await?; - + /// [Manage Channels]: Permissions::MANAGE_CHANNELS + /// [Manage Roles]: Permissions::MANAGE_ROLES + pub async fn edit(&mut self, cache_http: impl CacheHttp, builder: EditChannel) -> Result<()> { + *self = builder + .execute( + cache_http, + self.id, + #[cfg(feature = "cache")] + Some(self.guild_id), + ) + .await?; Ok(()) } /// Edits a [`Message`] in the channel given its Id. /// - /// Message editing preserves all unchanged message data. + /// Message editing preserves all unchanged message data, with some exceptions for embeds and + /// attachments. /// - /// Refer to the documentation for [`EditMessage`] for more information - /// regarding message restrictions and requirements. + /// **Note**: In most cases requires that the current user be the author of the message. /// - /// **Note**: Requires that the current user be the author of the message. + /// Refer to the documentation for [`EditMessage`] for information regarding content + /// restrictions and requirements. /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the [`the limit`], containing the number of unicode code points - /// over the limit. - /// - /// [`the limit`]: crate::builder::EditMessage::content + /// See [`EditMessage::execute`] for a list of possible errors, and their corresponding + /// reasons. #[inline] - pub async fn edit_message<'a, F>( + pub async fn edit_message<'a>( &self, http: impl AsRef, message_id: impl Into, - f: F, - ) -> Result - where - F: for<'b> FnOnce(&'b mut EditMessage<'a>) -> &'b mut EditMessage<'a>, - { - self.id.edit_message(&http, message_id, f).await + builder: EditMessage<'a>, + ) -> Result { + self.id.edit_message(http, message_id, builder).await } /// Edits a thread. @@ -462,21 +452,16 @@ impl GuildChannel { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission. - pub async fn edit_thread(&self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut EditThread) -> &mut EditThread, - { - self.id.edit_thread(http, f).await + pub async fn edit_thread(&mut self, http: impl AsRef, builder: EditThread) -> Result<()> { + *self = self.id.edit_thread(http, builder).await?; + Ok(()) } - /// Edits a voice state in a stage channel. Pass [`None`] for `user_id` to - /// edit the current user's voice state. + /// Edits the voice state of a given user in a stage channel. /// - /// Requires the [Mute Members] permission to suppress another user or - /// unsuppress the current user. This is not required if suppressing - /// the current user. - /// - /// Requires the [Request to Speak] permission. + /// **Note**: Requires the [Request to Speak] permission. Also requires the [Mute Members] + /// permission to suppress another user or unsuppress the current user. This is not required if + /// suppressing the current user. /// /// # Example /// @@ -492,41 +477,40 @@ impl GuildChannel { /// # let cache = Cache::default(); /// # let (channel_id, user_id) = (ChannelId::new(1), UserId::new(1)); /// # + /// use serenity::builder::EditVoiceState; /// use serenity::model::ModelError; /// /// // assuming the cache has been unlocked /// let channel = cache.guild_channel(channel_id).ok_or(ModelError::ItemMissing)?; /// - /// channel.edit_voice_state(&http, user_id, |v| v.suppress(false)).await?; + /// let builder = EditVoiceState::default().suppress(false); + /// channel.edit_voice_state(&http, user_id, builder).await?; /// # Ok(()) /// # } /// ``` /// /// # Errors /// - /// Returns a [`ModelError::InvalidChannelType`] if the channel type is not - /// stage. + /// If the `cache` is enabled, returns a [`ModelError::InvalidChannelType`] if the channel is + /// not a stage channel. + /// + /// Returns [`Error::Http`] if the user lacks permission, or if invalid data is given. /// - /// [Mute Members]: crate::model::permissions::Permissions::MUTE_MEMBERS - /// [Request to Speak]: crate::model::permissions::Permissions::REQUEST_TO_SPEAK - pub async fn edit_voice_state( + /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK + /// [Mute Members]: Permissions::MUTE_MEMBERS + pub async fn edit_voice_state( &self, - http: impl AsRef, + cache_http: impl CacheHttp, user_id: impl Into, - f: F, - ) -> Result<()> - where - F: FnOnce(&mut EditVoiceState) -> &mut EditVoiceState, - { - self._edit_voice_state(http, Some(user_id), f).await + builder: EditVoiceState, + ) -> Result<()> { + builder.execute(cache_http, self.guild_id, self.id, Some(user_id.into())).await } /// Edits the current user's voice state in a stage channel. /// - /// The [Mute Members] permission is **not** required if suppressing the - /// current user. - /// - /// Requires the [Request to Speak] permission. + /// **Note**: Requires the [Request to Speak] permission. The [Mute Members] permission is + /// **not** required. /// /// # Example /// @@ -542,57 +526,38 @@ impl GuildChannel { /// # let cache = Cache::default(); /// # let channel_id = ChannelId::new(1); /// # + /// use serenity::builder::EditVoiceState; /// use serenity::model::ModelError; /// /// // assuming the cache has been unlocked /// let channel = cache.guild_channel(channel_id).ok_or(ModelError::ItemMissing)?; /// /// // Send a request to speak - /// channel.edit_own_voice_state(&http, |v| v.request_to_speak(true)).await?; + /// let builder = EditVoiceState::default().request_to_speak(true); + /// channel.edit_own_voice_state(&http, builder.clone()).await?; /// /// // Clear own request to speak - /// channel.edit_own_voice_state(&http, |v| v.request_to_speak(false)).await?; + /// let builder = builder.request_to_speak(false); + /// channel.edit_own_voice_state(&http, builder).await?; /// # Ok(()) /// # } /// ``` /// /// # Errors /// - /// Returns a [`ModelError::InvalidChannelType`] if the channel type is not - /// stage. + /// If the `cache` is enabled, returns a [`ModelError::InvalidChannelType`] if the channel is + /// not a stage channel. /// - /// [Mute Members]: crate::model::permissions::Permissions::MUTE_MEMBERS - /// [Request to Speak]: crate::model::permissions::Permissions::REQUEST_TO_SPEAK - pub async fn edit_own_voice_state(&self, http: impl AsRef, f: F) -> Result<()> - where - F: FnOnce(&mut EditVoiceState) -> &mut EditVoiceState, - { - self._edit_voice_state(http, None::, f).await - } - - async fn _edit_voice_state( + /// Returns [`Error::Http`] if the user lacks permission, or if invalid data is given. + /// + /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK + /// [Mute Members]: Permissions::MUTE_MEMBERS + pub async fn edit_own_voice_state( &self, - http: impl AsRef, - user_id: Option>, - f: F, - ) -> Result<()> - where - F: FnOnce(&mut EditVoiceState) -> &mut EditVoiceState, - { - if self.kind != ChannelType::Stage { - return Err(Error::from(ModelError::InvalidChannelType)); - } - - let mut voice_state = EditVoiceState::default(); - f(&mut voice_state); - - voice_state.channel_id = Some(self.id); - - if let Some(id) = user_id { - http.as_ref().edit_voice_state(self.guild_id.get(), id.into().get(), &voice_state).await - } else { - http.as_ref().edit_voice_state_me(self.guild_id.get(), &voice_state).await - } + cache_http: impl CacheHttp, + builder: EditVoiceState, + ) -> Result<()> { + builder.execute(cache_http, self.guild_id, self.id, None).await } /// Follows the News Channel @@ -666,24 +631,21 @@ impl GuildChannel { /// Gets messages from the channel. /// - /// Refer to the [`GetMessages`]-builder for more information on how to - /// use `builder`. - /// - /// **Note**: Returns an empty [`Vec`] if the current user does not have the - /// [Read Message History] permission. + /// **Note**: If the user does not have the [Read Message History] permission, returns an empty + /// [`Vec`]. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission to - /// view the channel. + /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY #[inline] - pub async fn messages(&self, http: impl AsRef, builder: F) -> Result> - where - F: FnOnce(&mut GetMessages) -> &mut GetMessages, - { - self.id.messages(&http, builder).await + pub async fn messages( + &self, + http: impl AsRef, + builder: GetMessages, + ) -> Result> { + self.id.messages(http, builder).await } /// Returns the name of the guild channel. @@ -735,6 +697,7 @@ impl GuildChannel { /// for demonstrative purposes): /// /// ```rust,no_run + /// use serenity::builder::CreateMessage; /// use serenity::model::channel::Channel; /// use serenity::model::prelude::*; /// use serenity::prelude::*; @@ -765,11 +728,10 @@ impl GuildChannel { /// }, /// }; /// + /// let builder = CreateMessage::default().content("here's a cat"); /// let _ = msg /// .channel_id - /// .send_files(&context.http, vec![(&file, "cat.png")], |m| { - /// m.content("here's a cat") - /// }) + /// .send_files(&context.http, vec![(&file, "cat.png")], builder) /// .await; /// } /// } @@ -897,82 +859,73 @@ impl GuildChannel { /// Sends a message with just the given message content in the channel. /// - /// # Errors + /// **Note**: Message content must be under 2000 unicode code points. /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the above limit, containing the number of unicode code points - /// over the limit. + /// # Errors /// - /// May also return [`Error::Http`] if the current user lacks permission - /// to send a message to the channel. + /// Returns a [`ModelError::MessageTooLong`] if the content length is over the above limit. See + /// [`CreateMessage::execute`] for more details. #[inline] - pub async fn say(&self, http: impl AsRef, content: impl Into) -> Result { - self.id.say(&http, content).await + pub async fn say( + &self, + cache_http: impl CacheHttp, + content: impl Into, + ) -> Result { + self.id.say(cache_http, content).await } - /// Sends (a) file(s) along with optional message contents. + /// Sends file(s) along with optional message contents. /// /// Refer to [`ChannelId::send_files`] for examples and more information. /// - /// The [Attach Files] and [Send Messages] permissions are required. - /// - /// **Note**: Message contents must be under 2000 unicode code points. - /// /// # Errors /// - /// If the content of the message is over the above limit, then a - /// [`ModelError::MessageTooLong`] will be returned, containing the number - /// of unicode code points over the limit. - /// - /// [Attach Files]: Permissions::ATTACH_FILES - /// [Send Messages]: Permissions::SEND_MESSAGES + /// See [`CreateMessage::execute`] for a list of possible errors, and their corresponding + /// reasons. #[inline] - pub async fn send_files<'a, F, T, It>( + pub async fn send_files<'a, T, It>( &self, - http: impl AsRef, + cache_http: impl CacheHttp, files: It, - f: F, + builder: CreateMessage<'a>, ) -> Result where - for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>, T: Into>, It: IntoIterator, { - self.id.send_files(&http, files, f).await + builder + .files(files) + .execute( + cache_http, + self.id, + #[cfg(feature = "cache")] + Some(self.guild_id), + ) + .await } - /// Sends a message to the channel with the given content. + /// Sends a message to the channel. /// - /// **Note**: Requires the [Send Messages] permission. + /// Refer to the documentation for [`CreateMessage`] for information regarding content + /// restrictions and requirements. /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the above limit, containing the number of unicode code points - /// over the limit. - /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user does - /// not have the required permissions. - /// - /// Otherwise will return [`Error::Http`] if the current user lacks permission. - /// - /// [Send Messages]: Permissions::SEND_MESSAGES - pub async fn send_message<'a, F>(&self, cache_http: impl CacheHttp, f: F) -> Result - where - for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>, - { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - let req = Permissions::SEND_MESSAGES; - - if let Ok(false) = utils::user_has_perms(cache, self.id, Some(self.guild_id), req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); - } - } - } - - self.id.send_message(&cache_http.http(), f).await + /// See [`CreateMessage::execute`] for a list of possible errors, and their corresponding + /// reasons. + pub async fn send_message<'a>( + &self, + cache_http: impl CacheHttp, + builder: CreateMessage<'a>, + ) -> Result { + builder + .execute( + cache_http, + self.id, + #[cfg(feature = "cache")] + Some(self.guild_id), + ) + .await } /// Starts typing in the channel for an indefinite period of time. @@ -1126,24 +1079,17 @@ impl GuildChannel { ReactionCollectorBuilder::new(shard_messenger).channel_id(self.id.0) } - /// Creates a webhook with only a name. + /// Creates a webhook in the channel. /// /// # Errors /// - /// Returns a [`ModelError::NameTooShort`] if the name of the webhook is - /// under the limit of 2 characters. - /// Returns a [`ModelError::NameTooLong`] if the name of the webhook is - /// over the limit of 100 characters. - /// Returns a [`ModelError::InvalidChannelType`] if the channel type is not text. - pub async fn create_webhook(&self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut CreateWebhook) -> &mut CreateWebhook, - { - if self.is_text_based() { - self.id.create_webhook(&http, f).await - } else { - Err(Error::Model(ModelError::InvalidChannelType)) - } + /// See [`CreateWebhook::execute`] for a detailed list of possible errors. + pub async fn create_webhook( + &self, + cache_http: impl CacheHttp, + builder: CreateWebhook, + ) -> Result { + self.id.create_webhook(cache_http, builder).await } /// Gets a stage instance. @@ -1165,41 +1111,30 @@ impl GuildChannel { /// # Errors /// /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. + /// /// Returns [`Error::Http`] if there is already a stage instance currently. - pub async fn create_stage_instance( + pub async fn create_stage_instance( &self, - http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateStageInstance) -> &mut CreateStageInstance, - { - if self.kind != ChannelType::Stage { - return Err(Error::Model(ModelError::InvalidChannelType)); - } - - self.id.create_stage_instance(http, f).await + cache_http: impl CacheHttp, + builder: CreateStageInstance, + ) -> Result { + self.id.create_stage_instance(cache_http, builder).await } - /// Edits a stage instance. + /// Edits the stage instance /// /// # Errors /// /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. - /// Returns [`Error::Http`] if there is no stage instance currently. - pub async fn edit_stage_instance( + /// + /// Returns [`Error::Http`] if the channel is not a stage channel, or there is no stage + /// instance currently. + pub async fn edit_stage_instance( &self, - http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut EditStageInstance) -> &mut EditStageInstance, - { - if self.kind != ChannelType::Stage { - return Err(Error::Model(ModelError::InvalidChannelType)); - } - - self.id.edit_stage_instance(http, f).await + cache_http: impl CacheHttp, + builder: EditStageInstance, + ) -> Result { + self.id.edit_stage_instance(cache_http, builder).await } /// Deletes a stage instance. @@ -1220,33 +1155,27 @@ impl GuildChannel { /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. - pub async fn create_public_thread( + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + pub async fn create_public_thread( &self, http: impl AsRef, message_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateThread) -> &mut CreateThread, - { - self.id.create_public_thread(http, message_id, f).await + builder: CreateThread, + ) -> Result { + self.id.create_public_thread(http, message_id, builder).await } /// Creates a private thread. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. - pub async fn create_private_thread( + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + pub async fn create_private_thread( &self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateThread) -> &mut CreateThread, - { - self.id.create_private_thread(http, f).await + builder: CreateThread, + ) -> Result { + self.id.create_private_thread(http, builder).await } } diff --git a/src/model/channel/message.rs b/src/model/channel/message.rs index b5cefad286d..d7953e97cd7 100644 --- a/src/model/channel/message.rs +++ b/src/model/channel/message.rs @@ -6,7 +6,7 @@ use std::fmt::Display; use std::fmt::Write; #[cfg(all(feature = "model", feature = "utils"))] -use crate::builder::{CreateEmbed, EditMessage}; +use crate::builder::{CreateAllowedMentions, CreateMessage, EditMessage, ParseValue}; #[cfg(all(feature = "cache", feature = "model"))] use crate::cache::{Cache, GuildRef}; #[cfg(feature = "collector")] @@ -18,21 +18,14 @@ use crate::collector::{ ReactionCollectorBuilder, }; #[cfg(feature = "model")] -use crate::http::{CacheHttp, Http}; +use crate::constants; #[cfg(feature = "model")] -use crate::json::prelude::*; +use crate::http::{CacheHttp, Http}; use crate::model::application::component::ActionRow; use crate::model::application::interaction::MessageInteraction; use crate::model::prelude::*; -#[cfg(feature = "model")] -use crate::{ - constants, - model::{ - id::{ApplicationId, ChannelId, GuildId, MessageId}, - sticker::StickerItem, - timestamp::Timestamp, - }, -}; +#[cfg(all(feature = "cache", feature = "model"))] +use crate::utils; /// A representation of a message over a guild's text channel, a group, or a /// private channel. @@ -305,37 +298,47 @@ impl Message { /// Edits this message, replacing the original content with new content. /// - /// Message editing preserves all unchanged message data. + /// Message editing preserves all unchanged message data, with some exceptions for embeds and + /// attachments. /// - /// Refer to the documentation for [`EditMessage`] for more information - /// regarding message restrictions and requirements. + /// **Note**: In most cases requires that the current user be the author of the message. /// - /// **Note**: Requires that the current user be the author of the message. + /// Refer to the documentation for [`EditMessage`] for information regarding content + /// restrictions and requirements. /// /// # Examples /// /// Edit a message with new content: /// - /// ```rust,ignore + /// ```rust,no_run + /// # use serenity::builder::EditMessage; + /// # use serenity::model::id::{ChannelId, MessageId}; + /// # use serenity::http::Http; + /// # + /// # async fn run() -> Result<(), Box> { + /// # let http = Http::new("token"); + /// # let mut message = ChannelId::new(7).message(&http, MessageId::new(8)).await?; /// // assuming a `message` has already been bound - /// - /// message.edit(&context, |m| m.content("new content")); + /// let builder = EditMessage::default().content("new content"); + /// message.edit(&http, builder).await?; + /// # Ok(()) + /// # } /// ``` /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidUser`] if the - /// current user is not the author. + /// If the `cache` is enabled, returns a [`ModelError::InvalidUser`] if the current user is not + /// the author. Otherwise returns [`Error::Http`] if the user lacks permission, as well as if + /// invalid data is given. /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over [`the limit`], containing the number of unicode code points - /// over the limit. + /// Returns a [`ModelError::MessageTooLong`] if the message contents are too long. /// - /// [`the limit`]: crate::builder::EditMessage::content - pub async fn edit<'a, F>(&mut self, cache_http: impl CacheHttp, f: F) -> Result<()> - where - F: for<'b> FnOnce(&'b mut EditMessage<'a>) -> &'b mut EditMessage<'a>, - { + /// [Manage Messages]: Permissions::MANAGE_MESSAGES + pub async fn edit<'a>( + &mut self, + cache_http: impl CacheHttp, + builder: EditMessage<'a>, + ) -> Result<()> { #[cfg(feature = "cache")] { if let Some(cache) = cache_http.cache() { @@ -344,33 +347,8 @@ impl Message { } } } - let mut builder = self._prepare_edit_builder(); - f(&mut builder); - self._send_edit(cache_http.http(), builder).await - } - - fn _prepare_edit_builder<'a>(&self) -> EditMessage<'a> { - let mut builder = EditMessage::default(); - - if !self.content.is_empty() { - builder.content(&self.content); - } - - let embeds: Vec<_> = self.embeds.iter().map(|e| CreateEmbed::from(e.clone())).collect(); - builder.set_embeds(embeds); - - for attachment in &self.attachments { - builder.add_existing_attachment(attachment.id); - } - builder - } - - async fn _send_edit<'a>(&mut self, http: &Http, mut builder: EditMessage<'a>) -> Result<()> { - let files = std::mem::take(&mut builder.files); - *self = http - .edit_message_and_attachments(self.channel_id.get(), self.id.get(), &builder, files) - .await?; + *self = builder.execute(cache_http.http(), self.channel_id, self.id).await?; Ok(()) } @@ -691,61 +669,18 @@ impl Message { } } - self.channel_id - .send_message(cache_http.http(), |builder| { - if let Some(ping_user) = inlined { - builder.reference_message(self).allowed_mentions(|f| { - f.replied_user(ping_user) - // By providing allowed_mentions, Discord disabled _all_ pings by - // default so we need to re-enable them - .parse(crate::builder::ParseValue::Everyone) - .parse(crate::builder::ParseValue::Users) - .parse(crate::builder::ParseValue::Roles) - }); - } - - builder.content(content) - }) - .await - } - - /// Delete all embeds in this message - /// **Note**: The logged in user must either be the author of the message or - /// have the [Manage Messages] permission. - /// - /// # Errors - /// - /// If the `cache` feature is enabled, then returns a - /// [`ModelError::InvalidPermissions`] if the current user does not have - /// the required permissions. - /// - /// Otherwise returns [`Error::Http`] if the current user lacks permission. - /// - /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn suppress_embeds(&mut self, cache_http: impl CacheHttp) -> Result<()> { - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - utils::user_has_perms_cache( - cache, - self.channel_id, - self.guild_id, - Permissions::MANAGE_MESSAGES, - )?; - - if self.author.id != cache.current_user().id { - return Err(Error::Model(ModelError::NotAuthor)); - } - } + let mut builder = CreateMessage::default().content(content); + if let Some(ping_user) = inlined { + let allowed_mentions = CreateAllowedMentions::default() + .replied_user(ping_user) + // By providing allowed_mentions, Discord disabled _all_ pings by default so we + // need to re-enable them + .parse(ParseValue::Everyone) + .parse(ParseValue::Users) + .parse(ParseValue::Roles); + builder = builder.reference_message(self).allowed_mentions(allowed_mentions); } - - let mut suppress = EditMessage::default(); - suppress.suppress_embeds(true); - - *self = - cache_http.http().edit_message(self.channel_id.get(), self.id.get(), &suppress).await?; - - Ok(()) + self.channel_id.send_message(cache_http, builder).await } /// Checks whether the message mentions passed [`UserId`]. @@ -866,94 +801,6 @@ impl Message { pub fn category_id(&self, cache: impl AsRef) -> Option { cache.as_ref().channel_category_id(self.channel_id) } - - #[allow(dead_code)] // Temporary, will be added back or removed in future - pub(crate) fn check_lengths(map: &JsonMap) -> Result<()> { - Self::check_content_length(map)?; - Self::check_embed_length(map)?; - Self::check_sticker_ids_length(map)?; - - Ok(()) - } - - #[allow(dead_code)] // Temporary, will be added back or removed in future - pub(crate) fn check_content_length(map: &JsonMap) -> Result<()> { - if let Some(Value::String(content)) = map.get("content") { - if let Some(length_over) = Message::overflow_length(content) { - return Err(Error::Model(ModelError::MessageTooLong(length_over))); - } - } - - Ok(()) - } - - #[allow(dead_code)] // Temporary, will be added back or removed in future - pub(crate) fn check_embed_length(map: &JsonMap) -> Result<()> { - let embeds = match map.get("embeds") { - Some(&Value::Array(ref value)) => value, - _ => return Ok(()), - }; - - if embeds.len() > 10 { - return Err(Error::Model(ModelError::EmbedAmount)); - } - - for embed in embeds { - let mut total: usize = 0; - - if let Some(&Value::Object(ref author)) = embed.get("author") { - if let Some(&Value::Object(ref name)) = author.get("name") { - total += name.len(); - } - } - - if let Some(&Value::String(ref description)) = embed.get("description") { - total += description.len(); - } - - if let Some(&Value::Array(ref fields)) = embed.get("fields") { - for field_as_value in fields { - if let Value::Object(ref field) = *field_as_value { - if let Some(&Value::String(ref field_name)) = field.get("name") { - total += field_name.len(); - } - - if let Some(&Value::String(ref field_value)) = field.get("value") { - total += field_value.len(); - } - } - } - } - - if let Some(&Value::Object(ref footer)) = embed.get("footer") { - if let Some(&Value::String(ref text)) = footer.get("text") { - total += text.len(); - } - } - - if let Some(&Value::String(ref title)) = embed.get("title") { - total += title.len(); - } - - if total > constants::EMBED_MAX_LENGTH { - let overflow = total - constants::EMBED_MAX_LENGTH; - return Err(Error::Model(ModelError::EmbedTooLarge(overflow))); - } - } - - Ok(()) - } - - #[allow(dead_code)] // Temporary, will be added back or removed in future - pub(crate) fn check_sticker_ids_length(map: &JsonMap) -> Result<()> { - if let Some(Value::Array(sticker_ids)) = map.get("sticker_ids") { - if sticker_ids.len() > constants::STICKER_MAX_COUNT { - return Err(Error::Model(ModelError::StickerAmount)); - } - } - - Ok(()) - } } impl AsRef for Message { diff --git a/src/model/channel/private_channel.rs b/src/model/channel/private_channel.rs index cf0f5626ff5..f6eaade7023 100644 --- a/src/model/channel/private_channel.rs +++ b/src/model/channel/private_channel.rs @@ -143,33 +143,26 @@ impl PrivateChannel { /// Edits a [`Message`] in the channel given its Id. /// - /// Message editing preserves all unchanged message data. + /// Message editing preserves all unchanged message data, with some exceptions for embeds and + /// attachments. /// - /// Refer to the documentation for [`EditMessage`] for more information - /// regarding message restrictions and requirements. + /// **Note**: In most cases requires that the current user be the author of the message. /// - /// **Note**: Requires that the current user be the author of the message. + /// Refer to the documentation for [`EditMessage`] for information regarding content + /// restrictions and requirements. /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the [`the limit`], containing the number of unicode code points - /// over the limit. - /// - /// Returns [`Error::Http`] if the current user is not the owner of the message. - /// - /// [`the limit`]: crate::builder::EditMessage::content + /// See [`EditMessage::execute`] for a list of possible errors, and their corresponding + /// reasons. #[inline] - pub async fn edit_message<'a, F>( + pub async fn edit_message<'a>( &self, http: impl AsRef, message_id: impl Into, - f: F, - ) -> Result - where - F: for<'b> FnOnce(&'b mut EditMessage<'a>) -> &'b mut EditMessage<'a>, - { - self.id.edit_message(&http, message_id, f).await + builder: EditMessage<'a>, + ) -> Result { + self.id.edit_message(http, message_id, builder).await } /// Determines if the channel is NSFW. @@ -200,17 +193,21 @@ impl PrivateChannel { /// Gets messages from the channel. /// - /// Refer to [`GetMessages`] for more information on how to use `builder`. + /// **Note**: If the user does not have the [Read Message History] permission, returns an empty + /// [`Vec`]. /// /// # Errors /// - /// Returns [`Error::Http`] if an invalid value is set in the builder. + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY #[inline] - pub async fn messages(&self, http: impl AsRef, builder: F) -> Result> - where - F: FnOnce(&mut GetMessages) -> &mut GetMessages, - { - self.id.messages(&http, builder).await + pub async fn messages( + &self, + http: impl AsRef, + builder: GetMessages, + ) -> Result> { + self.id.messages(http, builder).await } /// Returns "DM with $username#discriminator". @@ -270,62 +267,59 @@ impl PrivateChannel { /// Sends a message with just the given message content in the channel. /// + /// **Note**: Message content must be under 2000 unicode code points. + /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the above limit, containing the number of unicode code points - /// over the limit. + /// Returns a [`ModelError::MessageTooLong`] if the content length is over the above limit. See + /// [`CreateMessage::execute`] for more details. #[inline] - pub async fn say(&self, http: impl AsRef, content: impl Into) -> Result { - self.id.say(&http, content).await + pub async fn say( + &self, + cache_http: impl CacheHttp, + content: impl Into, + ) -> Result { + self.id.say(cache_http, content).await } - /// Sends (a) file(s) along with optional message contents. + /// Sends file(s) along with optional message contents. /// /// Refer to [`ChannelId::send_files`] for examples and more information. /// - /// The [Attach Files] and [Send Messages] permissions are required. - /// - /// **Note**: Message contents must be under 2000 unicode code points. - /// /// # Errors /// - /// If the content of the message is over the above limit, then a - /// [`ModelError::MessageTooLong`] will be returned, containing the number - /// of unicode code points over the limit. - /// - /// [Send Messages]: Permissions::SEND_MESSAGES + /// See [`CreateMessage::execute`] for a list of possible errors, and their corresponding + /// reasons. #[inline] - pub async fn send_files<'a, F, T, It>( + pub async fn send_files<'a, T, It>( &self, - http: impl AsRef, + cache_http: impl CacheHttp, files: It, - f: F, + builder: CreateMessage<'a>, ) -> Result where - for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>, T: Into>, It: IntoIterator, { - self.id.send_files(&http, files, f).await + self.id.send_files(cache_http, files, builder).await } - /// Sends a message to the channel with the given content. + /// Sends a message to the channel. /// - /// Refer to the documentation for [`CreateMessage`] for more information - /// regarding message restrictions and requirements. + /// Refer to the documentation for [`CreateMessage`] for information regarding content + /// restrictions and requirements. /// /// # Errors /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the above limit, containing the number of unicode code points - /// over the limit. + /// See [`CreateMessage::execute`] for a list of possible errors, and their corresponding + /// reasons. #[inline] - pub async fn send_message<'a, F>(&self, http: impl AsRef, f: F) -> Result - where - for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>, - { - self.id.send_message(&http, f).await + pub async fn send_message<'a>( + &self, + cache_http: impl CacheHttp, + builder: CreateMessage<'a>, + ) -> Result { + self.id.send_message(cache_http, builder).await } /// Starts typing in the channel for an indefinite period of time. diff --git a/src/model/channel/reaction.rs b/src/model/channel/reaction.rs index 47e72896e05..2b5998d472d 100644 --- a/src/model/channel/reaction.rs +++ b/src/model/channel/reaction.rs @@ -113,7 +113,7 @@ impl Reaction { } if user_id.is_some() { - utils::user_has_perms_cache( + crate::utils::user_has_perms_cache( cache, self.channel_id, self.guild_id, @@ -152,7 +152,7 @@ impl Reaction { #[cfg(feature = "cache")] { if let Some(cache) = cache_http.cache() { - utils::user_has_perms_cache( + crate::utils::user_has_perms_cache( cache, self.channel_id, self.guild_id, diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index 49d032f26ad..9bb43ced8b5 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -84,48 +84,42 @@ impl GuildId { /// ``` /// use std::time::Duration; /// + /// use serenity::builder::EditAutoModRule; /// use serenity::model::guild::automod::{Action, Trigger}; /// use serenity::model::id::GuildId; /// /// # async fn run() { /// # use serenity::http::Http; /// # let http = Http::new("token"); - /// let _rule = GuildId::new(7) - /// .create_automod_rule(&http, |r| { - /// r.name("foobar filter") - /// .trigger(Trigger::Keyword(vec!["foo*".to_string(), "*bar".to_string()])) - /// .actions(vec![Action::BlockMessage, Action::Timeout(Duration::from_secs(60))]) - /// }) - /// .await; + /// let builder = EditAutoModRule::default() + /// .name("foobar filter") + /// .trigger(Trigger::Keyword(vec!["foo*".to_string(), "*bar".to_string()])) + /// .actions(vec![Action::BlockMessage, Action::Timeout(Duration::from_secs(60))]); + /// let _rule = GuildId::new(7).create_automod_rule(&http, builder).await; /// # } /// ``` /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD #[inline] pub async fn create_automod_rule( self, http: impl AsRef, - f: impl FnOnce(&mut EditAutoModRule) -> &mut EditAutoModRule, + builder: EditAutoModRule, ) -> Result { - let mut builder = EditAutoModRule::default(); - f(&mut builder); - - http.as_ref().create_automod_rule(self.get(), &builder).await + builder.execute(http, self, None).await } - /// Edit an auto moderation [`Rule`] by its ID. + /// Edit an auto moderation [`Rule`], given its Id. /// /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD #[inline] @@ -133,12 +127,9 @@ impl GuildId { self, http: impl AsRef, rule_id: impl Into, - f: impl FnOnce(&mut EditAutoModRule) -> &mut EditAutoModRule, + builder: EditAutoModRule, ) -> Result { - let mut builder = EditAutoModRule::default(); - f(&mut builder); - - http.as_ref().edit_automod_rule(self.get(), rule_id.into().get(), &builder).await + builder.execute(http, self, Some(rule_id.into())).await } /// Deletes an auto moderation [`Rule`] from the guild. @@ -162,23 +153,20 @@ impl GuildId { /// Adds a [`User`] to this guild with a valid OAuth2 access token. /// - /// Returns the created [`Member`] object, or nothing if the user is already a member of the guild. + /// Returns the created [`Member`] object, or nothing if the user is already a member of the + /// guild. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. #[inline] pub async fn add_member( self, http: impl AsRef, user_id: impl Into, - f: impl FnOnce(&mut AddMember) -> &mut AddMember, + builder: AddMember, ) -> Result> { - let mut builder = AddMember::default(); - f(&mut builder); - - http.as_ref().add_guild_member(self.get(), user_id.into().get(), &builder).await + builder.execute(http, self, user_id.into()).await } /// Ban a [`User`] from the guild, deleting a number of @@ -316,40 +304,39 @@ impl GuildId { /// /// Refer to [`Http::create_channel`] for more information. /// - /// Requires the [Manage Channels] permission. + /// **Note**: Requires the [Manage Channels] permission. /// /// # Examples /// /// Create a voice channel in a guild with the name `test`: /// /// ```rust,no_run + /// # use serenity::http::Http; + /// use serenity::builder::CreateChannel; /// use serenity::model::channel::ChannelType; /// use serenity::model::id::GuildId; /// - /// # async fn run() { - /// # use serenity::http::Http; + /// # async fn run() -> Result<(), Box> { /// # let http = Http::new("token"); - /// let _channel = - /// GuildId::new(7).create_channel(&http, |c| c.name("test").kind(ChannelType::Voice)).await; + /// let builder = CreateChannel::default().name("test").kind(ChannelType::Voice); + /// let _channel = GuildId::new(7).create_channel(&http, builder).await?; + /// # Ok(()) /// # } /// ``` /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS #[inline] pub async fn create_channel( self, - http: impl AsRef, - f: impl FnOnce(&mut CreateChannel) -> &mut CreateChannel, + cache_http: impl CacheHttp, + builder: CreateChannel, ) -> Result { - let mut builder = CreateChannel::default(); - f(&mut builder); - - http.as_ref().create_channel(self.get(), &builder, None).await + builder.execute(cache_http, self).await } /// Creates an emoji in the guild with a name and base64-encoded image. @@ -420,25 +407,13 @@ impl GuildId { /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid data is given. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES #[inline] - pub async fn create_role(self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut EditRole) -> &mut EditRole, - { - let mut edit_role = EditRole::default(); - f(&mut edit_role); - - let role = http.as_ref().create_role(self.get(), &edit_role, None).await?; - - if let Some(position) = edit_role.position { - self.edit_role_position(&http, role.id, position as u64).await?; - } - - Ok(role) + pub async fn create_role(self, cache_http: impl CacheHttp, builder: EditRole) -> Result { + builder.execute(cache_http, self, None).await } /// Creates a new scheduled event in the guild with the data set, if any. @@ -447,21 +422,16 @@ impl GuildId { /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Events]: Permissions::MANAGE_EVENTS - pub async fn create_scheduled_event( - &self, - http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateScheduledEvent) -> &mut CreateScheduledEvent, - { - let mut builder = CreateScheduledEvent::default(); - f(&mut builder); - - http.as_ref().create_scheduled_event(self.get(), &builder, None).await + pub async fn create_scheduled_event( + self, + cache_http: impl CacheHttp, + builder: CreateScheduledEvent, + ) -> Result { + builder.execute(cache_http, self).await } /// Creates a new sticker in the guild with the data set, if any. @@ -470,22 +440,17 @@ impl GuildId { /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid data is given. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// - /// [Manage Emojis and Stickers]: crate::model::permissions::Permissions::MANAGE_EMOJIS_AND_STICKERS + /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS #[inline] - pub async fn create_sticker<'a, F>(self, http: impl AsRef, f: F) -> Result - where - for<'b> F: FnOnce(&'b mut CreateSticker<'a>) -> &'b mut CreateSticker<'a>, - { - let mut create_sticker = CreateSticker::default(); - f(&mut create_sticker); - - let (map, file) = - create_sticker.build().ok_or(Error::Model(ModelError::NoStickerFileSet))?; - - http.as_ref().create_sticker(self.get(), map, file, None).await + pub async fn create_sticker<'a>( + self, + cache_http: impl CacheHttp, + builder: CreateSticker<'_>, + ) -> Result { + builder.execute(cache_http, self).await } /// Deletes the current guild if the current account is the owner of the @@ -602,26 +567,21 @@ impl GuildId { /// Edits the current guild with new data where specified. /// - /// Refer to [`Guild::edit`] for more information. - /// - /// **Note**: Requires the current user to have the [Manage Guild] - /// permission. + /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if an invalid value is set. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD #[inline] - pub async fn edit(&mut self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut EditGuild) -> &mut EditGuild, - { - let mut edit_guild = EditGuild::default(); - f(&mut edit_guild); - - http.as_ref().edit_guild(self.get(), &edit_guild, None).await + pub async fn edit( + self, + cache_http: impl CacheHttp, + builder: EditGuild, + ) -> Result { + builder.execute(cache_http, self).await } /// Edits an [`Emoji`]'s name in the guild. @@ -650,37 +610,42 @@ impl GuildId { http.as_ref().edit_emoji(self.get(), emoji_id.into().get(), &map, None).await } - /// Edits the properties of member of the guild, such as muting or - /// nicknaming them. + /// Edits the properties a guild member, such as muting or nicknaming them. Returns the new + /// member. /// - /// Refer to [`EditMember`]'s documentation for a full list of methods and - /// permission restrictions. + /// Refer to the documentation of [`EditMember`] for a full list of methods and permission + /// restrictions. /// /// # Examples /// /// Mute a member and set their roles to just one role with a predefined Id: /// - /// ```rust,ignore - /// guild.edit_member(&context, user_id, |m| m.mute(true).roles(&vec![role_id])); + /// ```rust,no_run + /// # use serenity::builder::EditMember; + /// # use serenity::http::Http; + /// # use serenity::model::id::{GuildId, RoleId, UserId}; + /// # + /// # async fn run() -> Result<(), Box> { + /// # let http = Http::new("token"); + /// # let role_id = RoleId::new(7); + /// # let user_id = UserId::new(7); + /// let builder = EditMember::default().mute(true).roles(vec![role_id]); + /// let _ = GuildId::new(7).edit_member(&http, user_id, builder).await?; + /// # Ok(()) + /// # } /// ``` /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks the necessary permissions. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. #[inline] - pub async fn edit_member( + pub async fn edit_member( self, http: impl AsRef, user_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditMember) -> &mut EditMember, - { - let mut edit_member = EditMember::default(); - f(&mut edit_member); - - http.as_ref().edit_member(self.get(), user_id.into().get(), &edit_member, None).await + builder: EditMember, + ) -> Result { + builder.execute(http, self, user_id.into()).await } /// Edits the current user's nickname for the guild. @@ -705,37 +670,44 @@ impl GuildId { /// Edits a [`Role`], optionally setting its new fields. /// - /// Requires the [Manage Roles] permission. + /// **Note**: Requires the [Manage Roles] permission. /// /// # Examples /// - /// Make a role hoisted: - /// - /// ```rust,ignore - /// use serenity::model::{GuildId, RoleId}; + /// Make a role hoisted, and change its name: /// - /// GuildId::new(7).edit_role(&context, RoleId::new(8), |r| r.hoist(true)); + /// ```rust,no_run + /// # use serenity::builder::EditRole; + /// # use serenity::http::Http; + /// # use serenity::model::id::{GuildId, RoleId}; + /// # use std::sync::Arc; + /// # + /// # async fn run() -> Result<(), Box> { + /// # let http = Arc::new(Http::new("token")); + /// # let guild_id = GuildId::new(2); + /// # let role_id = RoleId::new(8); + /// # + /// // assuming a `role_id` and `guild_id` has been bound + /// let builder = EditRole::default().name("a test role").hoist(true); + /// let role = guild_id.edit_role(&http, role_id, builder).await?; + /// # Ok(()) + /// # } /// ``` /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES #[inline] - pub async fn edit_role( + pub async fn edit_role( self, - http: impl AsRef, + cache_http: impl CacheHttp, role_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditRole) -> &mut EditRole, - { - let mut edit_role = EditRole::default(); - f(&mut edit_role); - - http.as_ref().edit_role(self.get(), role_id.into().get(), &edit_role, None).await + builder: EditRole, + ) -> Result { + builder.execute(cache_http, self, Some(role_id.into())).await } /// Modifies a scheduled event in the guild with the data set, if any. @@ -744,65 +716,60 @@ impl GuildId { /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Events]: Permissions::MANAGE_EVENTS - pub async fn edit_scheduled_event( + pub async fn edit_scheduled_event( self, - http: impl AsRef, + cache_http: impl CacheHttp, event_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditScheduledEvent) -> &mut EditScheduledEvent, - { - let mut edit_scheduled_event = EditScheduledEvent::default(); - f(&mut edit_scheduled_event); - - http.as_ref() - .edit_scheduled_event(self.get(), event_id.into().get(), &edit_scheduled_event, None) - .await + builder: EditScheduledEvent, + ) -> Result { + builder.execute(cache_http, self, event_id.into()).await } - /// Edits a [`Sticker`], optionally setting its fields. + /// Edits a sticker. /// - /// Requires the [Manage Emojis and Stickers] permission. + /// **Note**: Requires the [Manage Emojis and Stickers] permission. /// /// # Examples /// /// Rename a sticker: /// - /// ```rust,ignore - /// guild.edit_sticker(&context, StickerId(7), |r| r.name("Bun bun meow")); + /// ```rust,no_run + /// # use serenity::http::Http; + /// use serenity::builder::EditSticker; + /// use serenity::model::id::{GuildId, StickerId}; + /// + /// # async fn run() -> Result<(), Box> { + /// # let http = Http::new("token"); + /// let builder = EditSticker::default().name("Bun bun meow"); + /// let _ = GuildId::new(7).edit_sticker(&http, StickerId::new(7), builder).await?; + /// # Ok(()) + /// # } /// ``` /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// - /// [Manage Emojis and Stickers]: crate::model::permissions::Permissions::MANAGE_EMOJIS_AND_STICKERS + /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS #[inline] - pub async fn edit_sticker( - &self, + pub async fn edit_sticker( + self, http: impl AsRef, sticker_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditSticker) -> &mut EditSticker, - { - let mut edit_sticker = EditSticker::default(); - f(&mut edit_sticker); - - http.as_ref().edit_sticker(self.get(), sticker_id.into().get(), &edit_sticker, None).await + builder: EditSticker, + ) -> Result { + builder.execute(http, self, sticker_id.into()).await } - /// Edits the order of [`Role`]s - /// Requires the [Manage Roles] permission. + /// Edit the position of a [`Role`] relative to all others in the [`Guild`]. /// - /// # Examples + /// **Note**: Requires the [Manage Roles] permission. /// - /// Change the order of a role: + /// # Examples /// /// ```rust,ignore /// use serenity::model::{GuildId, RoleId}; @@ -824,39 +791,38 @@ impl GuildId { http.as_ref().edit_role_position(self.get(), role_id.into().get(), position, None).await } - /// Edits the [`GuildWelcomeScreen`]. + /// Edits the guild's welcome screen. + /// + /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns an [`Error::Http`] if some mandatory fields are not provided. - pub async fn edit_welcome_screen( - &self, + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Guild]: Permissions::MANAGE_GUILD + pub async fn edit_welcome_screen( + self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut EditGuildWelcomeScreen) -> &mut EditGuildWelcomeScreen, - { - let mut map = EditGuildWelcomeScreen::default(); - f(&mut map); - - http.as_ref().edit_guild_welcome_screen(self.get(), &map).await + builder: EditGuildWelcomeScreen, + ) -> Result { + builder.execute(http, self).await } - /// Edits the [`GuildWidget`]. + /// Edits the guild's widget. + /// + /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns an [`Error::Http`] if the bot does not have the `MANAGE_GUILD` - /// permission. - pub async fn edit_widget(&self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut EditGuildWidget) -> &mut EditGuildWidget, - { - let mut map = EditGuildWidget::default(); - f(&mut map); - - http.as_ref().edit_guild_widget(self.get(), &map).await + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Guild]: Permissions::MANAGE_GUILD + pub async fn edit_widget( + self, + http: impl AsRef, + builder: EditGuildWidget, + ) -> Result { + builder.execute(http, self).await } /// Gets all of the guild's roles over the REST API. @@ -1103,13 +1069,12 @@ impl GuildId { /// Moves a member to a specific voice channel. /// - /// Requires the [Move Members] permission. + /// **Note**: Requires the [Move Members] permission. /// /// # Errors /// - /// Returns an [`Error::Http`] if the current user - /// lacks permission, or if the member is not currently - /// in a voice channel for this [`Guild`]. + /// Returns [`Error::Http`] if the current user lacks permission, or if the member is not + /// currently in a voice channel for this [`Guild`]. /// /// [Move Members]: Permissions::MOVE_MEMBERS #[inline] @@ -1119,7 +1084,8 @@ impl GuildId { user_id: impl Into, channel_id: impl Into, ) -> Result { - self.edit_member(http, user_id, |m| m.voice_channel(channel_id.into())).await + let builder = EditMember::default().voice_channel(channel_id.into()); + self.edit_member(http, user_id, builder).await } /// Returns the name of whatever guild this id holds. @@ -1131,12 +1097,12 @@ impl GuildId { /// Disconnects a member from a voice channel in the guild. /// - /// Requires the [Move Members] permission. + /// **Note**: Requires the [Move Members] permission. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if the member is not currently in a voice channel for this guild. + /// Returns [`Error::Http`] if the current user lacks permission, or if the member is not + /// currently in a voice channel for this [`Guild`]. /// /// [Move Members]: Permissions::MOVE_MEMBERS #[inline] @@ -1145,7 +1111,7 @@ impl GuildId { http: impl AsRef, user_id: impl Into, ) -> Result { - self.edit_member(http, user_id, EditMember::disconnect_member).await + self.edit_member(http, user_id, EditMember::default().disconnect_member()).await } /// Gets the number of [`Member`]s that would be pruned with the given @@ -1451,96 +1417,63 @@ impl GuildId { ReactionCollectorBuilder::new(shard_messenger).guild_id(self.0) } - /// Creates a guild specific [`Command`] + /// Create a guild specific application [`Command`]. /// - /// **Note**: Unlike global `Command`s, guild commands will update instantly. + /// **Note**: Unlike global commands, guild commands will update instantly. /// /// # Errors /// - /// Returns the same possible errors as [`create_global_application_command`]. - /// - /// [`create_global_application_command`]: Command::create_global_application_command - pub async fn create_application_command( - &self, + /// See [`CreateApplicationCommand::execute`] for a list of possible errors. + pub async fn create_application_command( + self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, - { - let map = Command::build_application_command(f); - http.as_ref().create_guild_application_command(self.get(), &map).await + builder: CreateApplicationCommand, + ) -> Result { + builder.execute(http, Some(self), None).await } - /// Overrides all guild application commands. + /// Override all guild application commands. /// /// # Errors /// - /// Returns the same possible errors as [`set_global_application_commands`]. - /// - /// [`set_global_application_commands`]: Command::set_global_application_commands - pub async fn set_application_commands( - &self, + /// See [`CreateApplicationCommands::execute`] for a list of possible errors. + pub async fn set_application_commands( + self, http: impl AsRef, - f: F, - ) -> Result> - where - F: FnOnce(&mut CreateApplicationCommands) -> &mut CreateApplicationCommands, - { - let mut array = CreateApplicationCommands::default(); - - f(&mut array); - - http.as_ref().create_guild_application_commands(self.get(), &array).await + builder: CreateApplicationCommands, + ) -> Result> { + builder.execute(http, Some(self)).await } - /// Creates a guild specific [`CommandPermission`]. + /// Create a guild specific [`CommandPermission`]. /// /// **Note**: It will update instantly. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn create_application_command_permission( - &self, + /// See [`CreateApplicationCommandPermissionsData::execute`] for a list of possible errors. + pub async fn create_application_command_permission( + self, http: impl AsRef, command_id: CommandId, - f: F, - ) -> Result - where - F: FnOnce( - &mut CreateApplicationCommandPermissionsData, - ) -> &mut CreateApplicationCommandPermissionsData, - { - let mut map = CreateApplicationCommandPermissionsData::default(); - f(&mut map); - - http.as_ref() - .edit_guild_application_command_permissions(self.get(), command_id.into(), &map) - .await + builder: CreateApplicationCommandPermissionsData, + ) -> Result { + builder.execute(http, self, command_id).await } - /// Overrides all application commands permissions. + /// Override permissions for all guild application commands. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. + /// See [`CreateApplicationCommandsPermissions::execute`] for a list of possible errors. #[deprecated(note = "use `create_appliction_command_permission`.")] #[allow(deprecated)] - pub async fn set_application_commands_permissions( - &self, + pub async fn set_application_commands_permissions( + self, http: impl AsRef, - f: F, - ) -> Result> - where - F: FnOnce( - &mut crate::builder::CreateApplicationCommandsPermissions, - ) -> &mut crate::builder::CreateApplicationCommandsPermissions, - { - let mut map = crate::builder::CreateApplicationCommandsPermissions::default(); - f(&mut map); - - http.as_ref().edit_guild_application_commands_permissions(self.get(), &map).await + builder: crate::builder::CreateApplicationCommandsPermissions, + ) -> Result> { + builder.execute(http, self).await } /// Get all guild application commands. @@ -1565,22 +1498,18 @@ impl GuildId { http.as_ref().get_guild_application_command(self.get(), command_id.into()).await } - /// Edit guild application command by its Id. + /// Edit a guild application command, given its Id. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn edit_application_command( - &self, + /// See [`CreateApplicationCommand::execute`] for a list of possible errors. + pub async fn edit_application_command( + self, http: impl AsRef, command_id: CommandId, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, - { - let map = Command::build_application_command(f); - http.as_ref().edit_guild_application_command(self.get(), command_id.into(), &map).await + builder: CreateApplicationCommand, + ) -> Result { + builder.execute(http, Some(self), Some(command_id)).await } /// Delete guild application command by its Id. diff --git a/src/model/guild/member.rs b/src/model/guild/member.rs index 1c5e8ba3642..5b2eea0044d 100644 --- a/src/model/guild/member.rs +++ b/src/model/guild/member.rs @@ -135,7 +135,8 @@ impl Member { let mut target_roles = self.roles.clone(); target_roles.extend_from_slice(role_ids); - self.edit(http, |b| b.roles(target_roles)).await + let builder = EditMember::default().roles(target_roles); + self.edit(http, builder).await } /// Ban a [`User`] from the guild, deleting a number of @@ -227,13 +228,8 @@ impl Member { http: impl AsRef, time: Timestamp, ) -> Result<()> { - match self - .guild_id - .edit_member(http, self.user.id, |member| { - member.disable_communication_until_datetime(time) - }) - .await - { + let builder = EditMember::default().disable_communication_until_datetime(time); + match self.guild_id.edit_member(http, self.user.id, builder).await { Ok(_) => { self.communication_disabled_until = Some(time); Ok(()) @@ -259,17 +255,18 @@ impl Member { /// Edits the member in place with the given data. /// - /// See [`EditMember`] for the permission(s) required for separate builder - /// methods, as well as usage of this. + /// See [`EditMember`] for the permission(s) required for separate builder methods, as well as + /// usage of this. + /// + /// # Examples + /// + /// See [`GuildId::edit_member`] for details. /// /// # Errors /// /// Returns [`Error::Http`] if the current user lacks necessary permissions. - pub async fn edit(&mut self, http: impl AsRef, f: F) -> Result<()> - where - F: FnOnce(&mut EditMember) -> &mut EditMember, - { - *self = self.guild_id.edit_member(http, self.user.id, f).await?; + pub async fn edit(&mut self, http: impl AsRef, builder: EditMember) -> Result<()> { + *self = self.guild_id.edit_member(http, self.user.id, builder).await?; Ok(()) } @@ -284,10 +281,8 @@ impl Member { /// [Moderate Members]: Permissions::MODERATE_MEMBERS #[doc(alias = "timeout")] pub async fn enable_communication(&mut self, http: impl AsRef) -> Result<()> { - *self = self - .guild_id - .edit_member(&http, self.user.id, EditMember::enable_communication) - .await?; + let builder = EditMember::default().enable_communication(); + *self = self.guild_id.edit_member(http, self.user.id, builder).await?; Ok(()) } @@ -507,7 +502,8 @@ impl Member { let mut target_roles = self.roles.clone(); target_roles.retain(|r| !role_ids.contains(r)); - self.edit(http, |b| b.roles(target_roles)).await + let builder = EditMember::default().roles(target_roles); + self.edit(http, builder).await } /// Retrieves the full role data for the user's roles. diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index 483e142d30f..0fea4c8eebe 100644 --- a/src/model/guild/mod.rs +++ b/src/model/guild/mod.rs @@ -302,54 +302,39 @@ impl Guild { /// /// # Examples /// - /// Create a custom keyword filter to block the message and timeout the author. - /// - /// ```ignore - /// use serenity::model::guild::automod::{Action, Trigger}; - /// use serenity::model::id::GuildId; - /// - /// let _rule = guild - /// .create_automod_rule(&http, |r| { - /// r.name("foobar filter") - /// .trigger(Trigger::Keyword(vec!["foo*".to_string(), "*bar".to_string()])) - /// .actions(vec![Action::BlockMessage, Action::Timeout(60)]) - /// }) - /// .await; - /// ``` + /// See [`GuildId::create_automod_rule`] for details. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD #[inline] pub async fn create_automod_rule( - self, + &self, http: impl AsRef, - f: impl FnOnce(&mut EditAutoModRule) -> &mut EditAutoModRule, + builder: EditAutoModRule, ) -> Result { - self.id.create_automod_rule(http, f).await + self.id.create_automod_rule(http, builder).await } - /// Edit an auto moderation [`Rule`] by its ID. + /// Edit an auto moderation [`Rule`], given its Id. /// /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD #[inline] pub async fn edit_automod_rule( - self, + &self, http: impl AsRef, rule_id: impl Into, - f: impl FnOnce(&mut EditAutoModRule) -> &mut EditAutoModRule, + builder: EditAutoModRule, ) -> Result { - self.id.edit_automod_rule(http, rule_id, f).await + self.id.edit_automod_rule(http, rule_id, builder).await } /// Deletes an auto moderation [`Rule`] from the guild. @@ -426,7 +411,11 @@ impl Guild { } #[cfg(feature = "cache")] - async fn has_perms(&self, cache_http: impl CacheHttp, mut permissions: Permissions) -> bool { + pub(crate) async fn has_perms( + &self, + cache_http: impl CacheHttp, + mut permissions: Permissions, + ) -> bool { if let Some(cache) = cache_http.cache() { let user_id = cache.current_user().id; @@ -574,20 +563,20 @@ impl Guild { /// Adds a [`User`] to this guild with a valid OAuth2 access token. /// - /// Returns the created [`Member`] object, or nothing if the user is already a member of the guild. + /// Returns the created [`Member`] object, or nothing if the user is already a member of the + /// guild. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. #[inline] pub async fn add_member( &self, http: impl AsRef, user_id: impl Into, - f: impl FnOnce(&mut AddMember) -> &mut AddMember, + builder: AddMember, ) -> Result> { - self.id.add_member(http, user_id, f).await + self.id.add_member(http, user_id, builder).await } /// Retrieves a list of [`AuditLogs`] for the guild. @@ -670,41 +659,36 @@ impl Guild { /// /// # Examples /// - /// ```rust,ignore - /// use serenity::model::ChannelType; + /// ```rust,no_run + /// # use serenity::http::Http; + /// # use serenity::model::guild::Guild; + /// # use serenity::model::id::GuildId; + /// use serenity::builder::CreateChannel; + /// use serenity::model::channel::ChannelType; /// - /// // assuming a `guild` has already been bound + /// # async fn run() -> Result<(), Box> { + /// # let http = Http::new("token"); + /// # let guild = Guild::get(&http, GuildId::new(7)).await?; + /// let builder = CreateChannel::default().name("my-test-channel").kind(ChannelType::Text); /// - /// let _ = guild - /// .create_channel(&http, |c| c.name("my-test-channel").kind(ChannelType::Text)) - /// .await; + /// // assuming a `guild` has already been bound + /// let _channel = guild.create_channel(&http, builder).await?; + /// # Ok(()) + /// # } /// ``` /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] - /// if the current user does not have permission to manage channels. - /// - /// Otherwise will return [`Error::Http`] if the current user lacks permission. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS pub async fn create_channel( &self, cache_http: impl CacheHttp, - f: impl FnOnce(&mut CreateChannel) -> &mut CreateChannel, + builder: CreateChannel, ) -> Result { - #[cfg(feature = "cache")] - { - if cache_http.cache().is_some() { - let req = Permissions::MANAGE_CHANNELS; - - if !self.has_perms(&cache_http, req).await { - return Err(Error::Model(ModelError::InvalidPermissions(req))); - } - } - } - - self.id.create_channel(cache_http.http(), f).await + self.id.create_channel(cache_http, builder).await } /// Creates an emoji in the guild with a name and base64-encoded image. The @@ -759,82 +743,64 @@ impl Guild { self.id.create_integration(&http, integration_id, kind).await } - /// Creates a guild specific [`Command`] + /// Create a guild specific application [`Command`]. /// - /// **Note**: Unlike global `Command`s, guild commands will update instantly. + /// **Note**: Unlike global commands, guild commands will update instantly. /// /// # Errors /// - /// Returns the same possible errors as `create_global_application_command`. + /// See [`CreateApplicationCommand::execute`] for a list of possible errors. #[inline] - pub async fn create_application_command( + pub async fn create_application_command( &self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, - { - self.id.create_application_command(http, f).await + builder: CreateApplicationCommand, + ) -> Result { + self.id.create_application_command(http, builder).await } - /// Overrides all guild application commands. - /// - /// [`create_application_command`]: Self::create_application_command + /// Override all guild application commands. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn set_application_commands( + /// See [`CreateApplicationCommands::execute`] for a list of possible errors. + pub async fn set_application_commands( &self, http: impl AsRef, - f: F, - ) -> Result> - where - F: FnOnce(&mut CreateApplicationCommands) -> &mut CreateApplicationCommands, - { - self.id.set_application_commands(http, f).await + builder: CreateApplicationCommands, + ) -> Result> { + self.id.set_application_commands(http, builder).await } - /// Creates a guild specific [`CommandPermission`]. + /// Create a guild specific [`CommandPermission`]. /// /// **Note**: It will update instantly. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn create_application_command_permission( + /// See [`CreateApplicationCommandPermissionsData::execute`] for a list of possible errors. + pub async fn create_application_command_permission( &self, http: impl AsRef, command_id: CommandId, - f: F, - ) -> Result - where - F: FnOnce( - &mut CreateApplicationCommandPermissionsData, - ) -> &mut CreateApplicationCommandPermissionsData, - { - self.id.create_application_command_permission(http, command_id, f).await + builder: CreateApplicationCommandPermissionsData, + ) -> Result { + self.id.create_application_command_permission(http, command_id, builder).await } - /// Overrides all application commands permissions. + /// Override permissions for all guild application commands. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. + /// See [`CreateApplicationCommandsPermissions::execute`] for a list of possible errors. #[deprecated(note = "use `create_appliction_command_permission`.")] #[allow(deprecated)] - pub async fn set_application_commands_permissions( + pub async fn set_application_commands_permissions( &self, http: impl AsRef, - f: F, - ) -> Result> - where - F: FnOnce( - &mut crate::builder::CreateApplicationCommandsPermissions, - ) -> &mut crate::builder::CreateApplicationCommandsPermissions, - { - self.id.set_application_commands_permissions(http, f).await + builder: crate::builder::CreateApplicationCommandsPermissions, + ) -> Result> { + self.id.set_application_commands_permissions(http, builder).await } /// Get all guild application commands. @@ -859,21 +825,18 @@ impl Guild { self.id.get_application_command(http, command_id).await } - /// Edit guild application command by its Id. + /// Edit a guild application command, given its Id. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn edit_application_command( + /// See [`CreateApplicationCommand::execute`] for a list of possible errors. + pub async fn edit_application_command( &self, http: impl AsRef, command_id: CommandId, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, - { - self.id.edit_application_command(http, command_id, f).await + builder: CreateApplicationCommand, + ) -> Result { + self.id.edit_application_command(http, command_id, builder).await } /// Delete guild application command by its Id. @@ -920,39 +883,16 @@ impl Guild { /// /// # Examples /// - /// Create a role which can be mentioned, with the name 'test': - /// - /// ```rust,ignore - /// // assuming a `guild` has been bound - /// - /// let role = guild.create_role(&http, |r| r.hoist(true).name("role")).await; - /// ``` + /// See the documentation for [`EditRole`] for details. /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] - /// if the current user does not have permission to manage roles. - /// - /// Otherwise will return [`Error::Http`] if the current user does - /// not have permission. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES - pub async fn create_role(&self, cache_http: impl CacheHttp, f: F) -> Result - where - F: FnOnce(&mut EditRole) -> &mut EditRole, - { - #[cfg(feature = "cache")] - { - if cache_http.cache().is_some() { - let req = Permissions::MANAGE_ROLES; - - if !self.has_perms(&cache_http, req).await { - return Err(Error::Model(ModelError::InvalidPermissions(req))); - } - } - } - - self.id.create_role(cache_http.http(), f).await + pub async fn create_role(&self, cache_http: impl CacheHttp, builder: EditRole) -> Result { + self.id.create_role(cache_http, builder).await } /// Creates a new scheduled event in the guild with the data set, if any. @@ -962,31 +902,15 @@ impl Guild { /// # Errors /// /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to manage scheduled events. - /// - /// Otherwise will return [`Error::Http`] if the current user does not have permission. + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Events]: Permissions::MANAGE_EVENTS - pub async fn create_scheduled_event( + pub async fn create_scheduled_event( &self, cache_http: impl CacheHttp, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateScheduledEvent) -> &mut CreateScheduledEvent, - { - #[cfg(feature = "cache")] - { - if cache_http.cache().is_some() { - let req = Permissions::MANAGE_EVENTS; - - if !self.has_perms(&cache_http, req).await { - return Err(Error::Model(ModelError::InvalidPermissions(req))); - } - } - } - - self.id.create_scheduled_event(cache_http.http(), f).await + builder: CreateScheduledEvent, + ) -> Result { + self.id.create_scheduled_event(cache_http, builder).await } /// Creates a new sticker in the guild with the data set, if any. @@ -995,26 +919,16 @@ impl Guild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] - /// if the current user does not have permission to manage roles. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// - /// [Manage Emojis and Stickers]: crate::model::permissions::Permissions::MANAGE_EMOJIS_AND_STICKERS - pub async fn create_sticker<'a, F>(&self, cache_http: impl CacheHttp, f: F) -> Result - where - for<'b> F: FnOnce(&'b mut CreateSticker<'a>) -> &'b mut CreateSticker<'a>, - { - #[cfg(feature = "cache")] - { - if cache_http.cache().is_some() { - let req = Permissions::MANAGE_EMOJIS_AND_STICKERS; - - if !self.has_perms(&cache_http, req).await { - return Err(Error::Model(ModelError::InvalidPermissions(req))); - } - } - } - - self.id.create_sticker(cache_http.http(), f).await + /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS + pub async fn create_sticker<'a>( + &self, + cache_http: impl CacheHttp, + builder: CreateSticker<'a>, + ) -> Result { + self.id.create_sticker(cache_http.http(), builder).await } /// Deletes the current guild if the current user is the owner of the @@ -1143,68 +1057,51 @@ impl Guild { /// Edits the current guild with new data where specified. /// - /// Refer to [`EditGuild`]'s documentation for a full list of methods. - /// - /// **Note**: Requires the current user to have the [Manage Guild] - /// permission. + /// **Note**: Requires the [Manage Guild] permission. /// /// # Examples /// - /// Change a guild's icon using a file name "icon.png": - /// - /// ```rust,ignore - /// use serenity::utils; + /// Change a guild's icon using a file named "icon.png": /// - /// // We are using read_image helper function from utils. - /// let base64_icon = utils::read_image("./icon.png") - /// .expect("Failed to read image"); + /// ```rust,no_run + /// # use serenity::builder::EditGuild; + /// # use serenity::{http::Http, model::id::GuildId}; + /// # + /// # async fn run() -> Result<(), Box> { + /// # let http = Http::new("token"); + /// # let mut guild = GuildId::new(1).to_partial_guild(&http).await?; + /// let base64_icon = serenity::utils::read_image("./icon.png")?; /// - /// guild.edit(|g| g.icon(base64_icon)); + /// // assuming a `guild` has already been bound + /// let builder = EditGuild::default().icon(Some(base64_icon)); + /// guild.edit(&http, builder).await?; + /// # Ok(()) + /// # } /// ``` /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] - /// if the current user does not have permission to edit the guild. - /// - /// Otherwise will return [`Error::Http`] if the current user does not have - /// permission. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - pub async fn edit(&mut self, cache_http: impl CacheHttp, f: F) -> Result<()> - where - F: FnOnce(&mut EditGuild) -> &mut EditGuild, - { - #[cfg(feature = "cache")] - { - if cache_http.cache().is_some() { - let req = Permissions::MANAGE_GUILD; + pub async fn edit(&mut self, cache_http: impl CacheHttp, builder: EditGuild) -> Result<()> { + let guild = self.id.edit(cache_http, builder).await?; + + self.afk_channel_id = guild.afk_channel_id; + self.afk_timeout = guild.afk_timeout; + self.default_message_notifications = guild.default_message_notifications; + self.emojis = guild.emojis; + self.features = guild.features; + self.icon = guild.icon; + self.mfa_level = guild.mfa_level; + self.name = guild.name; + self.owner_id = guild.owner_id; + self.roles = guild.roles; + self.splash = guild.splash; + self.verification_level = guild.verification_level; - if !self.has_perms(&cache_http, req).await { - return Err(Error::Model(ModelError::InvalidPermissions(req))); - } - } - } - - match self.id.edit(cache_http.http(), f).await { - Ok(guild) => { - self.afk_channel_id = guild.afk_channel_id; - self.afk_timeout = guild.afk_timeout; - self.default_message_notifications = guild.default_message_notifications; - self.emojis = guild.emojis; - self.features = guild.features; - self.icon = guild.icon; - self.mfa_level = guild.mfa_level; - self.name = guild.name; - self.owner_id = guild.owner_id; - self.roles = guild.roles; - self.splash = guild.splash; - self.verification_level = guild.verification_level; - - Ok(()) - }, - Err(why) => Err(why), - } + Ok(()) } /// Edits an [`Emoji`]'s name in the guild. @@ -1229,34 +1126,27 @@ impl Guild { self.id.edit_emoji(&http, emoji_id, name).await } - /// Edits the properties of member of the guild, such as muting or - /// nicknaming them. Returns the new member. + /// Edits the properties a guild member, such as muting or nicknaming them. Returns the new + /// member. /// - /// Refer to [`EditMember`]'s documentation for a full list of methods and - /// permission restrictions. + /// Refer to the documentation of [`EditMember`] for a full list of methods and permission + /// restrictions. /// /// # Examples /// - /// Mute a member and set their roles to just one role with a predefined Id: - /// - /// ```rust,ignore - /// guild.edit_member(user_id, |m| m.mute(true).roles(&vec![role_id])); - /// ``` + /// See [`GuildId::edit_member`] for details. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks the necessary permissions. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. #[inline] - pub async fn edit_member( + pub async fn edit_member( &self, http: impl AsRef, user_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditMember) -> &mut EditMember, - { - self.id.edit_member(&http, user_id, f).await + builder: EditMember, + ) -> Result { + self.id.edit_member(http, user_id, builder).await } /// Edits the current user's nickname for the guild. @@ -1295,32 +1185,26 @@ impl Guild { /// Edits a role, optionally setting its fields. /// - /// Requires the [Manage Roles] permission. + /// **Note**: Requires the [Manage Roles] permission. /// /// # Examples /// - /// Make a role hoisted: - /// - /// ```rust,ignore - /// guild.edit_role(&context, RoleId::new(7), |r| r.hoist(true)); - /// ``` + /// See the documentation of [`GuildId::edit_role`] for details. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES #[inline] - pub async fn edit_role( + pub async fn edit_role( &self, - http: impl AsRef, + cache_http: impl CacheHttp, role_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditRole) -> &mut EditRole, - { - self.id.edit_role(&http, role_id, f).await + builder: EditRole, + ) -> Result { + self.id.edit_role(cache_http, role_id, builder).await } /// Edits the order of [`Role`]s @@ -1357,80 +1241,89 @@ impl Guild { /// # Errors /// /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user - /// does not have permission to manage roles. - /// - /// Otherwise will return [`Error::Http`] if the current user does not have permission. + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Events]: Permissions::MANAGE_EVENTS - pub async fn edit_scheduled_event( + pub async fn edit_scheduled_event( &self, - http: impl AsRef, + cache_http: impl CacheHttp, event_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditScheduledEvent) -> &mut EditScheduledEvent, - { - self.id.edit_scheduled_event(&http, event_id, f).await + builder: EditScheduledEvent, + ) -> Result { + self.id.edit_scheduled_event(cache_http, event_id, builder).await } - /// Edits a sticker, optionally setting its fields. + /// Edits a sticker. /// - /// Requires the [Manage Emojis and Stickers] permission. + /// **Note**: Requires the [Manage Emojis and Stickers] permission. /// /// # Examples /// /// Rename a sticker: /// - /// ```rust,ignore - /// guild.edit_sticker(&context, StickerId(7), |r| r.name("Bun bun meow")); + /// ```rust,no_run + /// # use serenity::http::Http; + /// # use serenity::model::guild::Guild; + /// # use serenity::model::id::GuildId; + /// use serenity::builder::EditSticker; + /// use serenity::model::id::StickerId; + /// + /// # async fn run() -> Result<(), Box> { + /// # let http = Http::new("token"); + /// # let guild = Guild::get(&http, GuildId::new(7)).await?; + /// let builder = EditSticker::default().name("Bun bun meow"); + /// guild.edit_sticker(&http, StickerId::new(7), builder).await?; + /// # Ok(()) + /// # } /// ``` /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// - /// [Manage Emojis and Stickers]: crate::model::permissions::Permissions::MANAGE_EMOJIS_AND_STICKERS + /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS #[inline] - pub async fn edit_sticker( + pub async fn edit_sticker( &self, http: impl AsRef, sticker_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditSticker) -> &mut EditSticker, - { - self.id.edit_sticker(&http, sticker_id, f).await + builder: EditSticker, + ) -> Result { + self.id.edit_sticker(http, sticker_id, builder).await } - /// Edits the [`GuildWelcomeScreen`]. + /// Edits the guild's welcome screen. + /// + /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns an [`Error::Http`] if some mandatory fields are not provided. - pub async fn edit_welcome_screen( + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Guild]: Permissions::MANAGE_GUILD + pub async fn edit_welcome_screen( &self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut EditGuildWelcomeScreen) -> &mut EditGuildWelcomeScreen, - { - self.id.edit_welcome_screen(http, f).await + builder: EditGuildWelcomeScreen, + ) -> Result { + self.id.edit_welcome_screen(http, builder).await } - /// Edits the [`GuildWidget`]. + /// Edits the guild's widget. + /// + /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns an [`Error::Http`] if the bot does not have the `MANAGE_GUILD` - /// permission. - pub async fn edit_widget(&self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut EditGuildWidget) -> &mut EditGuildWidget, - { - self.id.edit_widget(http, f).await + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Guild]: Permissions::MANAGE_GUILD + pub async fn edit_widget( + &self, + http: impl AsRef, + builder: EditGuildWidget, + ) -> Result { + self.id.edit_widget(http, builder).await } /// Gets a partial amount of guild data by its Id. diff --git a/src/model/guild/partial_guild.rs b/src/model/guild/partial_guild.rs index ca81f4d9e41..fe0499d12e7 100644 --- a/src/model/guild/partial_guild.rs +++ b/src/model/guild/partial_guild.rs @@ -191,53 +191,39 @@ impl PartialGuild { /// /// # Examples /// - /// Create a custom keyword filter to block the message and timeout the author. - /// - /// ```ignore - /// use serenity::model::guild::automod::{Action, Trigger}; - /// - /// let _rule = guild - /// .create_automod_rule(&http, |r| { - /// r.name("foobar filter") - /// .trigger(Trigger::Keyword(vec!["foo*".to_string(), "*bar".to_string()])) - /// .actions(vec![Action::BlockMessage, Action::Timeout(60)]) - /// }) - /// .await; - /// ``` + /// See [`GuildId::create_automod_rule`] for details. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD #[inline] pub async fn create_automod_rule( - self, + &self, http: impl AsRef, - f: impl FnOnce(&mut EditAutoModRule) -> &mut EditAutoModRule, + builder: EditAutoModRule, ) -> Result { - self.id.create_automod_rule(http, f).await + self.id.create_automod_rule(http, builder).await } - /// Edit an auto moderation [`Rule`] by its ID. + /// Edit an auto moderation [`Rule`], given its Id. /// /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD #[inline] pub async fn edit_automod_rule( - self, + &self, http: impl AsRef, rule_id: impl Into, - f: impl FnOnce(&mut EditAutoModRule) -> &mut EditAutoModRule, + builder: EditAutoModRule, ) -> Result { - self.id.edit_automod_rule(http, rule_id, f).await + self.id.edit_automod_rule(http, rule_id, builder).await } /// Deletes an auto moderation [`Rule`] from the guild. @@ -382,32 +368,42 @@ impl PartialGuild { /// /// Refer to [`Http::create_channel`] for more information. /// - /// Requires the [Manage Channels] permission. + /// **Note**: Requires the [Manage Channels] permission. /// /// # Examples /// /// Create a voice channel in a guild with the name `test`: /// - /// ```rust,ignore - /// use serenity::model::ChannelType; + /// ```rust,no_run + /// # use serenity::http::Http; + /// # use serenity::model::guild::PartialGuild; + /// # use serenity::model::id::GuildId; + /// use serenity::builder::CreateChannel; + /// use serenity::model::channel::ChannelType; /// - /// guild.create_channel(|c| c.name("test").kind(ChannelType::Voice)); + /// # async fn run() -> Result<(), Box> { + /// # let http = Http::new("token"); + /// # let guild = PartialGuild::get(&http, GuildId::new(7)).await?; + /// let builder = CreateChannel::default().name("my-test-channel").kind(ChannelType::Text); + /// + /// // assuming a `guild` has already been bound + /// let _channel = guild.create_channel(&http, builder).await?; + /// # Ok(()) + /// # } /// ``` /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid data was given, such as the channel name being - /// too long. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - #[inline] pub async fn create_channel( &self, - http: impl AsRef, - f: impl FnOnce(&mut CreateChannel) -> &mut CreateChannel, + cache_http: impl CacheHttp, + builder: CreateChannel, ) -> Result { - self.id.create_channel(&http, f).await + self.id.create_channel(cache_http, builder).await } /// Creates an emoji in the guild with a name and base64-encoded image. @@ -460,85 +456,64 @@ impl PartialGuild { self.id.create_integration(&http, integration_id, kind).await } - /// Creates a guild specific [`Command`] + /// Create a guild specific application [`Command`]. /// - /// **Note**: Unlike global `Command`s, guild commands will update instantly. + /// **Note**: Unlike global commands, guild commands will update instantly. /// /// # Errors /// - /// Returns the same possible errors as `create_global_application_command`. + /// See [`CreateApplicationCommand::execute`] for a list of possible errors. #[inline] - pub async fn create_application_command( + pub async fn create_application_command( &self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, - { - self.id.create_application_command(http, f).await + builder: CreateApplicationCommand, + ) -> Result { + self.id.create_application_command(http, builder).await } - /// Overrides all guild application commands. - /// - /// [`create_application_command`]: Self::create_application_command + /// Override all guild application commands. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn set_application_commands( + /// See [`CreateApplicationCommands::execute`] for a list of possible errors. + pub async fn set_application_commands( &self, http: impl AsRef, - f: F, - ) -> Result> - where - F: FnOnce(&mut CreateApplicationCommands) -> &mut CreateApplicationCommands, - { - self.id.set_application_commands(http, f).await + builder: CreateApplicationCommands, + ) -> Result> { + self.id.set_application_commands(http, builder).await } - /// Creates a guild specific [`CommandPermission`]. + /// Create a guild specific [`CommandPermission`]. /// /// **Note**: It will update instantly. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn create_application_command_permission( + /// See [`CreateApplicationCommandPermissionsData::execute`] for a list of possible errors. + pub async fn create_application_command_permission( &self, http: impl AsRef, command_id: CommandId, - f: F, - ) -> Result - where - F: FnOnce( - &mut CreateApplicationCommandPermissionsData, - ) -> &mut CreateApplicationCommandPermissionsData, - { - self.id.create_application_command_permission(http, command_id, f).await + builder: CreateApplicationCommandPermissionsData, + ) -> Result { + self.id.create_application_command_permission(http, command_id, builder).await } - /// Same as [`create_application_command_permission`] but allows to create - /// more than one permission per call. - /// - /// [`create_application_command_permission`]: Self::create_application_command_permission + /// Override permissions for all guild application commands. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. + /// See [`CreateApplicationCommandsPermissions::execute`] for a list of possible errors. #[deprecated(note = "use `create_appliction_command_permission`.")] #[allow(deprecated)] - pub async fn set_application_commands_permissions( + pub async fn set_application_commands_permissions( &self, http: impl AsRef, - f: F, - ) -> Result> - where - F: FnOnce( - &mut crate::builder::CreateApplicationCommandsPermissions, - ) -> &mut crate::builder::CreateApplicationCommandsPermissions, - { - self.id.set_application_commands_permissions(http, f).await + builder: crate::builder::CreateApplicationCommandsPermissions, + ) -> Result> { + self.id.set_application_commands_permissions(http, builder).await } /// Get all guild application commands. @@ -563,21 +538,18 @@ impl PartialGuild { self.id.get_application_command(http, command_id).await } - /// Edit guild application command by its Id. + /// Edit a guild application command, given its Id. /// /// # Errors /// - /// If there is an error, it will be either [`Error::Http`] or [`Error::Json`]. - pub async fn edit_application_command( + /// See [`CreateApplicationCommand::execute`] for a list of possible errors. + pub async fn edit_application_command( &self, http: impl AsRef, command_id: CommandId, - f: F, - ) -> Result - where - F: FnOnce(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, - { - self.id.edit_application_command(http, command_id, f).await + builder: CreateApplicationCommand, + ) -> Result { + self.id.edit_application_command(http, command_id, builder).await } /// Delete guild application command by its Id. @@ -626,16 +598,13 @@ impl PartialGuild { /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if an invalid value was set. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES #[inline] - pub async fn create_role(&self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut EditRole) -> &mut EditRole, - { - self.id.create_role(&http, f).await + pub async fn create_role(&self, cache_http: impl CacheHttp, builder: EditRole) -> Result { + self.id.create_role(cache_http, builder).await } /// Creates a new sticker in the guild with the data set, if any. @@ -644,26 +613,16 @@ impl PartialGuild { /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] - /// if the current user does not have permission to manage roles. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// - /// [Manage Emojis and Stickers]: crate::model::permissions::Permissions::MANAGE_EMOJIS_AND_STICKERS - pub async fn create_sticker<'a, F>(&self, cache_http: impl CacheHttp, f: F) -> Result - where - for<'b> F: FnOnce(&'b mut CreateSticker<'a>) -> &'b mut CreateSticker<'a>, - { - #[cfg(feature = "cache")] - { - if cache_http.cache().is_some() { - let req = Permissions::MANAGE_EMOJIS_AND_STICKERS; - - if !self.has_perms(&cache_http, req).await { - return Err(Error::Model(ModelError::InvalidPermissions(req))); - } - } - } - - self.id.create_sticker(cache_http.http(), f).await + /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS + pub async fn create_sticker<'a>( + &self, + cache_http: impl CacheHttp, + builder: CreateSticker<'a>, + ) -> Result { + self.id.create_sticker(cache_http, builder).await } /// Deletes the current guild if the current user is the owner of the @@ -761,38 +720,31 @@ impl PartialGuild { /// Edits the current guild with new data where specified. /// - /// **Note**: Requires the current user to have the [Manage Guild] - /// permission. + /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns [`Error::Http`] if an invalid value is set, or if the current user - /// lacks permission to edit the guild. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - pub async fn edit(&mut self, http: impl AsRef, f: F) -> Result<()> - where - F: FnOnce(&mut EditGuild) -> &mut EditGuild, - { - match self.id.edit(&http, f).await { - Ok(guild) => { - self.afk_channel_id = guild.afk_channel_id; - self.afk_timeout = guild.afk_timeout; - self.default_message_notifications = guild.default_message_notifications; - self.emojis = guild.emojis; - self.features = guild.features; - self.icon = guild.icon; - self.mfa_level = guild.mfa_level; - self.name = guild.name; - self.owner_id = guild.owner_id; - self.roles = guild.roles; - self.splash = guild.splash; - self.verification_level = guild.verification_level; - - Ok(()) - }, - Err(why) => Err(why), - } + pub async fn edit(&mut self, cache_http: impl CacheHttp, builder: EditGuild) -> Result<()> { + let guild = self.id.edit(cache_http, builder).await?; + + self.afk_channel_id = guild.afk_channel_id; + self.afk_timeout = guild.afk_timeout; + self.default_message_notifications = guild.default_message_notifications; + self.emojis = guild.emojis; + self.features = guild.features; + self.icon = guild.icon; + self.mfa_level = guild.mfa_level; + self.name = guild.name; + self.owner_id = guild.owner_id; + self.roles = guild.roles; + self.splash = guild.splash; + self.verification_level = guild.verification_level; + + Ok(()) } /// Edits an [`Emoji`]'s name in the guild. @@ -818,36 +770,27 @@ impl PartialGuild { self.id.edit_emoji(&http, emoji_id, name).await } - /// Edits the properties of member of the guild, such as muting or - /// nicknaming them. + /// Edits the properties a guild member, such as muting or nicknaming them. Returns the new + /// member. /// - /// Refer to [`EditMember`]'s documentation for a full list of methods and - /// permission restrictions. + /// Refer to the documentation of [`EditMember`] for a full list of methods and permission + /// restrictions. /// /// # Examples /// - /// Mute a member and set their roles to just one role with a predefined Id: - /// - /// ```rust,ignore - /// use serenity::model::GuildId; - /// - /// GuildId::new(7).edit_member(user_id, |m| m.mute(true).roles(&vec![role_id])).await; - /// ``` + /// See [`GuildId::edit_member`] for details. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks the necessary permissions. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. #[inline] - pub async fn edit_member( + pub async fn edit_member( &self, http: impl AsRef, user_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditMember) -> &mut EditMember, - { - self.id.edit_member(&http, user_id, f).await + builder: EditMember, + ) -> Result { + self.id.edit_member(http, user_id, builder).await } /// Edits the current user's nickname for the guild. @@ -873,32 +816,26 @@ impl PartialGuild { /// Edits a role, optionally setting its fields. /// - /// Requires the [Manage Roles] permission. + /// **Note**: Requires the [Manage Roles] permission. /// /// # Examples /// - /// Make a role hoisted: - /// - /// ```rust,ignore - /// partial_guild.edit_role(&context, RoleId::new(7), |r| r.hoist(true)); - /// ``` + /// See the documentation of [`GuildId::edit_role`] for details. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Manage Roles]: Permissions::MANAGE_ROLES #[inline] - pub async fn edit_role( + pub async fn edit_role( self, - http: impl AsRef, + cache_http: impl CacheHttp, role_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditRole) -> &mut EditRole, - { - self.id.edit_role(http, role_id, f).await + builder: EditRole, + ) -> Result { + self.id.edit_role(cache_http, role_id, builder).await } /// Edits the order of [`Role`]s @@ -928,63 +865,77 @@ impl PartialGuild { self.id.edit_role_position(&http, role_id, position).await } - /// Edits a sticker, optionally setting its fields. + /// Edits a sticker. /// - /// Requires the [Manage Emojis and Stickers] permission. + /// **Note**: Requires the [Manage Emojis and Stickers] permission. /// /// # Examples /// /// Rename a sticker: /// - /// ```rust,ignore - /// guild.edit_sticker(&context, StickerId(7), |r| r.name("Bun bun meow")); + /// ```rust,no_run + /// # use serenity::http::Http; + /// # use serenity::model::guild::PartialGuild; + /// # use serenity::model::id::GuildId; + /// use serenity::builder::EditSticker; + /// use serenity::model::id::StickerId; + /// + /// # async fn run() -> Result<(), Box> { + /// # let http = Http::new("token"); + /// # let guild = PartialGuild::get(&http, GuildId::new(7)).await?; + /// let builder = EditSticker::default().name("Bun bun meow"); + /// guild.edit_sticker(&http, StickerId::new(7), builder).await?; + /// # Ok(()) + /// # } /// ``` /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// - /// [Manage Emojis and Stickers]: crate::model::permissions::Permissions::MANAGE_EMOJIS_AND_STICKERS + /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS #[inline] - pub async fn edit_sticker( + pub async fn edit_sticker( &self, http: impl AsRef, sticker_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditSticker) -> &mut EditSticker, - { - self.id.edit_sticker(&http, sticker_id, f).await + builder: EditSticker, + ) -> Result { + self.id.edit_sticker(http, sticker_id, builder).await } - /// Edits the [`GuildWelcomeScreen`]. + /// Edits the guild's welcome screen. + /// + /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns an [`Error::Http`] if some mandatory fields are not provided. - pub async fn edit_welcome_screen( + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Guild]: Permissions::MANAGE_GUILD + pub async fn edit_welcome_screen( &self, http: impl AsRef, - f: F, - ) -> Result - where - F: FnOnce(&mut EditGuildWelcomeScreen) -> &mut EditGuildWelcomeScreen, - { - self.id.edit_welcome_screen(http, f).await + builder: EditGuildWelcomeScreen, + ) -> Result { + self.id.edit_welcome_screen(http, builder).await } - /// Edits the [`GuildWidget`]. + /// Edits the guild's widget. + /// + /// **Note**: Requires the [Manage Guild] permission. /// /// # Errors /// - /// Returns an [`Error::Http`] if the bot does not have the `MANAGE_GUILD` - /// permission. - pub async fn edit_widget(&self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut EditGuildWidget) -> &mut EditGuildWidget, - { - self.id.edit_widget(http, f).await + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Guild]: Permissions::MANAGE_GUILD + pub async fn edit_widget( + &self, + http: impl AsRef, + builder: EditGuildWidget, + ) -> Result { + self.id.edit_widget(http, builder).await } /// Gets a partial amount of guild data by its Id. diff --git a/src/model/guild/role.rs b/src/model/guild/role.rs index 2e1bd769c54..a47808cf26f 100644 --- a/src/model/guild/role.rs +++ b/src/model/guild/role.rs @@ -151,14 +151,7 @@ impl Role { /// /// # Examples /// - /// Make a role hoisted: - /// - /// ```rust,ignore - /// # use serenity::model::id::RoleId; - /// # let role = RoleId::new(7).to_role_cached(&cache).unwrap(); - /// // assuming a `role` has already been bound - /// role.edit(|r| r.hoist(true)); - /// ``` + /// See the documentation of [`EditRole`] for details. /// /// # Errors /// @@ -167,12 +160,9 @@ impl Role { /// /// [Manage Roles]: Permissions::MANAGE_ROLES #[inline] - pub async fn edit( - &self, - http: impl AsRef, - f: impl FnOnce(&mut EditRole) -> &mut EditRole, - ) -> Result { - self.guild_id.edit_role(http, self.id, f).await + pub async fn edit(&mut self, http: impl AsRef, builder: EditRole) -> Result<()> { + *self = self.guild_id.edit_role(http.as_ref(), self.id, builder).await?; + Ok(()) } /// Check that the role has the given permission. diff --git a/src/model/invite.rs b/src/model/invite.rs index bfeeee1b46e..3b41bf9d5ad 100644 --- a/src/model/invite.rs +++ b/src/model/invite.rs @@ -1,8 +1,6 @@ //! Models for server and channel invites. use super::prelude::*; -#[cfg(all(feature = "cache", feature = "model"))] -use super::{utils as model_utils, Permissions}; #[cfg(feature = "model")] use crate::builder::CreateInvite; #[cfg(all(feature = "cache", feature = "model"))] @@ -11,7 +9,6 @@ use crate::cache::Cache; use crate::http::{CacheHttp, Http}; #[cfg(feature = "model")] use crate::internal::prelude::*; -use crate::model::Timestamp; /// Information about an invite code. /// @@ -64,47 +61,23 @@ pub struct Invite { #[cfg(feature = "model")] impl Invite { - /// Creates an invite for a [`GuildChannel`], providing a builder so that - /// fields may optionally be set. - /// - /// See the documentation for the [`CreateInvite`] builder for information - /// on how to use this and the default values that it provides. + /// Creates an invite for the given channel. /// - /// Requires the [Create Instant Invite] permission. + /// **Note**: Requires the [Create Instant Invite] permission. /// /// # Errors /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] - /// if the current user does not have the required [permission]. + /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user + /// lacks permission. Otherwise returns [`Error::Http`], as well as if invalid data is given. /// /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE - /// [permission]: super::permissions #[inline] - pub async fn create( + pub async fn create( cache_http: impl CacheHttp, channel_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(CreateInvite) -> CreateInvite, - { - let channel_id = channel_id.into(); - - #[cfg(feature = "cache")] - { - if let Some(cache) = cache_http.cache() { - model_utils::user_has_perms_cache( - cache, - channel_id, - None, - Permissions::CREATE_INSTANT_INVITE, - )?; - } - } - - let builder = f(CreateInvite::default()); - - cache_http.http().create_invite(channel_id.get(), &builder, None).await + builder: CreateInvite, + ) -> Result { + channel_id.into().create_invite(cache_http, builder).await } /// Deletes the invite. @@ -126,7 +99,7 @@ impl Invite { { if let Some(cache) = cache_http.cache() { let guild_id = self.guild.as_ref().map(|g| g.id); - model_utils::user_has_perms_cache( + crate::utils::user_has_perms_cache( cache, self.channel.id, guild_id, @@ -353,7 +326,7 @@ impl RichInvite { if let Some(cache) = cache_http.cache() { let guild_id = self.guild.as_ref().map(|g| g.id); - model_utils::user_has_perms_cache( + crate::utils::user_has_perms_cache( cache, self.channel.id, guild_id, diff --git a/src/model/mention.rs b/src/model/mention.rs index 862ed245738..b397611496c 100644 --- a/src/model/mention.rs +++ b/src/model/mention.rs @@ -23,6 +23,7 @@ pub trait Mentionable { /// /// ``` /// # #[cfg(feature = "client")] { + /// # use serenity::builder::CreateMessage; /// # use serenity::model::guild::Member; /// # use serenity::model::channel::GuildChannel; /// # use serenity::model::id::ChannelId; @@ -35,18 +36,14 @@ pub trait Mentionable { /// to_channel: GuildChannel, /// rules_channel: ChannelId, /// ) -> Result<(), Error> { - /// to_channel - /// .id - /// .send_message(ctx, |m| { - /// m.content(format!( - /// "Hi {member}, welcome to the server! \ - /// Please refer to {rules} for our code of conduct, \ - /// and enjoy your stay.", - /// member = member.mention(), - /// rules = rules_channel.mention(), - /// )) - /// }) - /// .await?; + /// let builder = CreateMessage::default().content(format!( + /// "Hi {member}, welcome to the server! \ + /// Please refer to {rules} for our code of conduct, \ + /// and enjoy your stay.", + /// member = member.mention(), + /// rules = rules_channel.mention(), + /// )); + /// to_channel.id.send_message(ctx, builder).await?; /// Ok(()) /// } /// # } diff --git a/src/model/sticker/mod.rs b/src/model/sticker/mod.rs index e8757aaed51..291b99c761f 100644 --- a/src/model/sticker/mod.rs +++ b/src/model/sticker/mod.rs @@ -80,30 +80,38 @@ impl Sticker { } } - /// Edits a sticker, optionally setting its fields. + /// Edits the sticker. /// - /// Requires the [Manage Emojis and Stickers] permission. + /// **Note**: Requires the [Manage Emojis and Stickers] permission. /// /// # Examples /// /// Rename a sticker: /// - /// ```rust,ignore - /// guild.edit_sticker(&context, StickerId(7), |r| r.name("Bun bun meow")); + /// ```rust,no_run + /// # use serenity::http::Http; + /// # use serenity::model::id::{GuildId, StickerId}; + /// use serenity::builder::EditSticker; + /// + /// # async fn run() -> Result<(), Box> { + /// # let http = Http::new("token"); + /// # let mut sticker = GuildId::new(7).sticker(&http, StickerId::new(7)).await?; + /// let builder = EditSticker::default().name("Bun bun meow"); + /// sticker.edit(&http, builder).await?; + /// # Ok(()) + /// # } /// ``` /// /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission. /// - /// [Manage Emojis and Stickers]: crate::model::permissions::Permissions::MANAGE_EMOJIS_AND_STICKERS + /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS #[inline] - pub async fn edit(&self, http: impl AsRef, f: F) -> Result - where - F: FnOnce(&mut EditSticker) -> &mut EditSticker, - { + pub async fn edit(&mut self, http: impl AsRef, builder: EditSticker) -> Result<()> { if let Some(guild_id) = self.guild_id { - guild_id.edit_sticker(&http, self.id, f).await + *self = self.id.edit(http, guild_id, builder).await?; + Ok(()) } else { Err(Error::Model(ModelError::DeleteNitroSticker)) } diff --git a/src/model/sticker/sticker_id.rs b/src/model/sticker/sticker_id.rs index c5bf3a87004..3db5f20e3d7 100644 --- a/src/model/sticker/sticker_id.rs +++ b/src/model/sticker/sticker_id.rs @@ -27,31 +27,27 @@ impl StickerId { /// /// # Errors /// - /// Returns [`Error::Http`] if a [`Sticker`] with that [`StickerId`] does - /// not exist, or is otherwise unavailable. + /// Returns [`Error::Http`] if a [`Sticker`] with that [`StickerId`] does not exist, or is + /// otherwise unavailable. pub async fn to_sticker(&self, http: impl AsRef) -> Result { http.as_ref().get_sticker(self.get()).await } /// Edits the sticker. /// - /// **Note**: The [Manage Emojis and Stickers] permission is required. + /// **Note**: Requires the [Manage Emojis and Stickers] permission. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid edits are given. + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. /// - /// [Manage Emojis and Stickers]: crate::model::permissions::Permissions::MANAGE_EMOJIS_AND_STICKERS - pub async fn edit( + /// [Manage Emojis and Stickers]: Permissions::MANAGE_EMOJIS_AND_STICKERS + pub async fn edit( &self, http: impl AsRef, guild_id: impl Into, - f: F, - ) -> Result - where - F: FnOnce(&mut EditSticker) -> &mut EditSticker, - { - guild_id.into().edit_sticker(http, self, f).await + builder: EditSticker, + ) -> Result { + guild_id.into().edit_sticker(http, self, builder).await } } diff --git a/src/model/user.rs b/src/model/user.rs index d1fc1785214..26fb40a034b 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -238,34 +238,25 @@ impl CurrentUser { /// Change the avatar: /// /// ```rust,no_run + /// # use serenity::builder::EditProfile; /// # use serenity::http::Http; /// # use serenity::model::user::CurrentUser; /// # /// # async fn run() -> Result<(), Box> { /// # let http = Http::new("token"); /// # let mut user = CurrentUser::default(); - /// let avatar = serenity::utils::read_image("./avatar.png")?; - /// - /// user.edit(&http, |p| p.avatar(Some(avatar))).await; + /// let builder = EditProfile::default().avatar(&http, "./avatar.png").await?; + /// user.edit(&http, builder).await; /// # Ok(()) /// # } /// ``` /// /// # Errors /// - /// Returns an [`Error::Http`] if an invalid value is set. - /// May also return an [`Error::Json`] if there is an error in - /// deserializing the API response. - pub async fn edit(&mut self, http: impl AsRef, f: F) -> Result<()> - where - F: FnOnce(&mut EditProfile) -> &mut EditProfile, - { - let mut edit_profile = EditProfile::default(); - edit_profile.username(self.name.clone()); - f(&mut edit_profile); - - *self = http.as_ref().edit_profile(&edit_profile).await?; - + /// Returns an [`Error::Http`] if an invalid value is set. May also return an [`Error::Json`] + /// if there is an error in deserializing the API response. + pub async fn edit(&mut self, http: impl AsRef, builder: EditProfile) -> Result<()> { + *self = builder.execute(http).await?; Ok(()) } @@ -468,11 +459,11 @@ impl CurrentUser { permissions: Permissions, scopes: &[Scope], ) -> Result { - let mut builder = CreateBotAuthParameters::default(); - - builder.permissions(permissions); - builder.auto_client_id(http).await?; - builder.scopes(scopes); + let builder = CreateBotAuthParameters::default() + .permissions(permissions) + .scopes(scopes) + .auto_client_id(http) + .await?; Ok(builder.build()) } @@ -799,8 +790,8 @@ impl User { default_avatar_url(self.discriminator) } - /// Sends a message to a user through a direct message channel. This is a - /// channel that can only be accessed by you and the recipient. + /// Sends a message to a user through a direct message channel. This is a channel that can only + /// be accessed by you and the recipient. /// /// # Examples /// @@ -808,10 +799,11 @@ impl User { /// help message, and then react with `'👌'` to verify message sending: /// /// ```rust,no_run - /// # #[cfg(feature="client")] { + /// # #[cfg(feature = "client")] { /// # use serenity::prelude::*; /// # use serenity::model::prelude::*; /// # + /// use serenity::builder::CreateMessage; /// use serenity::model::Permissions; /// /// struct Handler; @@ -826,14 +818,14 @@ impl User { /// Ok(v) => v, /// Err(why) => { /// println!("Error creating invite url: {:?}", why); - /// /// return; /// }, /// }; /// - /// let help = format!("Helpful info here. Invite me with this link: <{}>", url,); + /// let help = format!("Helpful info here. Invite me with this link: <{}>", url); /// - /// let dm = msg.author.direct_message(&ctx, |m| m.content(&help)).await; + /// let builder = CreateMessage::default().content(help); + /// let dm = msg.author.direct_message(&ctx, builder).await; /// /// match dm { /// Ok(_) => { @@ -859,29 +851,28 @@ impl User { /// /// # Errors /// - /// Returns a [`ModelError::MessagingBot`] if the user being direct messaged - /// is a bot user. + /// Returns a [`ModelError::MessagingBot`] if the user being direct messaged is a bot user. /// - /// May also return an [`Error::Http`] if the message was illformed, or if the - /// user cannot be sent a direct message. + /// May also return an [`Error::Http`] if the user cannot be sent a direct message. /// - /// [`Error::Json`] can also be returned if there is an error deserializing - /// the API response. - pub async fn direct_message<'a, F>(&self, cache_http: impl CacheHttp, f: F) -> Result - where - for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>, - { - self.create_dm_channel(&cache_http).await?.send_message(&cache_http.http(), f).await + /// Returns an [`Error::Json`] if there is an error deserializing the API response. + pub async fn direct_message<'a>( + &self, + cache_http: impl CacheHttp, + builder: CreateMessage<'a>, + ) -> Result { + self.create_dm_channel(&cache_http).await?.send_message(cache_http, builder).await } /// This is an alias of [`Self::direct_message`]. #[allow(clippy::missing_errors_doc)] #[inline] - pub async fn dm<'a, F>(&self, cache_http: impl CacheHttp, f: F) -> Result - where - for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>, - { - self.direct_message(cache_http, f).await + pub async fn dm<'a>( + &self, + cache_http: impl CacheHttp, + builder: CreateMessage<'a>, + ) -> Result { + self.direct_message(cache_http, builder).await } /// Retrieves the URL to the user's avatar, falling back to the default diff --git a/src/model/utils.rs b/src/model/utils.rs index 64623799366..ef991aa77f6 100644 --- a/src/model/utils.rs +++ b/src/model/utils.rs @@ -5,13 +5,7 @@ use std::marker::PhantomData; use serde::ser::{Serialize, SerializeSeq, Serializer}; -#[cfg(all(feature = "cache", feature = "model"))] -use super::permissions::Permissions; use super::prelude::*; -#[cfg(all(feature = "cache", feature = "model"))] -use crate::cache::Cache; -#[cfg(feature = "cache")] -use crate::internal::prelude::*; pub fn default_true() -> bool { true @@ -292,76 +286,6 @@ pub fn serialize_map_values( seq.end() } -/// Tries to find a user's permissions using the cache. -/// Unlike [`user_has_perms`], this function will return `true` even when -/// the permissions are not in the cache. -#[cfg(all(feature = "cache", feature = "model"))] -#[inline] -pub fn user_has_perms_cache( - cache: impl AsRef, - channel_id: ChannelId, - guild_id: Option, - permissions: Permissions, -) -> Result<()> { - if match user_has_perms(cache, channel_id, guild_id, permissions) { - Err(Error::Model(err)) => err.is_cache_err(), - result => result?, - } { - Ok(()) - } else { - Err(Error::Model(ModelError::InvalidPermissions(permissions))) - } -} - -#[cfg(all(feature = "cache", feature = "model"))] -pub fn user_has_perms( - cache: impl AsRef, - channel_id: ChannelId, - guild_id: Option, - mut permissions: Permissions, -) -> Result { - let cache = cache.as_ref(); - - let channel = match cache.channel(channel_id) { - Some(channel) => channel, - None => return Err(Error::Model(ModelError::ChannelNotFound)), - }; - - // Both users in DMs, all users in groups, and maybe all channels in categories - // will have the same permissions. - // - // The only exception to this is when the current user is blocked by - // the recipient in a DM channel, preventing the current user - // from sending messages. - // - // Since serenity can't _reasonably_ check and keep track of these, - // just assume that all permissions are granted and return `true`. - let (guild_id, guild_channel) = match channel { - Channel::Guild(channel) => (channel.guild_id, channel), - Channel::Category(_) => return Ok(true), - Channel::Private(_) => match guild_id { - Some(_) => return Err(Error::Model(ModelError::InvalidChannelType)), - None => return Ok(true), - }, - }; - - let guild = match cache.guild(guild_id) { - Some(guild) => guild, - None => return Err(Error::Model(ModelError::GuildNotFound)), - }; - - let member = match guild.members.get(&cache.current_user().id) { - Some(member) => member, - None => return Err(Error::Model(ModelError::MemberNotFound)), - }; - - let perms = guild.user_permissions_in(&guild_channel, member)?; - - permissions.remove(perms); - - Ok(permissions.is_empty()) -} - /// Deserializes a sequence and builds a `HashMap` with the key extraction function. pub(in crate::model) struct SequenceToMapVisitor { key: F, diff --git a/src/model/webhook.rs b/src/model/webhook.rs index 8e3315e540f..2620165d6ae 100644 --- a/src/model/webhook.rs +++ b/src/model/webhook.rs @@ -67,7 +67,7 @@ pub struct Webhook { pub kind: WebhookType, /// The default avatar. /// - /// This can be modified via [`ExecuteWebhook::avatar_url`]. + /// This can be temporarily overridden via [`ExecuteWebhook::avatar_url`]. pub avatar: Option, /// The Id of the channel that owns the webhook. pub channel_id: Option, @@ -75,7 +75,7 @@ pub struct Webhook { pub guild_id: Option, /// The default name of the webhook. /// - /// This can be modified via [`ExecuteWebhook::username`]. + /// This can be temporarily overridden via [`ExecuteWebhook::username`]. pub name: Option, /// The webhook's secure token. pub token: Option, @@ -214,13 +214,15 @@ impl Webhook { http.as_ref().delete_webhook_with_token(self.id.get(), token).await } - /// Edits the webhook + /// Edits the webhook. + /// + /// Does not require authentication, as a token is required. /// - /// Does not require authentication, as this calls [`Http::edit_webhook_with_token`] internally. /// # Examples /// /// ```rust,no_run /// # use serenity::http::Http; + /// # use serenity::builder::EditWebhook; /// # use serenity::model::webhook::Webhook; /// # /// # async fn run() -> Result<(), Box> { @@ -228,45 +230,33 @@ impl Webhook { /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// let mut webhook = Webhook::from_url(&http, url).await?; /// - /// webhook.edit(&http, |b| b.name("new name")).await?; + /// let builder = EditWebhook::default().name("new name"); + /// webhook.edit(&http, builder).await?; /// # Ok(()) /// # } /// ``` /// /// # Errors /// - /// Returns an [`Error::Model`] if the [`Self::token`] is [`None`]. + /// Returns an [`Error::Model`] if [`Self::token`] is [`None`]. /// /// May also return an [`Error::Http`] if the content is malformed, or if the token is invalid. /// /// Or may return an [`Error::Json`] if there is an error in deserialising Discord's response. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn edit(&mut self, http: impl AsRef, f: F) -> Result<()> - where - F: FnOnce(&mut EditWebhook) -> &mut EditWebhook, - { + pub async fn edit(&mut self, http: impl AsRef, builder: EditWebhook) -> Result<()> { let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?; - - let mut builder = EditWebhook::default(); - f(&mut builder); - - *self = http.as_ref().edit_webhook_with_token(self.id.get(), token, &builder).await?; + *self = builder.execute(http, self.id, token).await?; Ok(()) } /// Executes a webhook with the fields set via the given builder. /// - /// The builder provides a method of setting only the fields you need, - /// without needing to pass a long set of arguments. - /// /// # Examples /// /// Execute a webhook with message content of `test`: /// /// ```rust,no_run + /// # use serenity::builder::ExecuteWebhook; /// # use serenity::http::Http; /// # use serenity::model::webhook::Webhook; /// # @@ -275,13 +265,14 @@ impl Webhook { /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// let mut webhook = Webhook::from_url(&http, url).await?; /// - /// webhook.execute(&http, false, |w| w.content("test")).await?; + /// let builder = ExecuteWebhook::default().content("test"); + /// webhook.execute(&http, false, builder).await?; /// # Ok(()) /// # } /// ``` /// - /// Execute a webhook with message content of `test`, overriding the - /// username to `serenity`, and sending an embed: + /// Execute a webhook with message content of `test`, overriding the username to `serenity`, + /// and sending an embed: /// /// ```rust,no_run /// # use serenity::http::Http; @@ -289,122 +280,41 @@ impl Webhook { /// # /// # async fn run() -> Result<(), Box> { /// # let http = Http::new("token"); - /// use serenity::model::channel::Embed; + /// use serenity::builder::{CreateEmbed, ExecuteWebhook}; /// /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// let mut webhook = Webhook::from_url(&http, url).await?; /// - /// let embed = Embed::fake(|e| { - /// e.title("Rust's website") - /// .description( - /// "Rust is a systems programming language that runs - /// blazingly fast, prevents segfaults, and guarantees - /// thread safety.", - /// ) - /// .url("https://rust-lang.org") - /// }); - /// - /// webhook - /// .execute(&http, false, |w| w.content("test").username("serenity").embeds(vec![embed])) - /// .await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// # Errors - /// - /// Returns an [`Error::Model`] if the [`Self::token`] is [`None`]. - /// - /// May also return an [`Error::Http`] if the content is malformed, or if the webhook's token is invalid. - /// - /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response. - #[inline] - pub async fn execute<'a, F>( - &self, - http: impl AsRef, - wait: bool, - f: F, - ) -> Result> - where - for<'b> F: FnOnce(&'b mut ExecuteWebhook<'a>) -> &'b mut ExecuteWebhook<'a>, - { - self._execute(http, None, wait, f).await - } - - /// Executes a webhook with the fields set via the given builder, in the context of the thread - /// with the provided Id. If the thread is archived, it will be automatically unarchived. - /// - /// # Examples - /// - /// Execute a webhook with message content of `test`: + /// let embed = CreateEmbed::default() + /// .title("Rust's website") + /// .description( + /// "Rust is a systems programming language that runs blazingly fast, prevents \ + /// segfaults, and guarantees thread safety.", + /// ) + /// .url("https://rust-lang.org"); /// - /// ```rust,no_run - /// # use serenity::http::Http; - /// # use serenity::model::webhook::Webhook; - /// # - /// # async fn run() -> Result<(), Box> { - /// # let http = Http::new("token"); - /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; - /// let mut webhook = Webhook::from_url(&http, url).await?; - /// - /// webhook.execute_in_thread(&http, 12345, false, |w| w.content("test")).await?; + /// let builder = ExecuteWebhook::default().content("test").username("serenity").embed(embed); + /// webhook.execute(&http, false, builder).await?; /// # Ok(()) /// # } /// ``` /// /// # Errors /// - /// Returns an [`Error::Model`] if the [`Self::token`] is [`None`]. + /// Returns an [`Error::Model`] if [`Self::token`] is [`None`]. /// - /// May also return an [`Error::Http`] if the content is malformed, if the webhook's token is - /// invalid. Additionally, this variant is returned if `thread_id` does not refer to a valid - /// thread, or if the thread it refers to does not belong to the webhook's associated - /// [`Channel`]. + /// May also return an [`Error::Http`] if the content is malformed, or if the webhook's token is invalid. /// /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - #[inline] - pub async fn execute_in_thread<'a, F>( - &self, - http: impl AsRef, - thread_id: impl Into, - wait: bool, - f: F, - ) -> Result> - where - for<'b> F: FnOnce(&'b mut ExecuteWebhook<'a>) -> &'b mut ExecuteWebhook<'a>, - { - self._execute(http, Some(thread_id.into()), wait, f).await - } - #[inline] - async fn _execute<'a, F>( + pub async fn execute<'a>( &self, http: impl AsRef, - thread_id: Option, wait: bool, - f: F, - ) -> Result> - where - for<'b> F: FnOnce(&'b mut ExecuteWebhook<'a>) -> &'b mut ExecuteWebhook<'a>, - { + builder: ExecuteWebhook<'a>, + ) -> Result> { let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?; - let mut builder = ExecuteWebhook::default(); - f(&mut builder); - - let http = http.as_ref(); - let thread_id = thread_id.map(ChannelId::get); - let files = std::mem::take(&mut builder.files); - - if files.is_empty() { - http.execute_webhook(self.id.get(), thread_id, token, wait, &builder).await - } else { - http.execute_webhook_with_files(self.id.get(), thread_id, token, wait, files, &builder) - .await - } + builder.execute(http, self.id, token, wait).await } /// Gets a previously sent message from the webhook. @@ -429,28 +339,25 @@ impl Webhook { /// Edits a webhook message with the fields set via the given builder. /// + /// **Note**: Message contents must be under 2000 unicode code points. + /// /// # Errors /// - /// Returns an [`Error::Model`] if the [`Self::token`] is [`None`]. + /// Returns an [`Error::Model`] if [`Self::token`] is [`None`], or if the message content is + /// too long. /// - /// May also return an [`Error::Http`] if the content is malformed, the webhook's token is invalid, or - /// the given message Id does not belong to the current webhook. + /// May also return an [`Error::Http`] if the content is malformed, the webhook's token is + /// invalid, or the given message Id does not belong to the current webhook. /// /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response. - pub async fn edit_message( + pub async fn edit_message( &self, http: impl AsRef, message_id: MessageId, - f: F, - ) -> Result - where - F: FnOnce(&mut EditWebhookMessage) -> &mut EditWebhookMessage, - { + builder: EditWebhookMessage, + ) -> Result { let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?; - let mut builder = EditWebhookMessage::default(); - f(&mut builder); - - http.as_ref().edit_webhook_message(self.id.get(), token, message_id.get(), &builder).await + builder.execute(http, message_id, self.id, token).await } /// Deletes a webhook message. diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 5caa18d05dd..e5f5955033a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -31,9 +31,10 @@ use std::io::Read; use std::num::NonZeroU64; use std::path::Path; +#[cfg(all(feature = "cache", feature = "model"))] +use crate::cache::Cache; use crate::internal::prelude::*; -use crate::model::id::{ChannelId, EmojiId, RoleId, UserId}; -use crate::model::misc::EmojiIdentifier; +use crate::model::prelude::*; #[cfg(feature = "model")] pub(crate) fn encode_image(raw: &[u8]) -> String { @@ -444,6 +445,76 @@ pub fn parse_webhook(url: &Url) -> Option<(u64, &str)> { Some((webhook_id.parse().ok()?, token)) } +/// Tries to find a user's permissions using the cache. +/// Unlike [`user_has_perms`], this function will return `true` even when +/// the permissions are not in the cache. +#[cfg(all(feature = "cache", feature = "model"))] +#[inline] +pub(crate) fn user_has_perms_cache( + cache: impl AsRef, + channel_id: ChannelId, + guild_id: Option, + permissions: Permissions, +) -> Result<()> { + if match user_has_perms(cache, channel_id, guild_id, permissions) { + Err(Error::Model(err)) => err.is_cache_err(), + result => result?, + } { + Ok(()) + } else { + Err(Error::Model(ModelError::InvalidPermissions(permissions))) + } +} + +#[cfg(all(feature = "cache", feature = "model"))] +pub(crate) fn user_has_perms( + cache: impl AsRef, + channel_id: ChannelId, + guild_id: Option, + mut permissions: Permissions, +) -> Result { + let cache = cache.as_ref(); + + let channel = match cache.channel(channel_id) { + Some(channel) => channel, + None => return Err(Error::Model(ModelError::ChannelNotFound)), + }; + + // Both users in DMs, all users in groups, and maybe all channels in categories + // will have the same permissions. + // + // The only exception to this is when the current user is blocked by + // the recipient in a DM channel, preventing the current user + // from sending messages. + // + // Since serenity can't _reasonably_ check and keep track of these, + // just assume that all permissions are granted and return `true`. + let (guild_id, guild_channel) = match channel { + Channel::Guild(channel) => (channel.guild_id, channel), + Channel::Category(_) => return Ok(true), + Channel::Private(_) => match guild_id { + Some(_) => return Err(Error::Model(ModelError::InvalidChannelType)), + None => return Ok(true), + }, + }; + + let guild = match cache.guild(guild_id) { + Some(guild) => guild, + None => return Err(Error::Model(ModelError::GuildNotFound)), + }; + + let member = match guild.members.get(&cache.current_user().id) { + Some(member) => member, + None => return Err(Error::Model(ModelError::MemberNotFound)), + }; + + let perms = guild.user_permissions_in(&guild_channel, member)?; + + permissions.remove(perms); + + Ok(permissions.is_empty()) +} + /// Calculates the Id of the shard responsible for a guild, given its Id and /// total number of shards used. ///