diff --git a/examples/e03_struct_utilities/src/main.rs b/examples/e03_struct_utilities/src/main.rs index 29a854ba0a9..825ac1032ef 100644 --- a/examples/e03_struct_utilities/src/main.rs +++ b/examples/e03_struct_utilities/src/main.rs @@ -19,10 +19,14 @@ 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; - - if let Err(why) = dm { - println!("Error when direct messaging user: {:?}", why); + match msg.author.dm(&context).await { + Ok(create_message) => { + let msg = create_message.content("Hello!").execute(&context).await; + if let Err(why) = msg { + println!("Error when direct messaging user: {:?}", why); + } + }, + Err(why) => println!("Error when direct messaging user: {:?}", why), } } } diff --git a/examples/e09_create_message_builder/src/main.rs b/examples/e09_create_message_builder/src/main.rs index b8523e53aa9..37b9876c593 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}; use serenity::model::channel::Message; use serenity::model::gateway::Ready; use serenity::model::Timestamp; @@ -16,26 +17,27 @@ 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 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 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") - }) + .send_message() + .content("Hello, World!") + .embed(embed) + .add_file("./ferris_eyes.png") + .execute(&ctx.http) .await; if let Err(why) = msg { diff --git a/examples/e13_parallel_loops/src/main.rs b/examples/e13_parallel_loops/src/main.rs index 1c75f0e1166..910e91aed92 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; use serenity::gateway::ActivityData; use serenity::model::channel::Message; use serenity::model::gateway::Ready; @@ -79,23 +80,19 @@ 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 message = ChannelId::new(381926291785383946).send_message().embed(embed).execute(ctx).await; if let Err(why) = message { eprintln!("Error sending message: {:?}", why); }; diff --git a/examples/e14_slash_commands/src/main.rs b/examples/e14_slash_commands/src/main.rs index a58670862f9..e55402924e6 100644 --- a/examples/e14_slash_commands/src/main.rs +++ b/examples/e14_slash_commands/src/main.rs @@ -1,6 +1,7 @@ use std::env; use serenity::async_trait; +use serenity::builder::CreateInteractionResponseData; use serenity::model::application::command::{Command, CommandOptionType}; use serenity::model::application::interaction::application_command::CommandDataOptionValue; use serenity::model::application::interaction::{Interaction, InteractionResponseType}; @@ -56,12 +57,12 @@ impl EventHandler for Handler { _ => "not implemented :(".to_string(), }; + let data = CreateInteractionResponseData::default().content(content); if let Err(why) = command - .create_interaction_response(&ctx.http, |response| { - response - .kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|message| message.content(content)) - }) + .create_interaction_response() + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(data) + .execute(&ctx.http) .await { println!("Cannot respond to slash command: {}", why); diff --git a/examples/e17_message_components/src/main.rs b/examples/e17_message_components/src/main.rs index 83b48ba8857..223e7d8e79f 100644 --- a/examples/e17_message_components/src/main.rs +++ b/examples/e17_message_components/src/main.rs @@ -5,7 +5,7 @@ use std::{env, fmt}; use dotenv::dotenv; use serenity::async_trait; -use serenity::builder::{CreateActionRow, CreateButton, CreateSelectMenu, CreateSelectMenuOption}; +use serenity::builder::*; use serenity::client::{Context, EventHandler}; use serenity::futures::StreamExt; use serenity::model::application::component::ButtonStyle; @@ -172,10 +172,10 @@ 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.add_action_row(Animal::action_row())) - }) + .send_message() + .content("Please select your favorite animal") + .components(|c| c.add_action_row(Animal::action_row())) + .execute(&ctx) .await .unwrap(); @@ -195,14 +195,15 @@ impl EventHandler for Handler { let animal = Animal::from_str(mci.data.values.get(0).unwrap()).unwrap(); // Acknowledge the interaction and edit the message - mci.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.add_action_row(Sound::action_row())) - }) - }) - .await - .unwrap(); + let data = CreateInteractionResponseData::default() + .content(format!("You chose: **{}**\nNow choose a sound!", animal)) + .components(|c| c.add_action_row(Sound::action_row())); + mci.create_interaction_response() + .kind(InteractionResponseType::UpdateMessage) + .interaction_response_data(data) + .execute(&ctx) + .await + .unwrap(); // Wait for multiple interactions @@ -211,18 +212,18 @@ impl EventHandler for Handler { while let Some(mci) = cib.next().await { let sound = Sound::from_str(&mci.data.custom_id).unwrap(); + let data = CreateInteractionResponseData::default() + // Make the message hidden for other users by setting `ephemeral(true)`. + .ephemeral(true) + .content(format!("The **{}** says __{}__", animal, sound)); // Acknowledge the interaction and send a reply - mci.create_interaction_response(&ctx, |r| { + mci.create_interaction_response() // 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).content(format!("The **{}** says __{}__", animal, sound)) - }, - ) - }) - .await - .unwrap(); + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(data) + .execute(&ctx) + .await + .unwrap(); } // Delete the orig message or there will be dangling components diff --git a/src/builder/add_member.rs b/src/builder/add_member.rs index 9b125ff0a04..63b8bacde08 100644 --- a/src/builder/add_member.rs +++ b/src/builder/add_member.rs @@ -1,10 +1,24 @@ +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; use crate::model::id::RoleId; +#[cfg(feature = "http")] +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)] +#[derive(Clone, Debug, Serialize)] +#[must_use] pub struct AddMember { + #[cfg(feature = "http")] + #[serde(skip)] + guild_id: GuildId, + #[cfg(feature = "http")] + #[serde(skip)] + user_id: UserId, + #[serde(skip_serializing_if = "Option::is_none")] access_token: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -18,10 +32,27 @@ pub struct AddMember { } impl AddMember { + pub fn new( + #[cfg(feature = "http")] guild_id: GuildId, + #[cfg(feature = "http")] user_id: UserId, + ) -> Self { + Self { + #[cfg(feature = "http")] + guild_id, + #[cfg(feature = "http")] + user_id, + access_token: None, + nick: None, + roles: None, + mute: None, + deaf: None, + } + } + /// 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 +62,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,7 +72,7 @@ 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 { + pub fn roles(mut self, roles: impl IntoIterator>) -> Self { self.roles = Some(roles.into_iter().map(Into::into).collect()); self } @@ -51,7 +82,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,8 +92,21 @@ 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 } + + /// Adds a [`User`] to the corresponding 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 values are set. + #[cfg(feature = "http")] + pub async fn execute(self, http: impl AsRef) -> Result> { + http.as_ref().add_guild_member(self.guild_id.into(), self.user_id.into(), &self).await + } } diff --git a/src/builder/bot_auth_parameters.rs b/src/builder/bot_auth_parameters.rs index e8f1b67283e..6874403b01e 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: U) -> Self { self.client_id = Some(client_id.into()); self } @@ -69,7 +70,7 @@ impl CreateBotAuthParameters { /// /// [`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 +80,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: G) -> 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_channel.rs b/src/builder/create_channel.rs index 5954c1001f8..30b34ad7ad9 100644 --- a/src/builder/create_channel.rs +++ b/src/builder/create_channel.rs @@ -1,3 +1,7 @@ +#[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`]. @@ -7,7 +11,11 @@ use crate::model::prelude::*; /// [`GuildChannel`]: crate::model::channel::GuildChannel /// [`Guild`]: crate::model::guild::Guild #[derive(Debug, Clone, Serialize)] +#[must_use] pub struct CreateChannel { + #[cfg(feature = "http")] + #[serde(skip)] + id: GuildId, kind: ChannelType, #[serde(skip_serializing_if = "Option::is_none")] name: Option, @@ -29,55 +37,66 @@ pub struct CreateChannel { } impl CreateChannel { + /// Creates a builder with default values, setting [`Self::kind`] to [`ChannelType::Text`]. + pub fn new(#[cfg(feature = "http")] id: GuildId) -> Self { + Self { + #[cfg(feature = "http")] + id, + name: None, + nsfw: None, + topic: None, + bitrate: None, + position: None, + parent_id: None, + user_limit: None, + rate_limit_per_user: None, + kind: ChannelType::Text, + permission_overwrites: Vec::new(), + } + } + /// 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,16 +110,14 @@ 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 } @@ -129,44 +146,50 @@ impl CreateChannel { /// kind: PermissionOverwriteType::Member(UserId::new(1234)), /// }]; /// - /// guild.create_channel(http, |c| c.name("my_new_cool_channel").permissions(permissions)).await?; + /// guild.create_channel().name("my_cool_channel").permissions(permissions).execute(&http).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`]. + #[cfg(feature = "http")] + #[inline] + /// Creates a new [`Channel`] in the guild. /// - /// # Examples + /// **Note**: Requires the [Manage Channels] permission. /// - /// Create a default [`CreateChannel`] builder: + /// # Errors /// - /// ```rust - /// use serenity::builder::CreateChannel; + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// does not have permission to manage channels. Otherwise returns [`Error::Http`], as well as + /// if invalid data was given. /// - /// let channel_builder = CreateChannel::default(); - /// ``` - fn default() -> Self { - CreateChannel { - name: None, - nsfw: None, - topic: None, - bitrate: None, - position: None, - parent_id: None, - user_limit: None, - rate_limit_per_user: None, - kind: ChannelType::Text, - permission_overwrites: Vec::new(), + /// [Manage Channels]: Permissions::MANAGE_CHANNELS + pub async fn execute(self, cache_http: impl CacheHttp) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(guild) = cache.guild(self.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()).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http) -> Result { + http.create_channel(self.id.into(), &self, None).await } } diff --git a/src/builder/create_embed.rs b/src/builder/create_embed.rs index 183dd37dbd9..7452ee2ec42 100644 --- a/src/builder/create_embed.rs +++ b/src/builder/create_embed.rs @@ -5,16 +5,16 @@ //! These are used in the [`ChannelId::send_message`] and //! [`ExecuteWebhook::embeds`] methods, both as part of builders. //! -//! The only builder that should be exposed is [`CreateEmbed`]. The rest of -//! these have no real reason for being exposed, but are for completeness' sake. -//! //! Documentation for embeds can be found [here]. //! -//! [`ChannelId::send_message`]: crate::model::id::ChannelId::send_message //! [`ExecuteWebhook::embeds`]: crate::builder::ExecuteWebhook::embeds //! [here]: https://discord.com/developers/docs/resources/channel#embed-object -use crate::model::channel::{Embed, EmbedField}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::channel::{Embed, EmbedAuthor, EmbedField, EmbedFooter}; +#[cfg(feature = "http")] +use crate::model::prelude::*; use crate::model::Timestamp; #[cfg(feature = "utils")] use crate::utils::Colour; @@ -40,10 +40,9 @@ impl HoldsUrl { /// Refer to the documentation for [`ChannelId::send_message`] for a very in-depth /// example on how to use this. /// -/// [`ChannelId::send_message`]: crate::model::id::ChannelId::send_message -/// [`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")] @@ -71,21 +70,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 } @@ -95,22 +83,21 @@ 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 { - self._colour(colour.into()); - self + pub fn colour>(self, colour: C) -> Self { + self._colour(colour.into()) } #[cfg(feature = "utils")] - fn _colour(&mut self, colour: Colour) { + fn _colour(mut self, colour: Colour) -> Self { self.colour = Some(colour.0); + self } /// Set the colour of the left-hand side of the embed. @@ -118,14 +105,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 } @@ -134,32 +120,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 an 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, @@ -171,35 +155,24 @@ impl CreateEmbed { 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 } @@ -208,14 +181,14 @@ impl CreateEmbed { /// /// You may pass a direct string: /// - /// - `2017-01-03T23:00:00` - /// - `2004-06-08T16:04:23` - /// - `2004-06-08T16:04:23` + /// - `2017-01-03T23:00:00Z` + /// - `2004-06-08T16:04:23Z` + /// - `2004-06-08T16:04:23Z` /// - /// This timestamp must be in ISO-8601 format. It must also be in UTC format. + /// This timestamp must be in RFC 3339 format. It must also be in UTC format. /// - /// You can also pass an instance of `chrono::DateTime`, - /// which will construct the timestamp string out of it. + /// You can also pass an instance of `chrono::DateTime`, which will construct the + /// timestamp string out of it. /// /// # Examples /// @@ -224,6 +197,7 @@ impl CreateEmbed { /// ```rust,no_run /// # #[cfg(feature = "client")] /// # async fn run() -> Result<(), Box> { + /// use serenity::builder::CreateEmbed; /// use serenity::model::channel::Message; /// use serenity::prelude::*; /// @@ -233,12 +207,8 @@ impl CreateEmbed { /// impl EventHandler for Handler { /// async fn message(&self, context: Context, mut msg: Message) { /// if msg.content == "~embed" { - /// let _ = msg - /// .channel_id - /// .send_message(&context.http, |m| { - /// m.embed(|e| e.title("hello").timestamp("2004-06-08T16:04:23")) - /// }) - /// .await; + /// let embed = CreateEmbed::default().title("hello").timestamp("2004-06-08T16:04:23Z"); + /// let _ = msg.channel_id.send_message().embed(embed).execute(&context.http).await; /// } /// } /// } @@ -258,6 +228,7 @@ impl CreateEmbed { /// ```rust,no_run /// # #[cfg(all(feature = "cache", feature = "client"))] /// # async fn run() -> Result<(), Box> { + /// use serenity::builder::{CreateEmbed, CreateEmbedAuthor}; /// use serenity::model::guild::Member; /// use serenity::model::id::GuildId; /// use serenity::prelude::*; @@ -274,19 +245,14 @@ 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(ref 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(ref joined_at) = member.joined_at { + /// embed = embed.timestamp(joined_at) + /// } + /// let _ = channel.send_message().embed(embed).execute(&context).await; /// } /// } /// } @@ -300,21 +266,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().to_string()); 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 } @@ -323,16 +289,51 @@ impl CreateEmbed { /// /// Note however, you have to be sure you set an attachment (with [`ChannelId::send_files`]) /// with the provided filename. Or else this won't work. - /// - /// [`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(crate) 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 { @@ -359,76 +360,50 @@ 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); + b = b.image(image.url); } - if let Some(thumbnail) = embed.thumbnail { - b.thumbnail(thumbnail.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, @@ -440,31 +415,42 @@ 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. +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. /// -/// This does not require any field be set. +/// This does not have any required fields. /// /// [`Embed`]: crate::model::channel::Embed #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateEmbedFooter { #[serde(skip_serializing_if = "Option::is_none")] icon_url: Option, @@ -474,18 +460,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; @@ -536,12 +531,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..df40039a69f 100644 --- a/src/builder/create_interaction_response.rs +++ b/src/builder/create_interaction_response.rs @@ -1,50 +1,113 @@ +#[cfg(not(feature = "http"))] +use std::marker::PhantomData; + use super::{CreateAllowedMentions, CreateComponents, CreateEmbed}; -use crate::json::prelude::*; +#[cfg(feature = "http")] +use crate::constants; +#[cfg(feature = "http")] +use crate::http::Http; +use crate::internal::prelude::*; use crate::model::application::interaction::{InteractionResponseType, MessageFlags}; -use crate::model::channel::AttachmentType; +use crate::model::prelude::*; #[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateInteractionResponse<'a> { + #[cfg(feature = "http")] + #[serde(skip)] + id: InteractionId, + #[serde(skip)] + #[cfg(feature = "http")] + token: &'a str, + #[cfg(not(feature = "http"))] + token: PhantomData<&'a ()>, + #[serde(rename = "type")] kind: InteractionResponseType, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) data: Option>, + data: Option>, } impl<'a> CreateInteractionResponse<'a> { + pub fn new( + #[cfg(feature = "http")] id: InteractionId, + #[cfg(feature = "http")] token: &'a str, + ) -> Self { + Self { + #[cfg(feature = "http")] + id, + #[cfg(feature = "http")] + token, + #[cfg(not(feature = "http"))] + token: PhantomData::default(), + + kind: InteractionResponseType::ChannelMessageWithSource, + data: None, + } + } + /// 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); - + pub fn interaction_response_data(mut self, data: CreateInteractionResponseData<'a>) -> Self { self.data = Some(data); self } -} -impl<'a> Default for CreateInteractionResponse<'a> { - fn default() -> CreateInteractionResponse<'a> { - Self { - kind: InteractionResponseType::ChannelMessageWithSource, - data: None, + /// Creates a response to the corresponding interaction received. + /// + /// **Note**: Message contents must be under 2000 unicode code points. + /// + /// # Errors + /// + /// Returns an [`Error::Model`] if the message content is too long. May also return an + /// [`Error::Http`] if invalid data is provided, or an [`Error::Json`] if there is an error + /// when deserializing the API response. + #[cfg(feature = "http")] + pub async fn execute(mut self, http: impl AsRef) -> Result<()> { + self.check_lengths()?; + 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(self.id.into(), self.token, &self).await + } else { + http.as_ref() + .create_interaction_response_with_files(self.id.into(), self.token, &self, files) + .await } } + + #[cfg(feature = "http")] + fn check_lengths(&self) -> Result<()> { + if let Some(ref data) = self.data { + if let Some(ref content) = data.content { + let length = content.chars().count(); + let max_length = constants::MESSAGE_CODE_LIMIT; + if length > max_length { + let overflow = length - max_length; + return Err(Error::Model(ModelError::MessageTooLong(overflow))); + } + } + + if data.embeds.len() > constants::EMBED_MAX_COUNT { + return Err(Error::Model(ModelError::EmbedAmount)); + } + for embed in &data.embeds { + embed.check_length()?; + } + } + Ok(()) + } } #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateInteractionResponseData<'a> { embeds: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -63,7 +126,7 @@ pub struct CreateInteractionResponseData<'a> { title: Option, #[serde(skip)] - pub(crate) files: Vec>, + files: Vec>, } impl<'a> CreateInteractionResponseData<'a> { @@ -72,22 +135,22 @@ 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 } @@ -97,9 +160,9 @@ impl<'a> CreateInteractionResponseData<'a> { /// 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,29 +171,24 @@ 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); + pub fn embed(self, embed: CreateEmbed) -> Self { 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 } @@ -139,22 +197,21 @@ impl<'a> CreateInteractionResponseData<'a> { /// /// 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 + pub fn set_embed(self, embed: CreateEmbed) -> Self { + self.set_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 { + pub fn set_embeds(mut self, embeds: Vec) -> Self { self.embeds = embeds.into_iter().collect(); self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(&mut self, f: F) -> &mut Self + pub fn allowed_mentions(mut self, f: F) -> Self where F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, { @@ -166,13 +223,13 @@ impl<'a> CreateInteractionResponseData<'a> { } /// 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 { + pub fn ephemeral(mut self, ephemeral: bool) -> Self { let flags = self.flags.unwrap_or_else(MessageFlags::empty); let flags = if ephemeral { @@ -186,7 +243,7 @@ impl<'a> CreateInteractionResponseData<'a> { } /// Creates components for this message. - pub fn components(&mut self, f: F) -> &mut Self + pub fn components(self, f: F) -> Self where F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, { @@ -197,19 +254,19 @@ impl<'a> CreateInteractionResponseData<'a> { } /// Sets the components of this message. - pub fn set_components(&mut self, components: CreateComponents) -> &mut Self { + pub fn set_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 { + 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 { + pub fn title(mut self, title: impl Into) -> Self { self.title = Some(title.into()); self } @@ -222,26 +279,59 @@ pub struct AutocompleteChoice { pub value: Value, } +#[derive(Clone, Debug, Serialize)] +#[must_use] +pub struct CreateAutocompleteResponse<'a> { + #[cfg(feature = "http")] + #[serde(skip)] + id: InteractionId, + #[serde(skip)] + #[cfg(feature = "http")] + token: &'a str, + #[cfg(not(feature = "http"))] + token: PhantomData<&'a ()>, + + data: CreateAutocompleteResponseData, + #[serde(rename = "type")] + kind: InteractionResponseType, +} + #[derive(Clone, Debug, Default, Serialize)] -pub struct CreateAutocompleteResponse { +struct CreateAutocompleteResponseData { choices: Vec, } -impl CreateAutocompleteResponse { +impl<'a> CreateAutocompleteResponse<'a> { + pub fn new( + #[cfg(feature = "http")] id: InteractionId, + #[cfg(feature = "http")] token: &'a str, + ) -> Self { + Self { + #[cfg(feature = "http")] + id, + #[cfg(feature = "http")] + token, + #[cfg(not(feature = "http"))] + token: PhantomData::default(), + data: CreateAutocompleteResponseData::default(), + kind: InteractionResponseType::Autocomplete, + } + } + /// 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 +341,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 +351,25 @@ 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 } + + /// 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) -> Result<()> { + http.as_ref().create_interaction_response(self.id.into(), self.token, &self).await + } } diff --git a/src/builder/create_interaction_response_followup.rs b/src/builder/create_interaction_response_followup.rs index 30b086b1c7b..07a164ec066 100644 --- a/src/builder/create_interaction_response_followup.rs +++ b/src/builder/create_interaction_response_followup.rs @@ -1,13 +1,28 @@ -#[cfg(not(feature = "model"))] +#[cfg(not(feature = "http"))] use std::marker::PhantomData; use super::{CreateAllowedMentions, CreateComponents, CreateEmbed}; +#[cfg(feature = "http")] +use crate::constants; +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; use crate::model::application::interaction::MessageFlags; -#[cfg(feature = "model")] -use crate::model::channel::AttachmentType; +use crate::model::prelude::*; #[derive(Clone, Debug, Default, Serialize)] +#[must_use] pub struct CreateInteractionResponseFollowup<'a> { + #[cfg(feature = "http")] + #[serde(skip)] + id: Option, + #[serde(skip)] + #[cfg(feature = "http")] + token: &'a str, + #[cfg(not(feature = "http"))] + token: PhantomData<&'a ()>, + embeds: Vec, #[serde(skip_serializing_if = "Option::is_none")] content: Option, @@ -25,32 +40,54 @@ 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> { + pub fn new( + #[cfg(feature = "http")] id: Option, + #[cfg(feature = "http")] token: &'a str, + ) -> Self { + Self { + #[cfg(feature = "http")] + id, + #[cfg(feature = "http")] + token, + #[cfg(not(feature = "http"))] + token: PhantomData::default(), + + embeds: Vec::new(), + content: None, + username: None, + avatar_url: None, + tts: None, + allowed_mentions: None, + flags: None, + components: None, + + files: Vec::new(), + } + } + /// 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 +97,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,33 +120,27 @@ 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); + pub fn embed(self, embed: CreateEmbed) -> Self { 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 } @@ -121,22 +149,21 @@ impl<'a> CreateInteractionResponseFollowup<'a> { /// /// 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 + pub fn set_embed(self, embed: CreateEmbed) -> Self { + self.set_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 { + pub fn set_embeds(mut self, embeds: impl IntoIterator) -> Self { self.embeds = embeds.into_iter().collect(); self } /// Set the allowed mentions for the message. - pub fn allowed_mentions(&mut self, f: F) -> &mut Self + pub fn allowed_mentions(mut self, f: F) -> Self where F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, { @@ -148,13 +175,13 @@ impl<'a> CreateInteractionResponseFollowup<'a> { } /// 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 { + pub fn ephemeral(mut self, ephemeral: bool) -> Self { let flags = self.flags.unwrap_or_else(MessageFlags::empty); let flags = if ephemeral { @@ -168,7 +195,7 @@ impl<'a> CreateInteractionResponseFollowup<'a> { } /// Creates components for this message. - pub fn components(&mut self, f: F) -> &mut Self + pub fn components(self, f: F) -> Self where F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, { @@ -179,8 +206,67 @@ impl<'a> CreateInteractionResponseFollowup<'a> { } /// Sets the components of this message. - pub fn set_components(&mut self, components: CreateComponents) -> &mut Self { + pub fn set_components(mut self, components: CreateComponents) -> Self { self.components = Some(components); self } + + /// Creates/Edits a followup response to the response sent. + /// + /// **Note**: Message contents must be under 2000 unicode code points, and embeds must be under + /// 6000 unicode code points. + /// + /// # Errors + /// + /// Returns an [`Error::Model`] if the message content is too long. May also return an + /// [`Error::Http`] if invalid data is provided, or an [`Error::Json`] if there is an error + /// when deserializing the API response. + #[cfg(feature = "http")] + pub async fn execute(self, http: impl AsRef) -> Result { + self.check_lengths()?; + self._execute(http.as_ref()).await + } + + #[cfg(feature = "http")] + async fn _execute(mut self, http: &Http) -> Result { + let files = std::mem::take(&mut self.files); + + match self.id { + Some(id) => { + if files.is_empty() { + http.edit_followup_message(self.token, id.into(), &self).await + } else { + http.edit_followup_message_and_attachments(self.token, id.into(), &self, files) + .await + } + }, + None => { + if files.is_empty() { + http.create_followup_message(self.token, &self).await + } else { + http.create_followup_message_with_files(self.token, &self, files).await + } + }, + } + } + + #[cfg(feature = "http")] + fn check_lengths(&self) -> Result<()> { + if let Some(ref content) = self.content { + let length = content.chars().count(); + let max_length = constants::MESSAGE_CODE_LIMIT; + if length > max_length { + let overflow = length - max_length; + return Err(Error::Model(ModelError::MessageTooLong(overflow))); + } + } + + if self.embeds.len() > constants::EMBED_MAX_COUNT { + return Err(Error::Model(ModelError::EmbedAmount)); + } + for embed in &self.embeds { + embed.check_length()?; + } + Ok(()) + } } diff --git a/src/builder/create_invite.rs b/src/builder/create_invite.rs index 382d538d6d0..e69757637de 100644 --- a/src/builder/create_invite.rs +++ b/src/builder/create_invite.rs @@ -1,5 +1,11 @@ +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; use crate::model::id::{ApplicationId, UserId}; use crate::model::invite::InviteTargetType; +#[cfg(feature = "http")] +use crate::model::prelude::*; /// A builder to create a [`RichInvite`] for use via [`GuildChannel::create_invite`]. /// @@ -16,7 +22,7 @@ use crate::model::invite::InviteTargetType; /// # use serenity::prelude::*; /// # use serenity::model::prelude::*; /// # use serenity::model::channel::Channel; -/// +/// # /// struct Handler; /// /// #[serenity::async_trait] @@ -33,7 +39,7 @@ use crate::model::invite::InviteTargetType; /// }; /// /// let creation = -/// channel.create_invite(&context, |i| i.max_age(3600).max_uses(10)).await; +/// channel.create_invite().max_age(3600).max_uses(10).execute(&context).await; /// /// let invite = match creation { /// Ok(invite) => invite, @@ -65,8 +71,16 @@ use crate::model::invite::InviteTargetType; /// /// [`GuildChannel::create_invite`]: crate::model::channel::GuildChannel::create_invite /// [`RichInvite`]: crate::model::invite::RichInvite -#[derive(Clone, Debug, Default, Serialize)] +#[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateInvite { + #[cfg(feature = "http")] + #[serde(skip)] + channel_id: ChannelId, + #[cfg(all(feature = "http", feature = "cache"))] + #[serde(skip)] + guild_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] max_age: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -84,6 +98,25 @@ pub struct CreateInvite { } impl CreateInvite { + pub fn new( + #[cfg(feature = "http")] channel_id: ChannelId, + #[cfg(all(feature = "http", feature = "cache"))] guild_id: Option, + ) -> Self { + Self { + #[cfg(feature = "http")] + channel_id, + #[cfg(all(feature = "http", feature = "cache"))] + guild_id, + max_age: None, + max_uses: None, + temporary: None, + unique: None, + target_type: None, + target_user_id: None, + target_application_id: None, + } + } + /// The duration that the invite will be valid for. /// /// Set to `0` for an invite which does not expire after an amount of time. @@ -106,11 +139,11 @@ impl CreateInvite { /// # 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 invite = channel.create_invite().max_age(3600).execute(&context).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 } @@ -137,11 +170,11 @@ impl CreateInvite { /// # 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 invite = channel.create_invite().max_uses(5).execute(&context).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 } @@ -166,13 +199,13 @@ impl CreateInvite { /// # 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 invite = channel.create_invite().temporary(true).execute(&context).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 } @@ -197,17 +230,17 @@ impl CreateInvite { /// # 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 invite = channel.create_invite().unique(true).execute(&context).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 +248,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 } @@ -230,12 +263,48 @@ impl CreateInvite { /// These are some of the known applications which have the flag: /// /// betrayal: `773336526917861400` + /// /// youtube: `755600276941176913` + /// /// 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 } + + /// Creates an invite leading to the channel id given to the builder. + /// + /// **Note**: Requires the [Create Instant Invite] permission. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns [`ModelError::InvalidPermissions`] if the current user + /// does not have permission to create invites. Otherwise, returns [`Error::Http`]. + /// + /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE + #[cfg(feature = "http")] + pub async fn execute(self, cache_http: impl CacheHttp) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + crate::utils::user_has_perms_cache( + cache, + self.channel_id, + self.guild_id, + Permissions::CREATE_INSTANT_INVITE, + )?; + } + } + + self._execute(cache_http.http()).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http) -> Result { + http.create_invite(self.channel_id.into(), &self, None).await + } } diff --git a/src/builder/create_message.rs b/src/builder/create_message.rs index 28872913120..cae6e911b9f 100644 --- a/src/builder/create_message.rs +++ b/src/builder/create_message.rs @@ -1,53 +1,55 @@ -#[cfg(not(feature = "model"))] -use std::marker::PhantomData; - -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`]. +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::*; + +/// 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; /// use serenity::model::id::ChannelId; /// # use serenity::http::Http; /// # use std::sync::Arc; /// # +/// # pub 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 _ = channel_id.send_message().content("test").tts(true).embed(embed).execute(&http).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)] +#[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateMessage<'a> { + #[cfg(feature = "http")] + #[serde(skip)] + channel_id: ChannelId, + #[cfg(all(feature = "http", feature = "cache"))] + #[serde(skip)] + guild_id: Option, + tts: bool, embeds: Vec, sticker_ids: Vec, @@ -62,86 +64,78 @@ 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 separately. #[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> { + pub fn new( + #[cfg(feature = "http")] channel_id: ChannelId, + #[cfg(all(feature = "http", feature = "cache"))] guild_id: Option, + ) -> Self { + Self { + #[cfg(feature = "http")] + channel_id, + #[cfg(all(feature = "http", feature = "cache"))] + guild_id, + + tts: false, + embeds: Vec::new(), + sticker_ids: Vec::new(), + content: None, + allowed_mentions: None, + message_reference: None, + components: None, + flags: None, + + files: Vec::new(), + reactions: Vec::new(), + } + } + /// 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 +145,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,45 +153,54 @@ 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 + pub fn allowed_mentions(mut self, f: F) -> Self where F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, { @@ -210,13 +213,13 @@ impl<'a> CreateMessage<'a> { /// 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 + pub fn components(self, f: F) -> Self where F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, { @@ -227,32 +230,24 @@ impl<'a> CreateMessage<'a> { } /// Sets the components of this message. - pub fn set_components(&mut self, components: CreateComponents) -> &mut Self { + pub fn set_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()]) - } - /// 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_id(&mut self, sticker_id: impl Into) -> &mut Self { + /// **Note**: This will keep all existing stickers. Use [`Self::sticker_ids()`] to replace + /// existing stickers. + pub fn add_sticker_id(mut self, sticker_id: impl Into) -> Self { self.sticker_ids.push(sticker_id.into()); self } @@ -261,31 +256,117 @@ impl<'a> CreateMessage<'a> { /// /// **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. + /// **Note**: This will keep all existing stickers. Use [`Self::sticker_ids()`] to replace + /// existing stickers. pub fn add_sticker_ids, It: IntoIterator>( - &mut self, + mut self, sticker_ids: It, - ) -> &mut Self { + ) -> Self { for sticker_id in sticker_ids { - self.add_sticker_id(sticker_id); + self = self.add_sticker_id(sticker_id); } - 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(self, sticker_id: impl Into) -> Self { + self.sticker_ids(vec![sticker_id.into()]) + } + /// 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 replace all existing stickers. Use - /// [`Self::add_sticker_id()`] or [`Self::add_sticker_ids()`] to keep - /// existing stickers. - pub fn set_sticker_ids, It: IntoIterator>( - &mut self, + /// **Note**: This will replace all existing stickers. Use [`Self::add_sticker_ids()`] or + /// [`Self::add_sticker_ids()`] to keep existing stickers. + pub fn sticker_ids, It: IntoIterator>( + mut self, sticker_ids: It, - ) -> &mut Self { + ) -> Self { self.sticker_ids = sticker_ids.into_iter().map(Into::into).collect(); self } + + /// Sends a message to the channel. + /// + /// **Note**: Requires the [Send Messages] permission. Adding files additionally requires the + /// [Attach Files] permission. + /// + /// **Note**: Message contents must be under 2000 unicode code points, and embeds must be under + /// 6000 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. + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// does not have the required permissions. + /// + /// Otherwise, returns [`Error::Http`] if the current user lacks permission, as well as if any + /// files sent are too large channel, or otherwise if any invalid data is sent. + /// + /// [Send Messages]: Permissions::SEND_MESSAGES + /// [Attach Files]: Permissions::ATTACH_FILES + #[cfg(feature = "http")] + pub async fn execute(self, cache_http: impl CacheHttp) -> 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, self.channel_id, self.guild_id, req)?; + } + } + + self._execute(cache_http.http()).await + } + + #[cfg(feature = "http")] + async fn _execute(mut self, http: &Http) -> Result { + self.check_lengths()?; + let files = std::mem::take(&mut self.files); + + let message = if files.is_empty() { + http.send_message(self.channel_id.into(), &self).await? + } else { + http.send_files(self.channel_id.into(), files, &self).await? + }; + + for reaction in self.reactions { + self.channel_id.create_reaction(&http, message.id, reaction).await?; + } + + Ok(message) + } + + #[cfg(feature = "http")] + pub(crate) fn check_lengths(&self) -> Result<()> { + if let Some(ref content) = self.content { + let length = content.chars().count(); + let max_length = constants::MESSAGE_CODE_LIMIT; + if length > max_length { + let overflow = length - max_length; + return Err(Error::Model(ModelError::MessageTooLong(overflow))); + } + } + + 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(()) + } } diff --git a/src/builder/create_scheduled_event.rs b/src/builder/create_scheduled_event.rs index 74ae08e7842..9107e68a559 100644 --- a/src/builder/create_scheduled_event.rs +++ b/src/builder/create_scheduled_event.rs @@ -1,17 +1,18 @@ -#[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 { + #[cfg(feature = "http")] + #[serde(skip)] + id: GuildId, + #[serde(skip_serializing_if = "Option::is_none")] channel_id: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -33,32 +34,55 @@ pub struct CreateScheduledEvent { } impl CreateScheduledEvent { + /// Creates a builder with default values, setting the `privacy_level` to `GUILD_ONLY`. As this + /// is the only possible value of this field, it's only used at event creation, and we don't + /// even parse it into the `ScheduledEvent` struct. + pub fn new(#[cfg(feature = "http")] id: GuildId) -> Self { + Self { + #[cfg(feature = "http")] + id, + + channel_id: None, + name: None, + description: None, + scheduled_start_time: None, + scheduled_end_time: None, + entity_type: None, + entity_metadata: None, + image: None, + + // Set `privacy_level` to `GUILD_ONLY`. This is the only current possible value of this + // field, so it's only used here and not even parsed in the `ScheduledEvent` struct. + privacy_level: 2, + } + } + /// Sets the channel id of the scheduled event. Required if the [`kind`] of the event is /// [`StageInstance`] or [`Voice`]. /// /// [`kind`]: CreateScheduledEvent::kind /// [`StageInstance`]: ScheduledEventType::StageInstance /// [`Voice`]: ScheduledEventType::Voice - pub fn channel_id>(&mut self, channel_id: C) -> &mut Self { + 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 } @@ -69,13 +93,13 @@ impl CreateScheduledEvent { /// [`kind`]: CreateScheduledEvent::kind /// [`External`]: ScheduledEventType::External #[inline] - pub fn end_time>(&mut self, timestamp: T) -> &mut Self { + 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 } @@ -85,11 +109,10 @@ impl CreateScheduledEvent { /// /// [`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,36 +120,58 @@ 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 a URL is given and the HTTP request fails, or if a path is given 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) } -} -impl Default for CreateScheduledEvent { - /// Creates a builder with default values, setting the `privacy_level` to `GUILD_ONLY`. As this - /// is the only possible value of this field, it's only used at event creation, and we don't - /// even parse it into the `ScheduledEvent` struct. - fn default() -> Self { - Self { - privacy_level: 2, + /// 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 + } - name: None, - image: None, - channel_id: None, - description: None, - entity_type: None, - entity_metadata: None, - scheduled_end_time: None, - scheduled_start_time: None, + /// Creates a new scheduled event in the guild with the data set, if any. + /// + /// **Note**: Requres the [Manage Events] permission. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] if the current user + /// does not have permission to manage scheduled events. Otherwise, returns [`Error::Http`]. + /// + /// [Manage Events]: Permissions::MANAGE_EVENTS + #[cfg(feature = "http")] + #[inline] + pub async fn execute(self, cache_http: impl CacheHttp) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(guild) = cache.guild(self.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()).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http) -> Result { + http.create_scheduled_event(self.id.into(), &self, None).await } } diff --git a/src/builder/create_stage_instance.rs b/src/builder/create_stage_instance.rs index 9fd362b4c23..60a84d23a6e 100644 --- a/src/builder/create_stage_instance.rs +++ b/src/builder/create_stage_instance.rs @@ -1,26 +1,60 @@ +#[cfg(feature = "http")] +use crate::http::CacheHttp; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(all(feature = "http", feature = "cache"))] +use crate::model::channel::ChannelType; +#[cfg(feature = "http")] +use crate::model::channel::StageInstance; use crate::model::id::ChannelId; +#[cfg(all(feature = "http", feature = "cache"))] +use crate::model::prelude::*; /// Creates a [`StageInstance`]. /// /// [`StageInstance`]: crate::model::channel::StageInstance -#[derive(Clone, Debug, Default, Serialize)] +#[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateStageInstance { - #[serde(skip_serializing_if = "Option::is_none")] - channel_id: Option, + channel_id: ChannelId, #[serde(skip_serializing_if = "Option::is_none")] topic: Option, } impl CreateStageInstance { - // Sets the stage channel id of the stage channel instance. - pub fn channel_id(&mut self, id: impl Into) -> &mut Self { - self.channel_id = Some(id.into()); - self + pub fn new(id: impl Into) -> Self { + Self { + channel_id: id.into(), + topic: None, + } } /// 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 } + + /// Creates the stage instance. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns [`ModelError::InvalidChannelType`] if the channel is not + /// a stage channel. Otherwise, returns [`Error::Http`], as well as if there is a already a + /// stage instance currently. + #[cfg(feature = "http")] + pub async fn execute(self, cache_http: impl CacheHttp) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(channel) = cache.guild_channel(self.channel_id) { + if channel.kind != ChannelType::Stage { + return Err(Error::Model(ModelError::InvalidChannelType)); + } + } + } + } + + cache_http.http().create_stage_instance(&self).await + } } diff --git a/src/builder/create_sticker.rs b/src/builder/create_sticker.rs index 3fa099efd9e..e864b5c30f0 100644 --- a/src/builder/create_sticker.rs +++ b/src/builder/create_sticker.rs @@ -1,10 +1,12 @@ -use std::borrow::Cow; - +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; use crate::model::channel::AttachmentType; +#[cfg(feature = "http")] +use crate::model::prelude::*; -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: /// @@ -16,20 +18,40 @@ type Field = (Cow<'static, str>, Cow<'static, str>); /// [`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)] +#[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateSticker<'a> { + #[cfg(feature = "http")] + #[serde(skip)] + id: GuildId, + + #[serde(skip_serializing_if = "Option::is_none")] name: Option, + #[serde(skip_serializing_if = "Option::is_none")] tags: Option, + #[serde(skip_serializing_if = "Option::is_none")] description: Option, - pub(crate) file: Option>, + #[serde(skip)] + file: Option>, } impl<'a> CreateSticker<'a> { + pub fn new(#[cfg(feature = "http")] id: GuildId) -> Self { + Self { + #[cfg(feature = "http")] + id, + name: None, + tags: None, + description: None, + file: None, + } + } + /// 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 +59,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 +67,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 +75,55 @@ 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); + /// 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`]. + /// + /// [Manage Emojis and Stickers]: crate::model::permissions::Permissions::MANAGE_EMOJIS_AND_STICKERS + #[cfg(feature = "http")] + #[inline] + pub async fn execute(self, cache_http: impl CacheHttp) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(guild) = cache.guild(self.id) { + let req = Permissions::MANAGE_EMOJIS_AND_STICKERS; - if let Some(name) = self.name { - buf.push(("name".into(), name.into())); + if !guild.has_perms(&cache_http, req).await { + return Err(Error::Model(ModelError::InvalidPermissions(req))); + } + } + } } - if let Some(description) = self.description { - buf.push(("description".into(), description.into())); - } + self._execute(cache_http.http()).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http) -> 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 { - buf.push(("tags".into(), tags.into())); + map.push(("tags".to_string(), tags)); + } + if let Some(description) = self.description { + map.push(("description".to_string(), description)); } - Some((buf, file)) + http.create_sticker(self.id.into(), map, file, None).await } } diff --git a/src/builder/create_thread.rs b/src/builder/create_thread.rs index 8c53a085f5f..54abd5e985a 100644 --- a/src/builder/create_thread.rs +++ b/src/builder/create_thread.rs @@ -1,7 +1,21 @@ +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; use crate::model::channel::ChannelType; +#[cfg(feature = "http")] +use crate::model::prelude::*; -#[derive(Clone, Debug, Default, Serialize)] +#[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateThread { + #[cfg(feature = "http")] + #[serde(skip)] + channel_id: ChannelId, + #[cfg(feature = "http")] + #[serde(skip)] + message_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] name: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -15,21 +29,35 @@ pub struct CreateThread { } impl CreateThread { + pub fn new( + #[cfg(feature = "http")] channel_id: ChannelId, + #[cfg(feature = "http")] message_id: Option, + ) -> Self { + Self { + #[cfg(feature = "http")] + channel_id, + #[cfg(feature = "http")] + message_id, + name: None, + auto_archive_duration: None, + rate_limit_per_user: None, + kind: None, + } + } + /// 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 } @@ -43,9 +71,8 @@ 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,9 +82,26 @@ 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 } + + /// Executes the request to create 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. + #[cfg(feature = "http")] + pub async fn execute(self, http: impl AsRef) -> Result { + match self.message_id { + Some(msg_id) => { + http.as_ref() + .create_public_thread(self.channel_id.into(), msg_id.into(), &self) + .await + }, + None => http.as_ref().create_private_thread(self.channel_id.into(), &self).await, + } + } } diff --git a/src/builder/create_webhook.rs b/src/builder/create_webhook.rs index 54b91ff0657..bec8011d410 100644 --- a/src/builder/create_webhook.rs +++ b/src/builder/create_webhook.rs @@ -1,23 +1,103 @@ -#[derive(Debug, Default, Clone, Serialize)] +#[cfg(feature = "http")] +use crate::http::{CacheHttp, Http}; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::prelude::*; +#[cfg(feature = "http")] +use crate::utils::encode_image; + +#[derive(Clone, Debug, Serialize)] +#[must_use] pub struct CreateWebhook { + #[cfg(feature = "http")] + #[serde(skip)] + id: ChannelId, #[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. + pub fn new(#[cfg(feature = "http")] id: ChannelId) -> Self { + Self { + #[cfg(feature = "http")] + id, + name: None, + avatar: None, + } + } + + /// 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 a URL is given and the HTTP request fails, or if a path is given 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(encode_image(&avatar_data)); + Ok(self) + } + + /// Set the webhook's default avatar. Requires the input be a base64-encoded image that is in + /// either JPG, GIF, or PNG format. + #[cfg(not(feature = "http"))] + pub fn avatar(mut self, avatar: String) -> Self { self.avatar = Some(avatar); self } + + /// Creates the webhook. + /// + /// # 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. + #[cfg(feature = "http")] + pub async fn execute(self, cache_http: impl CacheHttp) -> Result { + #[cfg(feature = "cache")] + { + if let Some(cache) = cache_http.cache() { + if let Some(channel) = cache.guild_channel(self.id) { + if !channel.is_text_based() { + return Err(Error::Model(ModelError::InvalidChannelType)); + } + } + } + } + + self._execute(cache_http.http()).await + } + + #[cfg(feature = "http")] + async fn _execute(self, http: &Http) -> Result { + if let Some(ref 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.as_ref().create_webhook(self.id.into(), &self, None).await + } } diff --git a/src/builder/edit_interaction_response.rs b/src/builder/edit_interaction_response.rs index d16a1482348..47e815ba7a8 100644 --- a/src/builder/edit_interaction_response.rs +++ b/src/builder/edit_interaction_response.rs @@ -25,12 +25,7 @@ impl EditInteractionResponse { } /// 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); + pub fn embed(&mut self, embed: CreateEmbed) -> &mut Self { self.add_embed(embed) } diff --git a/src/builder/edit_message.rs b/src/builder/edit_message.rs index 2d184f088be..8120e4f5e9d 100644 --- a/src/builder/edit_message.rs +++ b/src/builder/edit_message.rs @@ -66,14 +66,9 @@ impl<'a> EditMessage<'a> { /// 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); + pub fn add_embed(&mut self, embed: CreateEmbed) -> &mut Self { self._add_embed(embed) } @@ -88,26 +83,9 @@ impl<'a> EditMessage<'a> { /// 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 { + /// **Note**: This will replace all existing embeds. Use [`Self::add_embed()`] to keep existing + /// embeds. + pub fn embed(&mut self, embed: CreateEmbed) -> &mut Self { self.set_embeds(vec![embed]) } diff --git a/src/builder/edit_profile.rs b/src/builder/edit_profile.rs index d667b93c44d..618dc8d6e07 100644 --- a/src/builder/edit_profile.rs +++ b/src/builder/edit_profile.rs @@ -1,16 +1,45 @@ +#[cfg(not(feature = "http"))] +use std::marker::PhantomData; + +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +#[cfg(feature = "http")] +use crate::model::user::CurrentUser; + /// A builder to edit the current user's settings, to be used in conjunction /// with [`CurrentUser::edit`]. /// /// [`CurrentUser::edit`]: crate::model::user::CurrentUser::edit -#[derive(Clone, Debug, Default, Serialize)] -pub struct EditProfile { +#[derive(Debug, Serialize)] +#[must_use] +pub struct EditProfile<'a> { + #[serde(skip)] + #[cfg(feature = "http")] + user: &'a mut CurrentUser, + #[cfg(not(feature = "http"))] + user: PhantomData<&'a ()>, + #[serde(skip_serializing_if = "Option::is_none")] avatar: Option>, #[serde(skip_serializing_if = "Option::is_none")] username: Option, } -impl EditProfile { +impl<'a> EditProfile<'a> { + pub fn new(#[cfg(feature = "http")] user: &'a mut CurrentUser) -> Self { + Self { + #[cfg(feature = "http")] + user, + #[cfg(not(feature = "http"))] + user: PhantomData::default(), + + avatar: None, + username: None, + } + } + /// Sets the avatar of the current user. [`None`] can be passed to remove an /// avatar. /// @@ -46,7 +75,7 @@ impl EditProfile { /// ``` /// /// [`utils::read_image`]: crate::utils::read_image - pub fn avatar(&mut self, avatar: Option) -> &mut Self { + pub fn avatar(mut self, avatar: Option) -> Self { self.avatar = Some(avatar); self } @@ -57,8 +86,20 @@ impl EditProfile { /// 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 { + pub fn username(mut self, username: impl Into) -> Self { self.username = Some(username.into()); self } + + /// Edits the current user's profile. + /// + /// # 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. + #[cfg(feature = "http")] + pub async fn execute(self, http: impl AsRef) -> Result<()> { + *self.user = http.as_ref().edit_profile(&self).await?; + Ok(()) + } } diff --git a/src/builder/edit_role.rs b/src/builder/edit_role.rs index f305aad4040..a6e8e3051fe 100644 --- a/src/builder/edit_role.rs +++ b/src/builder/edit_role.rs @@ -144,8 +144,8 @@ impl EditRole { /// /// # 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. + /// May error if a URL is given and the HTTP request fails, or if a path is given to a file + /// that does not exist. #[cfg(feature = "model")] pub async fn icon<'a>( &mut self, @@ -159,4 +159,13 @@ impl EditRole { Ok(self) } + + /// The image to set as the role icon. Requires the input be a base64-encoded image that is in + /// either JPG, GIF, or PNG format. + #[cfg(not(feature = "model"))] + pub fn icon(&mut self, icon: String) -> &mut 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..eb279e1bb1b 100644 --- a/src/builder/edit_scheduled_event.rs +++ b/src/builder/edit_scheduled_event.rs @@ -141,8 +141,8 @@ 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. + /// May error if a URL is given and the HTTP request fails, or if a path is given to a file + /// that does not exist. #[cfg(feature = "model")] pub async fn image<'a>( &mut self, @@ -153,4 +153,12 @@ impl EditScheduledEvent { 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 = "model"))] + pub fn image(&mut self, image: String) -> &mut Self { + self.image = Some(image); + self + } } diff --git a/src/builder/edit_webhook.rs b/src/builder/edit_webhook.rs index bb292d897d3..dc4ba197610 100644 --- a/src/builder/edit_webhook.rs +++ b/src/builder/edit_webhook.rs @@ -1,7 +1,21 @@ -use crate::model::id::ChannelId; +#[cfg(not(feature = "http"))] +use std::marker::PhantomData; + +#[cfg(feature = "http")] +use crate::http::Http; +#[cfg(feature = "http")] +use crate::internal::prelude::*; +use crate::model::prelude::*; + +#[derive(Debug, Serialize)] +#[must_use] +pub struct EditWebhook<'a> { + #[serde(skip)] + #[cfg(feature = "http")] + webhook: &'a mut Webhook, + #[cfg(not(feature = "http"))] + webhook: PhantomData<&'a ()>, -#[derive(Debug, Default, Clone, Serialize)] -pub struct EditWebhook { #[serde(skip_serializing_if = "Option::is_none")] name: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -10,24 +24,80 @@ pub struct EditWebhook { avatar: Option>, } -impl EditWebhook { - /// Set default name of the Webhook. +impl<'a> EditWebhook<'a> { + pub fn new(#[cfg(feature = "http")] webhook: &'a mut Webhook) -> Self { + Self { + #[cfg(feature = "http")] + webhook, + #[cfg(not(feature = "http"))] + webhook: PhantomData::default(), + + name: None, + channel_id: None, + avatar: None, + } + } + + /// 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 a URL is given and the HTTP request fails, or if a path is given to a file + /// that does not exist. + #[cfg(featuer = "http")] + pub async fn avatar( + mut self, + http: impl AsRef, + avatar: impl Into>, + ) -> Result { + let avatar_data = avatar.into().data(&http.as_ref().client).await?; + self.avatar = Some(Some(encode_image(&avatar_data))); + Ok(self) + } + + /// Set the webhook's default avatar. Requires the input be a base64-encoded image that is in + /// either JPG, GIF, or PNG format. + #[cfg(not(feature = "http"))] + pub fn avatar(mut self, avatar: String) -> Self { + self.avatar = Some(Some(avatar)); + self + } + + /// Deletes the webhook's avatar, resetting it to the default logo. + pub fn delete_avatar(mut self) -> Self { + self.avatar = Some(None); self } + + /// Sends off the request and edits the webhook. Does not require authentication, as a token is + /// required. + /// + /// # Errors + /// + /// Returns an [`Error::Model`] if the token field of the current webhook 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. + #[cfg(feature = "http")] + pub async fn execute(self, http: impl AsRef) -> Result<()> { + let token = self.webhook.token.as_ref().ok_or(ModelError::NoTokenSet)?; + *self.webhook = + http.as_ref().edit_webhook_with_token(self.webhook.id.into(), token, &self).await?; + Ok(()) + } } diff --git a/src/builder/edit_webhook_message.rs b/src/builder/edit_webhook_message.rs index c3b2562d06a..10d04bc35e7 100644 --- a/src/builder/edit_webhook_message.rs +++ b/src/builder/edit_webhook_message.rs @@ -1,10 +1,29 @@ +#[cfg(not(feature = "http"))] +use std::marker::PhantomData; + 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)] -pub struct EditWebhookMessage { +#[derive(Clone, Debug, Serialize)] +#[must_use] +pub struct EditWebhookMessage<'a> { + #[serde(skip)] + #[cfg(feature = "http")] + webhook: &'a Webhook, + #[cfg(not(feature = "http"))] + webhook: PhantomData<&'a ()>, + #[cfg(feature = "http")] + #[serde(skip)] + message_id: MessageId, + #[serde(skip_serializing_if = "Option::is_none")] content: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -15,12 +34,31 @@ pub struct EditWebhookMessage { components: Option, } -impl EditWebhookMessage { +impl<'a> EditWebhookMessage<'a> { + pub fn new( + #[cfg(feature = "http")] webhook: &'a Webhook, + #[cfg(feature = "http")] message_id: MessageId, + ) -> Self { + Self { + #[cfg(feature = "http")] + webhook, + #[cfg(not(feature = "http"))] + webhook: PhantomData::default(), + #[cfg(feature = "http")] + message_id, + + content: None, + embeds: None, + allowed_mentions: None, + components: None, + } + } + /// 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,13 +72,13 @@ 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 + pub fn allowed_mentions(mut self, f: F) -> Self where F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, { @@ -57,7 +95,7 @@ impl EditWebhookMessage { /// /// [`WebhookType::Application`]: crate::model::webhook::WebhookType /// [`WebhookType::Incoming`]: crate::model::webhook::WebhookType - pub fn components(&mut self, f: F) -> &mut Self + pub fn components(mut self, f: F) -> Self where F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, { @@ -67,4 +105,22 @@ impl EditWebhookMessage { self.components = Some(components); self } + + /// Edits the webhook message. + /// + /// # Errors + /// + /// Returns an [`Error::Model`] if the token field of the current webhook is `None`. + /// + /// 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. + #[cfg(feature = "http")] + pub async fn execute(self, http: impl AsRef) -> Result { + let token = self.webhook.token.as_ref().ok_or(ModelError::NoTokenSet)?; + http.as_ref() + .edit_webhook_message(self.webhook.id.into(), token, self.message_id.into(), &self) + .await + } } diff --git a/src/builder/execute_webhook.rs b/src/builder/execute_webhook.rs index 05c6301fbf2..8bfb56380ba 100644 --- a/src/builder/execute_webhook.rs +++ b/src/builder/execute_webhook.rs @@ -1,27 +1,24 @@ -#[cfg(not(feature = "model"))] +#[cfg(not(feature = "http"))] 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. +/// A builder to create the content for a [`Webhook`]'s execution. /// -/// This is a structured way of cleanly creating the inner execution payload, -/// to reduce potential argument counts. -/// -/// Refer to the documentation for [`execute_webhook`] on restrictions with -/// execution payloads and its fields. +/// Refer to [`Http::execute_webhook`] for restrictions on the execution payload and its fields. /// /// # 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; /// use serenity::http::Http; -/// use serenity::model::channel::Embed; /// use serenity::model::webhook::Webhook; /// use serenity::utils::Colour; /// @@ -30,34 +27,36 @@ 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]) -/// }) +/// let msg = webhook +/// .execute() +/// .content("Here's some information on Rust:") +/// .embeds(vec![website, resources]) +/// .execute(&http) /// .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)] +#[derive(Clone, Debug, Serialize)] +#[must_use] pub struct ExecuteWebhook<'a> { + #[serde(skip)] + #[cfg(feature = "http")] + webhook: &'a Webhook, + #[cfg(not(feature = "http"))] + webhook: PhantomData<&'a ()>, + tts: bool, embeds: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -74,13 +73,36 @@ pub struct ExecuteWebhook<'a> { flags: Option, #[serde(skip)] - #[cfg(feature = "model")] - pub(crate) files: Vec>, - #[cfg(not(feature = "model"))] - files: PhantomData<&'a ()>, + wait: bool, + #[serde(skip)] + thread_id: Option, + #[serde(skip)] + files: Vec>, } impl<'a> ExecuteWebhook<'a> { + pub fn new(#[cfg(feature = "http")] webhook: &'a Webhook) -> Self { + Self { + #[cfg(feature = "http")] + webhook, + #[cfg(not(feature = "http"))] + webhook: PhantomData::default(), + + tts: false, + embeds: Vec::new(), + avatar_url: None, + content: None, + allowed_mentions: None, + components: None, + username: None, + flags: None, + + wait: false, + thread_id: None, + files: Vec::new(), + } + } + /// Override the default avatar of the webhook with an image URL. /// /// # Examples @@ -97,19 +119,18 @@ impl<'a> ExecuteWebhook<'a> { /// # /// 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?; + /// webhook.execute().avatar_url(avatar_url).content("Here's a webhook").execute(&http).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 } /// Set the content of the message. /// - /// Note that when setting at least one embed via [`Self::embeds`], this may be - /// omitted. + /// Note that when setting at least one embed via [`Self::embeds`], this may be omitted. /// /// # Examples /// @@ -123,7 +144,7 @@ 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 execution = webhook.execute().content("foo").execute(&http).await; /// /// if let Err(why) = execution { /// println!("Err sending webhook: {:?}", why); @@ -131,43 +152,66 @@ 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 the context of a thread belonging to the Webhook's channel. If the thread is + /// archived, it will automatically be unarchived. If the provided thread Id doesn't belong to + /// the current webhook's thread, then the API will return an error at request execution time. + /// + /// # Examples + /// + /// Execute a webhook with message content of `test`, in a thread with Id `12345678`: + /// + /// ```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(12345678).content("test").execute(&http).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: T) -> 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 + pub fn allowed_mentions(mut self, f: F) -> Self where F: FnOnce(&mut CreateAllowedMentions) -> &mut CreateAllowedMentions, { @@ -181,10 +225,7 @@ impl<'a> ExecuteWebhook<'a> { /// Creates 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 + pub fn components(self, f: F) -> Self where F: FnOnce(&mut CreateComponents) -> &mut CreateComponents, { @@ -195,10 +236,8 @@ impl<'a> ExecuteWebhook<'a> { } /// 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 { + /// [`Self::components`] for details. + pub fn set_components(mut self, components: CreateComponents) -> Self { self.components = Some(components); self } @@ -207,11 +246,10 @@ impl<'a> ExecuteWebhook<'a> { /// /// # Examples /// - /// 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 embeds(mut self, embeds: Vec) -> Self { self.embeds = embeds; self } @@ -230,7 +268,7 @@ 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 execution = webhook.execute().content("hello").tts(true).execute(&http).await; /// /// if let Err(why) = execution { /// println!("Err sending webhook: {:?}", why); @@ -238,11 +276,20 @@ 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 } + /// Set this to `true` to wait for server confirmation of the message having been sent before + /// receiving a response. See the [Discord docs] for more details. + /// + /// [Discord docs]: https://discord.com/developers/docs/resources/webhook#execute-webhook-query-string-params + pub fn wait(mut self, wait: bool) -> Self { + self.wait = wait; + self + } + /// Override the default username of the webhook. /// /// # Examples @@ -257,7 +304,7 @@ 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 execution = webhook.execute().content("hello").username("hakase").execute(&http).await; /// /// if let Err(why) = execution { /// println!("Err sending webhook: {:?}", why); @@ -265,7 +312,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 } @@ -286,10 +333,10 @@ impl<'a> ExecuteWebhook<'a> { /// # 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) - /// }) + /// .execute() + /// .content("https://docs.rs/serenity/latest/serenity/") + /// .flags(MessageFlags::SUPPRESS_EMBEDS) + /// .execute(&http) /// .await; /// /// if let Err(why) = execution { @@ -298,8 +345,41 @@ 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 } + + /// Executes the webhook with the given content. + /// + /// If [`Self::wait`] is set to false, then this function will return `Ok(None)`. Otherwise + /// Discord will wait for confirmation that the message was sent, and this function will return + /// `Ok(Some(Message))`. + /// + /// # Errors + /// + /// Returns an [`Error::Model`] if the token field of the current webhook is `None`. + /// + /// May also return an [`Error::Http`] if the content is malformed, or if the token is invalid. + /// Additionally, this error variant is returned if the webhook attempts to execute in the + /// context of a thread whose Id is invalid, or does not belonging to the webhook's + /// associated [`Channel`]. + /// + /// Finally, may return an [`Error::Json`] if there is an error in deserialising Discord's + /// response. + #[cfg(feature = "http")] + pub async fn execute(mut self, http: impl AsRef) -> Result> { + let token = self.webhook.token.as_ref().ok_or(ModelError::NoTokenSet)?; + let id = self.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(id, thread_id, token, self.wait, &self).await + } else { + http.as_ref() + .execute_webhook_with_files(id, thread_id, token, self.wait, files, &self) + .await + } + } } diff --git a/src/builder/get_messages.rs b/src/builder/get_messages.rs index 6aa7da2b0e7..a0c4488a360 100644 --- a/src/builder/get_messages.rs +++ b/src/builder/get_messages.rs @@ -1,27 +1,33 @@ -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 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 number of messages to 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; @@ -34,24 +40,38 @@ use crate::model::id::MessageId; /// let channel_id = ChannelId::new(81384788765712384); /// /// let _messages = channel_id -/// .messages(&http, |retriever| retriever.after(MessageId::new(158339864557912064)).limit(25)) +/// .messages() +/// .after(MessageId::new(158339864557912064)) +/// .limit(25) +/// .execute(&http) /// .await?; /// # Ok(()) /// # } /// ``` /// /// [`GuildChannel::messages`]: crate::model::channel::GuildChannel::messages -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug)] +#[must_use] pub struct GetMessages { - pub search_filter: Option, - pub limit: Option, + #[cfg(feature = "http")] + id: ChannelId, + search_filter: Option, + limit: Option, } impl GetMessages { - /// Indicates to retrieve the messages after a specific message, given by - /// its Id. + pub fn new(#[cfg(feature = "http")] id: ChannelId) -> Self { + Self { + #[cfg(feature = "http")] + id, + search_filter: None, + limit: None, + } + } + + /// Indicates to retrieve the messages after a specific message, given its Id. #[inline] - pub fn after>(&mut self, message_id: M) -> &mut Self { + pub fn after>(mut self, message_id: M) -> Self { self._after(message_id.into()); self } @@ -60,10 +80,10 @@ impl GetMessages { self.search_filter = Some(SearchFilter::After(message_id)); } - /// Indicates to retrieve the messages _around_ a specific message in either - /// direction (before+after) the given message. + /// Indicates to retrieve the messages _around_ a specific message, in other words in either + /// direction from the message in time. #[inline] - pub fn around>(&mut self, message_id: M) -> &mut Self { + pub fn around>(mut self, message_id: M) -> Self { self._around(message_id.into()); self } @@ -72,10 +92,9 @@ impl GetMessages { self.search_filter = Some(SearchFilter::Around(message_id)); } - /// Indicates to retrieve the messages before a specific message, given by - /// its Id. + /// Indicates to retrieve the messages before a specific message, given its Id. #[inline] - pub fn before>(&mut self, message_id: M) -> &mut Self { + pub fn before>(mut self, message_id: M) -> Self { self._before(message_id.into()); self } @@ -88,13 +107,40 @@ impl GetMessages { /// /// 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 } + + /// 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 does not have permission to view the channel. + /// + /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY + #[cfg(feature = "http")] + pub async fn execute(self, http: impl AsRef) -> Result> { + let mut query = "?".to_string(); + if let Some(limit) = self.limit { + write!(query, "limit={}", limit)?; + } + + 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)?, + } + } + + http.as_ref().get_messages(self.id.into(), &query).await + } } #[derive(Clone, Copy, Debug)] diff --git a/src/framework/standard/help_commands.rs b/src/framework/standard/help_commands.rs index 5dc8c3bccf2..d4e348077d5 100644 --- a/src/framework/standard/help_commands.rs +++ b/src/framework/standard/help_commands.rs @@ -1023,18 +1023,16 @@ 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 = builder::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 + channel_id.send_message().embed(embed).execute(http.as_ref()).await } /// Sends embed showcasing information about a single command. @@ -1046,76 +1044,65 @@ async fn send_single_command_embed( 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 = builder::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.get(0) - { - 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.get(0) { + 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.get(0) - { - 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.get(0) { + 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 + channel_id.send_message().embed(embed).execute(http.as_ref()).await } /// Sends embed listing commands that are similar to the sent one. @@ -1129,7 +1116,8 @@ 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 = builder::CreateEmbed::default().colour(colour).description(text); + channel_id.send_message().embed(embed).execute(http.as_ref()).await } /// Sends an embed explaining fetching commands failed. @@ -1140,7 +1128,8 @@ async fn send_error_embed( input: &str, colour: Colour, ) -> Result { - channel_id.send_message(&http, |m| m.embed(|e| e.colour(colour).description(input))).await + let embed = builder::CreateEmbed::default().colour(colour).description(input); + channel_id.send_message().embed(embed).execute(http.as_ref()).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 64b4b81c104..4216d0ca7ad 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -867,19 +867,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), @@ -887,6 +889,7 @@ impl Http { guild_id, }, }) + .await } /// Creates a webhook for the given [channel][`GuildChannel`]'s Id, passing in @@ -2175,20 +2178,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: /// @@ -2201,9 +2203,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 determine by Discord. /// /// # Examples /// diff --git a/src/model/application/interaction/application_command.rs b/src/model/application/interaction/application_command.rs index aa9c7d5434c..1aece1a52b4 100644 --- a/src/model/application/interaction/application_command.rs +++ b/src/model/application/interaction/application_command.rs @@ -84,56 +84,12 @@ impl ApplicationCommandInteraction { http.as_ref().get_original_interaction_response(&self.token).await } - /// Creates a response to the interaction received. + /// Returns a request builder that, when executed, will create a response to the received + /// interaction. /// /// **Note**: Message contents must be under 2000 unicode 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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn create_interaction_response<'a, F>( - &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>, - ) -> 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 - } + pub fn create_interaction_response(&self) -> CreateInteractionResponse<'_> { + CreateInteractionResponse::new(self.id, &self.token) } /// Edits the initial interaction response. @@ -177,85 +133,23 @@ impl ApplicationCommandInteraction { http.as_ref().delete_original_interaction_response(&self.token).await } - /// Creates a followup response to the response sent. + /// Returns a request builder that, when executed, creates a followup response to the response + /// previously sent. /// /// **Note**: Message contents must be under 2000 unicode code points. - /// - /// # 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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn create_followup_message<'a, F>( - &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 + pub async fn create_followup_message(&self) -> CreateInteractionResponseFollowup<'_> { + CreateInteractionResponseFollowup::new(None, &self.token) } - async fn _create_followup_message<'a>( - &self, - http: &Http, - mut interaction_response: 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 - } - } - - /// Edits a followup response to the response sent. + /// Returns a request builder that, when executed, edits a followup response to the response + /// previously sent. /// /// **Note**: Message contents must be under 2000 unicode code points. - /// - /// # 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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn edit_followup_message<'a, F, M: Into>( + pub async fn edit_followup_message( &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, + ) -> CreateInteractionResponseFollowup<'_> { + CreateInteractionResponseFollowup::new(Some(message_id.into()), &self.token) } /// Deletes a followup message. @@ -286,21 +180,17 @@ 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. - /// - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json + /// 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 + self.create_interaction_response() + .kind(InteractionResponseType::DeferredChannelMessageWithSource) + .execute(&http) + .await } } diff --git a/src/model/application/interaction/autocomplete.rs b/src/model/application/interaction/autocomplete.rs index b6b598537d9..c66e587c090 100644 --- a/src/model/application/interaction/autocomplete.rs +++ b/src/model/application/interaction/autocomplete.rs @@ -6,13 +6,9 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "http")] use crate::builder::CreateAutocompleteResponse; -#[cfg(feature = "http")] -use crate::http::Http; use crate::internal::prelude::*; use crate::model::application::command::{CommandOptionType, CommandType}; use crate::model::application::interaction::add_guild_id_to_resolved; -#[cfg(feature = "http")] -use crate::model::application::interaction::InteractionResponseType; use crate::model::guild::Member; use crate::model::id::{ ApplicationId, @@ -64,33 +60,10 @@ pub struct AutocompleteInteraction { #[cfg(feature = "http")] impl AutocompleteInteraction { - /// Creates a response to an autocomplete interaction. - /// - /// # 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 + /// Returns a request builder that creates a response to an autocomplete interaction when + /// executed. + pub async fn create_autocomplete_response(&self) -> CreateAutocompleteResponse<'_> { + CreateAutocompleteResponse::new(self.id, &self.token) } } diff --git a/src/model/application/interaction/message_component.rs b/src/model/application/interaction/message_component.rs index 17a3d9c5f88..ac6ee503366 100644 --- a/src/model/application/interaction/message_component.rs +++ b/src/model/application/interaction/message_component.rs @@ -72,50 +72,12 @@ impl MessageComponentInteraction { http.as_ref().get_original_interaction_response(&self.token).await } - /// Creates a response to the interaction received. + /// Returns a request builder that, when executed, will create a response to the received + /// interaction. /// /// **Note**: Message contents must be under 2000 unicode 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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn create_interaction_response<'a, F>( - &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 - } + pub fn create_interaction_response(&self) -> CreateInteractionResponse<'_> { + CreateInteractionResponse::new(self.id, &self.token) } /// Edits the initial interaction response. @@ -161,65 +123,23 @@ impl MessageComponentInteraction { http.as_ref().delete_original_interaction_response(&self.token).await } - /// Creates a followup response to the response sent. + /// Returns a request builder that, when executed, creates a followup response to the response + /// previously sent. /// /// **Note**: Message contents must be under 2000 unicode code points. - /// - /// # 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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn create_followup_message<'a, F>( - &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 + pub async fn create_followup_message(&self) -> CreateInteractionResponseFollowup<'_> { + CreateInteractionResponseFollowup::new(None, &self.token) } - /// Edits a followup response to the response sent. + /// Returns a request builder that, when executed, edits a followup response to the response + /// previously sent. /// /// **Note**: Message contents must be under 2000 unicode code points. - /// - /// # 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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn edit_followup_message<'a, F, M: Into>( + pub async fn edit_followup_message( &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, + ) -> CreateInteractionResponseFollowup<'_> { + CreateInteractionResponseFollowup::new(Some(message_id.into()), &self.token) } /// Deletes a followup message. @@ -250,23 +170,17 @@ 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 - /// - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json + /// 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 + self.create_interaction_response() + .kind(InteractionResponseType::DeferredUpdateMessage) + .execute(&http) + .await } } diff --git a/src/model/application/interaction/modal.rs b/src/model/application/interaction/modal.rs index b7548801819..8c3b392ba61 100644 --- a/src/model/application/interaction/modal.rs +++ b/src/model/application/interaction/modal.rs @@ -74,50 +74,12 @@ impl ModalSubmitInteraction { http.as_ref().get_original_interaction_response(&self.token).await } - /// Creates a response to the interaction received. + /// Returns a request builder that, when executed, will create a response to the received + /// interaction. /// /// **Note**: Message contents must be under 2000 unicode 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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn create_interaction_response<'a, F>( - &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 - } + pub fn create_interaction_response(&self) -> CreateInteractionResponse<'_> { + CreateInteractionResponse::new(self.id, &self.token) } /// Edits the initial interaction response. @@ -163,65 +125,23 @@ impl ModalSubmitInteraction { http.as_ref().delete_original_interaction_response(&self.token).await } - /// Creates a followup response to the response sent. + /// Returns a request builder that, when executed, creates a followup response to the response + /// previously sent. /// /// **Note**: Message contents must be under 2000 unicode code points. - /// - /// # 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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn create_followup_message<'a, F>( - &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 + pub async fn create_followup_message(&self) -> CreateInteractionResponseFollowup<'_> { + CreateInteractionResponseFollowup::new(None, &self.token) } - /// Edits a followup response to the response sent. + /// Returns a request builder that, when executed, edits a followup response to the response + /// previously sent. /// /// **Note**: Message contents must be under 2000 unicode code points. - /// - /// # 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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn edit_followup_message<'a, F, M: Into>( + pub async fn edit_followup_message( &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, + ) -> CreateInteractionResponseFollowup<'_> { + CreateInteractionResponseFollowup::new(Some(message_id.into()), &self.token) } /// Deletes a followup message. @@ -237,23 +157,18 @@ impl ModalSubmitInteraction { ) -> Result<()> { http.as_ref().delete_followup_message(&self.token, message_id.into().into()).await } - /// 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. + + /// Helper function to defer an interaction. /// /// # Errors /// - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json + /// 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 + self.create_interaction_response() + .kind(InteractionResponseType::DeferredUpdateMessage) + .execute(&http) + .await } } diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index 492ee9d84ea..d37a256277d 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; @@ -78,19 +75,13 @@ impl ChannelId { /// /// **Note**: Requires the [Create Instant Invite] permission. /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - /// /// [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 fn create_invite(self) -> CreateInvite { + CreateInvite::new( + self, + #[cfg(feature = "cache")] + None, + ) } /// Creates a [permission overwrite][`PermissionOverwrite`] for either a @@ -476,36 +467,9 @@ impl ChannelId { cache_http.http().get_message(self.get(), message_id.get()).await } - /// 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. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user does not have - /// permission to view the channel. - /// - /// [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 + /// Returns a request builder that gets messages from the channel when executed. + pub fn messages(self) -> GetMessages { + GetMessages::new(self) } /// Streams over all the messages in a channel. @@ -644,27 +608,25 @@ impl ChannelId { /// Sends a message with just the given message content in the channel. /// + /// **Note**: Message contents 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 of the message is over the above + /// limit, containing the number of unicode code points over the limit. #[inline] pub async fn say(self, http: impl AsRef, content: impl Into) -> Result { - self.send_message(&http, |m| m.content(content)).await + self.send_message().content(content).execute(http.as_ref()).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. - /// - /// The [Attach Files] and [Send Messages] permissions are required. + /// Returns a request builder that, when executed, sends file(s) along with optional message + /// contents. The filename _must_ be specified. /// - /// **Note**: Message contents must be under 2000 unicode code points, and embeds must be under - /// 6000 unicode code points. + /// Message contents may be passed by calling the [`CreateMessage::content`] method on the + /// returned builder. /// + /// Refer to the documentation for [`CreateMessage`] for more information regarding message + /// restrictions and requirements. /// /// # Examples /// @@ -682,7 +644,7 @@ impl ChannelId { /// /// 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 _ = channel_id.send_files(paths).content("a file").execute(&http).await; /// # } /// ``` /// @@ -704,83 +666,30 @@ 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 _ = channel_id.send_files(files).content("a file").execute(&http).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. - /// - /// [`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>( - self, - http: impl AsRef, - files: It, - f: F, - ) -> Result + pub fn send_files<'a, T, It>(self, files: It) -> CreateMessage<'a> 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 + self.send_message().files(files) } - /// 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. + /// Returns a request builder that sends a message to the channel when executed. /// - /// **Note**: Message contents 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 [`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) + /// Refer to the documentation for [`CreateMessage`] for more information regarding message + /// restrictions and requirements. + pub fn send_message<'a>(self) -> CreateMessage<'a> { + CreateMessage::new( + self, + #[cfg(feature = "cache")] + None, + ) } /// Starts typing in the channel for an indefinite period of time. @@ -868,22 +777,8 @@ impl ChannelId { /// 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 + pub fn create_webhook(self) -> CreateWebhook { + CreateWebhook::new(self) } /// Returns a future that will await one message sent in this channel. @@ -926,24 +821,9 @@ impl ChannelId { http.as_ref().get_stage_instance(self.get()).await } - /// Creates a stage instance. - /// - /// # 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 a request builder that creates a [`StageInstance`] when executed. + pub fn create_stage_instance(self) -> CreateStageInstance { + CreateStageInstance::new(self) } /// Edits a stage instance. @@ -991,44 +871,15 @@ impl ChannelId { http.as_ref().delete_stage_instance(self.get()).await } - /// Creates a public thread that is connected to a message. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - 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 + /// Returns a request builder that, when executed, will create a public thread that is + /// connected to a message. + pub fn create_public_thread(self, message_id: impl Into) -> CreateThread { + CreateThread::new(self, Some(message_id.into())) } - /// Creates a private thread. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - 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 + /// Returns a request builder that will create a private thread on execution. + pub fn create_private_thread(self) -> CreateThread { + CreateThread::new(self, None).kind(ChannelType::PrivateThread) } /// Gets the thread members, if this channel is a thread. @@ -1215,16 +1066,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 = self.channel_id.messages().limit(grab_size); + if let Some(before) = self.before { + builder = builder.before(before); + } + self.buffer = builder.execute(&self.http).await?; self.buffer.reverse(); diff --git a/src/model/channel/embed.rs b/src/model/channel/embed.rs index d8e35949b08..29d0735778e 100644 --- a/src/model/channel/embed.rs +++ b/src/model/channel/embed.rs @@ -1,16 +1,13 @@ -#[cfg(feature = "model")] -use crate::builder::CreateEmbed; #[cfg(feature = "utils")] use crate::utils::Colour; -/// Represents a rich embed which allows using richer markdown, multiple fields -/// and more. This was heavily inspired by [slack's attachments]. +/// Represents a rich embed which allows using richer markdown, multiple fields and more. This was +/// heavily inspired by [slack's attachments]. /// -/// You can include an attachment in your own message by a user or a bot, or in -/// a webhook. +/// You can include an attachment in your own message by a user or a bot, or in a webhook. /// -/// **Note**: Maximum amount of characters you can put is 256 in a field name, -/// 1024 in a field value, and 2048 in a description. +/// **Note**: Maximum amount of characters you can put is 256 in a field name, 1024 in a field +/// value, and 2048 in a description. /// /// [slack's attachments]: https://api.slack.com/docs/message-attachments #[derive(Clone, Debug, Deserialize, Serialize)] @@ -45,8 +42,8 @@ pub struct Embed { pub kind: Option, /// Provider information for the embed. /// - /// For example, if the embed [`Self::kind`] is `"video"`, the provider might - /// contain information about YouTube. + /// For example, if the embed [`Self::kind`] is `"video"`, the provider might contain + /// information about YouTube. pub provider: Option, /// Thumbnail information of the embed. pub thumbnail: Option, @@ -62,40 +59,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. #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index 085cb122bb0..55ebfc4b2f8 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -169,36 +169,18 @@ 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 invite = channel.create_invite().max_uses(5).execute(&context).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. - /// /// [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 fn create_invite(&self) -> CreateInvite { + CreateInvite::new( + self.id, + #[cfg(feature = "cache")] + Some(self.guild_id), + ) } /// Creates a [permission overwrite][`PermissionOverwrite`] for either a @@ -310,7 +292,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), @@ -414,7 +396,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), @@ -667,26 +649,10 @@ impl GuildChannel { self.id.message(&cache_http, message_id).await } - /// 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. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission to - /// view the channel. - /// - /// [Read Message History]: Permissions::READ_MESSAGE_HISTORY + /// Returns a request builder that gets messages from the channel when executed. #[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 fn messages(&self) -> GetMessages { + self.id.messages() } /// Returns the name of the guild channel. @@ -770,9 +736,9 @@ impl GuildChannel { /// /// let _ = msg /// .channel_id - /// .send_files(&context.http, vec![(&file, "cat.png")], |m| { - /// m.content("here's a cat") - /// }) + /// .send_files(vec![(&file, "cat.png")]) + /// .content("here's a cat") + /// .execute(&context.http) /// .await; /// } /// } @@ -896,83 +862,44 @@ impl GuildChannel { /// Sends a message with just the given message content in the channel. /// + /// **Note**: Message contents 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 of the message is over the above + /// limit, containing the number of unicode code points over the limit. /// - /// May also return [`Error::Http`] if the current user lacks permission - /// to send a message to the channel. + /// Returns an [`Error::Http`] if the current user lacks permission to send a message to the + /// channel. #[inline] pub async fn say(&self, http: impl AsRef, content: impl Into) -> Result { self.id.say(&http, content).await } - /// Sends (a) 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. + /// Returns a request builder that, when executed, sends (a) file(s) to the channel, along with + /// optional message contents. /// - /// **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 + /// Refer to the documentation for [`CreateMessage`] for more information regarding message + /// restrictions and requirements. #[inline] - pub async fn send_files<'a, F, T, It>( - &self, - http: impl AsRef, - files: It, - f: F, - ) -> Result + pub fn send_files<'a, T, It>(&self, files: It) -> CreateMessage<'a> 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(files) } - /// Sends a message to the channel with the given content. - /// - /// **Note**: Requires the [Send Messages] permission. - /// - /// # 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 + /// Returns a request builder that sends a message to the channel when executed. + /// + /// Refer to the documentation for [`CreateMessage`] for more information regarding message + /// restrictions and requirements. + pub fn send_message<'a>(&self) -> CreateMessage<'a> { + CreateMessage::new( + self.id, + #[cfg(feature = "cache")] + Some(self.guild_id), + ) } /// Starts typing in the channel for an indefinite period of time. @@ -1147,15 +1074,8 @@ impl GuildChannel { /// 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)) - } + pub fn create_webhook(&self) -> CreateWebhook { + self.id.create_webhook() } /// Gets a stage instance. @@ -1172,25 +1092,9 @@ impl GuildChannel { self.id.get_stage_instance(http).await } - /// Creates a 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. - 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 + /// Returns a request builder that creates a [`StageInstance`] when executed. + pub fn create_stage_instance(&self) -> CreateStageInstance { + self.id.create_stage_instance() } /// Edits a stage instance. @@ -1228,37 +1132,15 @@ impl GuildChannel { self.id.delete_stage_instance(http).await } - /// Creates a public thread that is connected to a message. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - 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 + /// Returns a request builder that, when executed, will create a public thread that is + /// connected to a message. + pub fn create_public_thread(&self, message_id: impl Into) -> CreateThread { + self.id.create_public_thread(message_id) } - /// Creates a private thread. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - 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 + /// Returns a request builder that will create a private thread on execution. + pub fn create_private_thread(&self) -> CreateThread { + self.id.create_private_thread() } } diff --git a/src/model/channel/message.rs b/src/model/channel/message.rs index 3b382733338..077073a566c 100644 --- a/src/model/channel/message.rs +++ b/src/model/channel/message.rs @@ -27,6 +27,8 @@ use crate::json::prelude::*; use crate::model::application::component::ActionRow; use crate::model::application::interaction::MessageInteraction; use crate::model::prelude::*; +#[cfg(all(feature = "cache", feature = "model"))] +use crate::utils; #[cfg(feature = "model")] use crate::{ constants, @@ -684,22 +686,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 + let mut builder = self.channel_id.send_message().content(content); + if let Some(ping_user) = inlined { + builder = 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.execute(cache_http.http()).await } /// Delete all embeds in this message diff --git a/src/model/channel/private_channel.rs b/src/model/channel/private_channel.rs index 48e97bb1899..8e2dcf23274 100644 --- a/src/model/channel/private_channel.rs +++ b/src/model/channel/private_channel.rs @@ -196,19 +196,10 @@ impl PrivateChannel { self.id.message(&cache_http, message_id).await } - /// Gets messages from the channel. - /// - /// Refer to [`GetMessages`] for more information on how to use `builder`. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if an invalid value is set in the builder. + /// Returns a request builder that gets messages from the channel when executed. #[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 fn messages(&self) -> GetMessages { + self.id.messages() } /// Returns "DM with $username#discriminator". @@ -294,36 +285,21 @@ impl PrivateChannel { /// /// [Send Messages]: Permissions::SEND_MESSAGES #[inline] - pub async fn send_files<'a, F, T, It>( - &self, - http: impl AsRef, - files: It, - f: F, - ) -> Result + pub fn send_files<'a, T, It>(&self, files: It) -> CreateMessage<'a> 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(files) } /// Sends a message to the channel with the given content. /// - /// Refer to the documentation for [`CreateMessage`] for more information - /// regarding message 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. + /// Refer to the documentation for [`CreateMessage`] for more information regarding message + /// restrictions and requirements. #[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 fn send_message<'a>(&self) -> CreateMessage<'a> { + self.id.send_message() } /// 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 9c5c9b7ff9b..baa93461bec 100644 --- a/src/model/channel/reaction.rs +++ b/src/model/channel/reaction.rs @@ -112,7 +112,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, @@ -151,7 +151,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 4629a02608f..8fd4f9f2bb2 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -48,25 +48,11 @@ use crate::model::prelude::*; #[cfg(feature = "model")] 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. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. + /// Returns a request builder that, when executed, will add a [`User`] to this guild with a + /// valid OAuth2 access token. #[inline] - pub async fn add_member( - self, - http: impl AsRef, - user_id: impl Into, - f: impl FnOnce(&mut AddMember) -> &mut AddMember, - ) -> Result> { - let mut builder = AddMember::default(); - f(&mut builder); - - http.as_ref().add_guild_member(self.get(), user_id.into().get(), &builder).await + pub fn add_member(self, user_id: impl Into) -> AddMember { + AddMember::new(self, user_id.into()) } /// Ban a [`User`] from the guild, deleting a number of @@ -200,11 +186,12 @@ impl GuildId { Ok(channels.into_iter().map(|c| (c.id, c)).collect()) } - /// Creates a [`GuildChannel`] in the the guild. + /// Returns a request builder that will create a new [`GuildChannel`] in the corresponding + /// guild when executed. /// /// Refer to [`Http::create_channel`] for more information. /// - /// Requires the [Manage Channels] permission. + /// **Note**: Requires the [Manage Channels] permission. /// /// # Examples /// @@ -218,26 +205,14 @@ impl GuildId { /// # use serenity::http::Http; /// # let http = Http::new("token"); /// let _channel = - /// GuildId::new(7).create_channel(&http, |c| c.name("test").kind(ChannelType::Voice)).await; + /// GuildId::new(7).create_channel().name("test").kind(ChannelType::Voice).execute(&http).await; /// # } /// ``` /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission, - /// or if invalid values are set. - /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS #[inline] - pub async fn create_channel( - self, - http: impl AsRef, - f: impl FnOnce(&mut CreateChannel) -> &mut CreateChannel, - ) -> Result { - let mut builder = CreateChannel::default(); - f(&mut builder); - - http.as_ref().create_channel(self.get(), &builder, None).await + pub fn create_channel(self) -> CreateChannel { + CreateChannel::new(self) } /// Creates an emoji in the guild with a name and base64-encoded image. @@ -329,51 +304,24 @@ impl GuildId { Ok(role) } - /// Creates a new scheduled event in the guild with the data set, if any. + /// Returns a request builder that will create a new [`ScheduledEvent`] in the guild when + /// executed. /// /// **Note**: Requires the [Manage Events] permission. /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission, or 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 fn create_scheduled_event(self) -> CreateScheduledEvent { + CreateScheduledEvent::new(self) } - /// Creates a new sticker in the guild with the data set, if any. + /// Returns a request builder that will create a new [`Sticker`] in the guild when executed. /// /// **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]: crate::model::permissions::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 fn create_sticker<'a>(self) -> CreateSticker<'a> { + CreateSticker::new(self) } /// Deletes the current guild if the current account is the owner of the diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index ffc17079a5e..177d69d6a87 100644 --- a/src/model/guild/mod.rs +++ b/src/model/guild/mod.rs @@ -316,7 +316,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; @@ -462,22 +466,11 @@ impl Guild { self.id.bans(cache_http.http()).await } - /// 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 values are set. + /// Returns a request builder that adds on execution will add a [`User`] to this guild with a + /// valid OAuth2 access token. #[inline] - pub async fn add_member( - &self, - http: impl AsRef, - user_id: impl Into, - f: impl FnOnce(&mut AddMember) -> &mut AddMember, - ) -> Result> { - self.id.add_member(http, user_id, f).await + pub fn add_member(&self, user_id: impl Into) -> AddMember { + self.id.add_member(user_id) } /// Retrieves a list of [`AuditLogs`] for the guild. @@ -554,47 +547,37 @@ impl Guild { http.as_ref().create_guild(&map).await } - /// Creates a new [`Channel`] in the guild. + /// Returns a request builder that will create a new [`GuildChannel`] in the guild when + /// executed. /// /// **Note**: Requires the [Manage Channels] permission. /// /// # 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::model::channel::ChannelType; /// /// // assuming a `guild` has already been bound /// - /// let _ = guild - /// .create_channel(&http, |c| c.name("my-test-channel").kind(ChannelType::Text)) - /// .await; + /// # async fn run() -> serenity::Result<()> { + /// # let http = Http::new("token"); + /// # let guild = Guild::get(&http, GuildId::new(7)).await?; + /// let channel = guild + /// .create_channel() + /// .name("my-test-channel") + /// .kind(ChannelType::Text) + /// .execute(&http) + /// .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. - /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - pub async fn create_channel( - &self, - cache_http: impl CacheHttp, - f: impl FnOnce(&mut CreateChannel) -> &mut 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 + pub fn create_channel(&self) -> CreateChannel { + self.id.create_channel() } /// Creates an emoji in the guild with a name and base64-encoded image. The @@ -870,66 +853,23 @@ impl Guild { self.id.create_role(cache_http.http(), f).await } - /// Creates a new scheduled event in the guild with the data set, if any. + /// Returns a request builder that will create a new [`ScheduledEvent`] in the guild when + /// executed. /// /// **Note**: Requires the [Manage Events] permission. /// - /// # 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. - /// /// [Manage Events]: Permissions::MANAGE_EVENTS - 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 + pub fn create_scheduled_event(&self) -> CreateScheduledEvent { + self.id.create_scheduled_event() } - /// Creates a new sticker in the guild with the data set, if any. + /// Returns a request builder that will create a new [`Sticker`] in the guild when executed. /// /// **Note**: Requires the [Manage Emojis and Stickers] permission. /// - /// # Errors - /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] - /// if the current user does not have permission to manage roles. - /// /// [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 + pub fn create_sticker<'a>(&self) -> CreateSticker<'a> { + self.id.create_sticker() } /// Deletes the current guild if the current user is the owner of the diff --git a/src/model/guild/partial_guild.rs b/src/model/guild/partial_guild.rs index fe1912ec28d..d28b34ad943 100644 --- a/src/model/guild/partial_guild.rs +++ b/src/model/guild/partial_guild.rs @@ -274,36 +274,40 @@ impl PartialGuild { None } - /// Creates a [`GuildChannel`] in the guild. + /// Returns a request builder that will create a new [`GuildChannel`] in the guild when + /// executed. /// /// 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; - /// - /// guild.create_channel(|c| c.name("test").kind(ChannelType::Voice)); + /// ```rust,no_run + /// # use serenity::http::Http; + /// # use serenity::model::guild::PartialGuild; + /// # use serenity::model::id::GuildId; + /// use serenity::model::channel::ChannelType; + /// + /// # async fn run() -> serenity::Result<()> { + /// # let http = Http::new("token"); + /// # let guild = PartialGuild::get(&http, GuildId::new(7)).await?; + /// let channel = guild + /// .create_channel() + /// .name("my-test-channel") + /// .kind(ChannelType::Voice) + /// .execute(&http) + /// .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. - /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS #[inline] - pub async fn create_channel( - &self, - http: impl AsRef, - f: impl FnOnce(&mut CreateChannel) -> &mut CreateChannel, - ) -> Result { - self.id.create_channel(&http, f).await + pub fn create_channel(&self) -> CreateChannel { + self.id.create_channel() } /// Creates an emoji in the guild with a name and base64-encoded image. @@ -559,32 +563,13 @@ impl PartialGuild { self.id.create_role(&http, f).await } - /// Creates a new sticker in the guild with the data set, if any. + /// Returns a request builder that will create a new [`Sticker`] in the guild when executed. /// /// **Note**: Requires the [Manage Emojis and Stickers] permission. /// - /// # Errors - /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] - /// if the current user does not have permission to manage roles. - /// /// [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 + pub fn create_sticker<'a>(&self) -> CreateSticker<'a> { + self.id.create_sticker() } /// Deletes the current guild if the current user is the owner of the @@ -1148,7 +1133,11 @@ impl PartialGuild { } #[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; diff --git a/src/model/invite.rs b/src/model/invite.rs index f1d7a712244..a614401b7ac 100644 --- a/src/model/invite.rs +++ b/src/model/invite.rs @@ -2,7 +2,8 @@ use super::prelude::*; #[cfg(all(feature = "cache", feature = "model"))] -use super::{utils as model_utils, Permissions}; +use super::Permissions; +use super::Timestamp; #[cfg(feature = "model")] use crate::builder::CreateInvite; #[cfg(all(feature = "cache", feature = "model"))] @@ -11,7 +12,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. /// @@ -70,39 +70,11 @@ impl Invite { /// /// 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]. - /// /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE /// [permission]: super::permissions #[inline] - 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 + pub fn create(channel_id: impl Into) -> CreateInvite { + channel_id.into().create_invite() } /// Deletes the invite. @@ -125,7 +97,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, @@ -344,7 +316,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 bc221e177bc..bc93f31750e 100644 --- a/src/model/mention.rs +++ b/src/model/mention.rs @@ -37,15 +37,15 @@ pub trait Mentionable { /// ) -> 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(), - /// )) - /// }) + /// .send_message() + /// .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(), + /// )) + /// .execute(ctx) /// .await?; /// Ok(()) /// } diff --git a/src/model/user.rs b/src/model/user.rs index 3c3deb4936f..384c48ef2c3 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -229,11 +229,8 @@ impl CurrentUser { default_avatar_url(self.discriminator) } - /// Edits the current user's profile settings. - /// - /// This mutates the current user in-place. - /// - /// Refer to [`EditProfile`]'s documentation for its methods. + /// Returns a builder that edits the current user's profile settings when executed, mutating + /// the current user in-place. /// /// # Examples /// @@ -248,30 +245,13 @@ impl CurrentUser { /// # let mut user = CurrentUser::default(); /// let avatar = serenity::utils::read_image("./avatar.png")?; /// - /// user.edit(&http, |p| p.avatar(Some(avatar))).await; + /// user.edit().avatar(Some(avatar)).execute(&http).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. - /// - /// [`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 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?; - - Ok(()) + pub fn edit(&mut self) -> EditProfile<'_> { + let name = self.name.clone(); + EditProfile::new(self).username(name) } /// Retrieves the URL to the current user's avatar, falling back to the @@ -476,13 +456,13 @@ impl CurrentUser { permissions: Permissions, scopes: &[Scope], ) -> Result { - let mut builder = CreateBotAuthParameters::default(); - - builder.permissions(permissions); - builder.auto_client_id(http).await?; - builder.scopes(scopes); - - Ok(builder.build()) + let url = CreateBotAuthParameters::default() + .permissions(permissions) + .scopes(scopes) + .auto_client_id(&http) + .await? + .build(); + Ok(url) } /// Returns a static formatted URL of the user's icon, if one exists. @@ -805,8 +785,11 @@ 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. + /// Returns a builder that, when executed, 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.' + /// + /// This method first attempts to create the channel if it does not already exist, before + /// returning a [`CreateMessage`] builder. /// /// # Examples /// @@ -837,13 +820,19 @@ impl User { /// }, /// }; /// - /// let help = format!("Helpful info here. Invite me with this link: <{}>", url,); - /// - /// let dm = msg.author.direct_message(&ctx, |m| m.content(&help)).await; - /// - /// match dm { - /// Ok(_) => { - /// let _ = msg.react(&ctx, '👌').await; + /// match msg.author.direct_message(&ctx).await { + /// Ok(create_message) => { + /// let help = format!("Invite me with this link: <{}>", url); + /// match create_message.content(&help).execute(&ctx).await { + /// Ok(_) => { + /// let _ = msg.react(&ctx, '👌').await; + /// }, + /// Err(why) => { + /// println!("Err sending help: {:?}", why); + /// + /// let _ = msg.reply(&ctx, "There was an error DMing you help.").await; + /// }, + /// } /// }, /// Err(why) => { /// println!("Err sending help: {:?}", why); @@ -865,32 +854,26 @@ 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. + /// [`Error::Json`] can also be returned if there is an error deserializing the API response. /// /// [`Error::Http`]: crate::error::Error::Http /// [`Error::Json`]: crate::error::Error::Json - 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 + pub async fn direct_message<'a>( + &self, + cache_http: impl CacheHttp, + ) -> Result> { + Ok(self.create_dm_channel(&cache_http).await?.send_message()) } /// 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) -> Result> { + self.direct_message(cache_http).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 072b3e3c911..450bbbcea02 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::*; use crate::json::from_number; use crate::model::application::command::CommandOptionType; use crate::model::application::interaction::application_command::{ @@ -381,76 +375,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 b4521023779..bdc9c4286af 100644 --- a/src/model/webhook.rs +++ b/src/model/webhook.rs @@ -222,9 +222,8 @@ impl Webhook { http.as_ref().delete_webhook_with_token(self.id.get(), token).await } - /// Edits the webhook + /// Returns a request builder that edits the webhook when executed. /// - /// Does not require authentication, as this calls [`Http::edit_webhook_with_token`] internally. /// # Examples /// /// ```rust,no_run @@ -236,39 +235,16 @@ 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?; + /// webhook.edit().name("new name").execute(&http).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 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, - { - 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?; - Ok(()) + pub fn edit(&mut self) -> EditWebhook<'_> { + EditWebhook::new(self) } - /// 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. + /// Returns a request builder that, when executed, executes the webhook with given content, + /// according to what fields are set on it. /// /// # Examples /// @@ -283,13 +259,13 @@ 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?; + /// webhook.execute().content("test").execute(&http).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 setting an embed: /// /// ```rust,no_run /// # use serenity::http::Http; @@ -297,126 +273,32 @@ impl Webhook { /// # /// # async fn run() -> Result<(), Box> { /// # let http = Http::new("token"); - /// use serenity::model::channel::Embed; + /// use serenity::builder::CreateEmbed; /// /// 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") - /// }); + /// 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"); /// /// webhook - /// .execute(&http, false, |w| w.content("test").username("serenity").embeds(vec![embed])) + /// .execute() + /// .content("test") + /// .username("serenity") + /// .embeds(vec![embed]) + /// .execute(&http) /// .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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - #[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`: - /// - /// ```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?; - /// # Ok(()) - /// # } - /// ``` - /// - /// # Errors - /// - /// Returns an [`Error::Model`] if the [`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`]. - /// - /// 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>( - &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>, - { - 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 - } + pub fn execute(&self) -> ExecuteWebhook<'_> { + ExecuteWebhook::new(self) } /// Gets a previously sent message from the webhook. @@ -443,34 +325,9 @@ impl Webhook { http.as_ref().get_webhook_message(self.id.get(), token, message_id.get()).await } - /// Edits a webhook message with the fields set via the given builder. - /// - /// # Errors - /// - /// Returns an [`Error::Model`] if the [`Self::token`] is [`None`]. - /// - /// 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. - /// - /// [`Error::Model`]: crate::error::Error::Model - /// [`Error::Http`]: crate::error::Error::Http - /// [`Error::Json`]: crate::error::Error::Json - pub async fn edit_message( - &self, - http: impl AsRef, - message_id: MessageId, - f: F, - ) -> Result - where - F: FnOnce(&mut EditWebhookMessage) -> &mut EditWebhookMessage, - { - 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 + /// Returns a request builder that edits a webhook message when executed. + pub fn edit_message(&self, message_id: MessageId) -> EditWebhookMessage<'_> { + EditWebhookMessage::new(self, message_id) } /// Deletes a webhook message. diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 9eff568fa72..8b55f4ff48a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -30,9 +30,13 @@ 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; +#[cfg(all(feature = "cache", feature = "model"))] +use crate::model::permissions::Permissions; +use crate::model::prelude::*; #[cfg(feature = "model")] pub(crate) fn encode_image(raw: &[u8]) -> String { @@ -443,6 +447,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. ///