From fe5f89df0ed3f2d635e15b3bfdf51c56f171cdec Mon Sep 17 00:00:00 2001 From: GnomedDev Date: Sat, 19 Jul 2025 13:36:15 +0100 Subject: [PATCH] Remove ArgumentConvert trait and implementations --- src/utils/argument_convert/_template.rs | 45 ------ src/utils/argument_convert/channel.rs | 169 -------------------- src/utils/argument_convert/emoji.rs | 74 --------- src/utils/argument_convert/guild.rs | 55 ------- src/utils/argument_convert/member.rs | 88 ----------- src/utils/argument_convert/message.rs | 73 --------- src/utils/argument_convert/mod.rs | 195 ------------------------ src/utils/argument_convert/role.rs | 97 ------------ src/utils/argument_convert/user.rs | 61 -------- src/utils/mod.rs | 133 +++++++++++++++- 10 files changed, 129 insertions(+), 861 deletions(-) delete mode 100644 src/utils/argument_convert/_template.rs delete mode 100644 src/utils/argument_convert/channel.rs delete mode 100644 src/utils/argument_convert/emoji.rs delete mode 100644 src/utils/argument_convert/guild.rs delete mode 100644 src/utils/argument_convert/member.rs delete mode 100644 src/utils/argument_convert/message.rs delete mode 100644 src/utils/argument_convert/mod.rs delete mode 100644 src/utils/argument_convert/role.rs delete mode 100644 src/utils/argument_convert/user.rs diff --git a/src/utils/argument_convert/_template.rs b/src/utils/argument_convert/_template.rs deleted file mode 100644 index 422406e9466..00000000000 --- a/src/utils/argument_convert/_template.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::fmt; -use super::ArgumentConvert; -use crate::{model::prelude::*, prelude::*}; - -/// Error that can be returned from [`PLACEHOLDER::convert`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum PLACEHOLDERParseError { -} - -impl std::error::Error for PLACEHOLDERParseError {} - -impl fmt::Display for PLACEHOLDERParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - } - } -} - -/// Look up a [`PLACEHOLDER`] by a string case-insensitively. -/// -/// Requires the cache feature to be enabled. -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by PLACEHOLDER -/// 2. [Lookup by PLACEHOLDER](`crate::utils::parse_PLACEHOLDER`). -#[async_trait::async_trait] -impl ArgumentConvert for PLACEHOLDER { - type Err = PLACEHOLDERParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - let lookup_by_PLACEHOLDER = || PLACEHOLDER; - - lookup_by_PLACEHOLDER() - .or_else(lookup_by_PLACEHOLDER) - .or_else(lookup_by_PLACEHOLDER) - .cloned() - .ok_or(PLACEHOLDERParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/argument_convert/channel.rs b/src/utils/argument_convert/channel.rs deleted file mode 100644 index 99e29c7d3c0..00000000000 --- a/src/utils/argument_convert/channel.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Channel::convert`]. -#[non_exhaustive] -#[derive(Debug)] -pub enum ChannelParseError { - /// When channel retrieval via HTTP failed - Http(SerenityError), - /// The provided channel string failed to parse, or the parsed result cannot be found in the - /// cache. - NotFoundOrMalformed, -} - -impl std::error::Error for ChannelParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Http(e) => Some(e), - Self::NotFoundOrMalformed => None, - } - } -} - -impl fmt::Display for ChannelParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Http(_) => f.write_str("Failed to request channel via HTTP"), - Self::NotFoundOrMalformed => f.write_str("Channel not found or unknown format"), - } - } -} - -fn channel_belongs_to_guild(channel: &Channel, guild: GuildId) -> bool { - match channel { - Channel::GuildThread(channel) => channel.base.guild_id == guild, - Channel::Guild(channel) => channel.base.guild_id == guild, - Channel::Private(_channel) => false, - } -} - -async fn lookup_channel_global( - ctx: impl CacheHttp, - guild_id: Option, - s: &str, -) -> Result { - if let Some(channel_id) = s - .parse() - .ok() - .or_else(|| crate::utils::parse_channel_mention(s)) - .or_else(|| crate::utils::parse_channel_url(s).map(|(_, channel_id)| channel_id)) - { - return channel_id.to_channel(ctx, guild_id).await.map_err(ChannelParseError::Http); - } - - let guild_id = guild_id.ok_or(ChannelParseError::NotFoundOrMalformed)?; - - #[cfg(feature = "cache")] - if let Some(cache) = ctx.cache() { - if let Some(guild) = cache.guild(guild_id) { - let channel = guild.channels.iter().find(|c| c.base.name.eq_ignore_ascii_case(s)); - if let Some(channel) = channel { - return Ok(Channel::Guild(channel.clone())); - } - } - - return Err(ChannelParseError::NotFoundOrMalformed); - } - - let channels = ctx.http().get_channels(guild_id).await.map_err(ChannelParseError::Http)?; - if let Some(channel) = channels.into_iter().find(|c| c.base.name.eq_ignore_ascii_case(s)) { - Ok(Channel::Guild(channel)) - } else { - Err(ChannelParseError::NotFoundOrMalformed) - } -} - -/// Look up a Channel by a string case-insensitively. -/// -/// Lookup are done via local guild. If in DMs, the global cache is used instead. -/// -/// The cache feature needs to be enabled. -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by ID. -/// 2. [Lookup by mention](`crate::utils::parse_channel_mention`). -/// 3. Lookup by name. -#[async_trait::async_trait] -impl ArgumentConvert for Channel { - type Err = ChannelParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - let channel = lookup_channel_global(&ctx, guild_id, s).await?; - - // Don't yield for other guilds' channels - if let Some(guild_id) = guild_id - && !channel_belongs_to_guild(&channel, guild_id) - { - return Err(ChannelParseError::NotFoundOrMalformed); - } - - Ok(channel) - } -} - -/// Error that can be returned from [`GuildChannel::convert`]. -#[non_exhaustive] -#[derive(Debug)] -pub enum GuildChannelParseError { - /// When channel retrieval via HTTP failed - Http(SerenityError), - /// The provided channel string failed to parse, or the parsed result cannot be found in the - /// cache. - NotFoundOrMalformed, - /// When the referenced channel is not a guild channel - NotAGuildChannel, -} - -impl std::error::Error for GuildChannelParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Http(e) => Some(e), - Self::NotFoundOrMalformed | Self::NotAGuildChannel => None, - } - } -} - -impl fmt::Display for GuildChannelParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Http(_) => f.write_str("Failed to request channel via HTTP"), - Self::NotFoundOrMalformed => f.write_str("Channel not found or unknown format"), - Self::NotAGuildChannel => f.write_str("Channel is not a guild channel"), - } - } -} - -/// Look up a GuildChannel by a string case-insensitively. -/// -/// Lookup is done by the global cache, hence the cache feature needs to be enabled. -/// -/// For more information, see the ArgumentConvert implementation for [`Channel`] -#[async_trait::async_trait] -impl ArgumentConvert for GuildChannel { - type Err = GuildChannelParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - channel_id: Option, - s: &str, - ) -> Result { - match Channel::convert(&ctx, guild_id, channel_id, s).await { - Ok(Channel::Guild(channel)) => Ok(channel), - Ok(_) => Err(GuildChannelParseError::NotAGuildChannel), - Err(ChannelParseError::Http(e)) => Err(GuildChannelParseError::Http(e)), - Err(ChannelParseError::NotFoundOrMalformed) => { - Err(GuildChannelParseError::NotFoundOrMalformed) - }, - } - } -} diff --git a/src/utils/argument_convert/emoji.rs b/src/utils/argument_convert/emoji.rs deleted file mode 100644 index 6edc05bd67a..00000000000 --- a/src/utils/argument_convert/emoji.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Emoji::convert`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum EmojiParseError { - /// Parser was invoked outside a guild. - OutsideGuild, - /// Guild was not in cache, or guild HTTP request failed. - FailedToRetrieveGuild, - /// The provided emoji string failed to parse, or the parsed result cannot be found in the - /// guild roles. - NotFoundOrMalformed, -} - -impl std::error::Error for EmojiParseError {} - -impl fmt::Display for EmojiParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::OutsideGuild => f.write_str("Tried to find emoji outside a guild"), - Self::FailedToRetrieveGuild => f.write_str("Could not retrieve guild data"), - Self::NotFoundOrMalformed => f.write_str("Emoji not found or unknown format"), - } - } -} - -/// Look up a [`Emoji`]. -/// -/// Requires the cache feature to be enabled. -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by ID. -/// 2. [Lookup by extracting ID from the emoji](`crate::utils::parse_emoji`). -/// 3. Lookup by name. -#[async_trait::async_trait] -impl ArgumentConvert for Emoji { - type Err = EmojiParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - // Get Guild or PartialGuild - let guild_id = guild_id.ok_or(EmojiParseError::OutsideGuild)?; - let guild = guild_id - .to_partial_guild(&ctx) - .await - .map_err(|_| EmojiParseError::FailedToRetrieveGuild)?; - - let direct_id = s.parse().ok(); - let id_from_mention = crate::utils::parse_emoji(s).map(|e| e.id); - - if let Some(emoji_id) = direct_id.or(id_from_mention) - && let Some(emoji) = guild.emojis.get(&emoji_id).cloned() - { - return Ok(emoji); - } - - if let Some(emoji) = - guild.emojis.iter().find(|emoji| emoji.name.eq_ignore_ascii_case(s)).cloned() - { - return Ok(emoji); - } - - Err(EmojiParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/argument_convert/guild.rs b/src/utils/argument_convert/guild.rs deleted file mode 100644 index 6a9ca408be1..00000000000 --- a/src/utils/argument_convert/guild.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Guild::convert`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum GuildParseError { - /// The provided guild string failed to parse, or the parsed result cannot be found in the - /// cache. - NotFoundOrMalformed, - /// No cache, so no guild search could be done. - NoCache, -} - -impl std::error::Error for GuildParseError {} - -impl fmt::Display for GuildParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotFoundOrMalformed => f.write_str("Guild not found or unknown format"), - Self::NoCache => f.write_str("No cached list of guilds was provided"), - } - } -} - -/// Look up a Guild, either by ID or by a string case-insensitively. -/// -/// Requires the cache feature to be enabled. -#[async_trait::async_trait] -impl ArgumentConvert for Guild { - type Err = GuildParseError; - - async fn convert( - ctx: impl CacheHttp, - _guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - let guilds = &ctx.cache().ok_or(GuildParseError::NoCache)?.guilds; - - let lookup_by_id = || guilds.get(&s.parse().ok()?).map(|g| g.clone()); - - let lookup_by_name = || { - guilds.iter().find_map(|m| { - let guild = m.value(); - guild.name.eq_ignore_ascii_case(s).then(|| guild.clone()) - }) - }; - - lookup_by_id().or_else(lookup_by_name).ok_or(GuildParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/argument_convert/member.rs b/src/utils/argument_convert/member.rs deleted file mode 100644 index fb65764ea5b..00000000000 --- a/src/utils/argument_convert/member.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Member::convert`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum MemberParseError { - /// Parser was invoked outside a guild. - OutsideGuild, - /// The guild in which the parser was invoked is not in cache. - GuildNotInCache, - /// The provided member string failed to parse, or the parsed result cannot be found in the - /// guild cache data. - NotFoundOrMalformed, -} - -impl std::error::Error for MemberParseError {} - -impl fmt::Display for MemberParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::OutsideGuild => f.write_str("Tried to find member outside a guild"), - Self::GuildNotInCache => f.write_str("Guild is not in cache"), - Self::NotFoundOrMalformed => f.write_str("Member not found or unknown format"), - } - } -} - -/// Look up a guild member by a string case-insensitively. -/// -/// Requires the cache feature to be enabled. -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by ID. -/// 2. [Lookup by mention](`crate::utils::parse_user_mention`). -/// 3. [Lookup by name#discrim](`crate::utils::parse_user_tag`). -/// 4. Lookup by name -/// 5. Lookup by nickname -#[async_trait::async_trait] -impl ArgumentConvert for Member { - type Err = MemberParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - let guild_id = guild_id.ok_or(MemberParseError::OutsideGuild)?; - - // DON'T use guild.members: it's only populated when guild presences intent is enabled! - - // If string is a raw user ID or a mention - if let Some(user_id) = s.parse().ok().or_else(|| crate::utils::parse_user_mention(s)) - && let Ok(member) = guild_id.member(&ctx, user_id).await - { - return Ok(member); - } - - // Following code is inspired by discord.py's MemberConvert::query_member_named - - // If string is a username+discriminator - let limit = nonmax::NonMaxU16::new(100); - if let Some((name, discrim)) = crate::utils::parse_user_tag(s) - && let Ok(member_results) = guild_id.search_members(ctx.http(), name, limit).await - && let Some(member) = member_results - .into_iter() - .find(|m| m.user.name.eq_ignore_ascii_case(name) && m.user.discriminator == discrim) - { - return Ok(member); - } - - // If string is username or nickname - if let Ok(member_results) = guild_id.search_members(ctx.http(), s, limit).await - && let Some(member) = member_results.into_iter().find(|m| { - m.user.name.eq_ignore_ascii_case(s) - || m.nick.as_ref().is_some_and(|nick| nick.eq_ignore_ascii_case(s)) - }) - { - return Ok(member); - } - - Err(MemberParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/argument_convert/message.rs b/src/utils/argument_convert/message.rs deleted file mode 100644 index 0081673721f..00000000000 --- a/src/utils/argument_convert/message.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Message::convert`]. -#[non_exhaustive] -#[derive(Debug)] -pub enum MessageParseError { - /// When the provided string does not adhere to any known guild message format - Malformed, - /// When message data retrieval via HTTP failed - Http(SerenityError), - /// When the `gateway` feature is disabled and the required information was not in cache. - HttpNotAvailable, -} - -impl std::error::Error for MessageParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Http(e) => Some(e), - Self::HttpNotAvailable | Self::Malformed => None, - } - } -} - -impl fmt::Display for MessageParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Malformed => { - f.write_str("Provided string did not adhere to any known guild message format") - }, - Self::Http(_) => f.write_str("Failed to request message data via HTTP"), - Self::HttpNotAvailable => f.write_str( - "Gateway feature is disabled and the required information was not in cache", - ), - } - } -} - -/// Look up a message by a string. -/// -/// The lookup strategy is as follows (in order): -/// 1. [Lookup by "{channel ID}-{message ID}"](`crate::utils::parse_message_id_pair`) (retrieved by -/// shift-clicking on "Copy ID") -/// 2. Lookup by message ID (the message must be in the context channel) -/// 3. [Lookup by message URL](`crate::utils::parse_message_url`) -#[async_trait::async_trait] -impl ArgumentConvert for Message { - type Err = MessageParseError; - - async fn convert( - ctx: impl CacheHttp, - _guild_id: Option, - channel_id: Option, - s: &str, - ) -> Result { - let extract_from_message_id = || Some((channel_id?, s.parse().ok()?)); - - let extract_from_message_url = || { - let (_guild_id, channel_id, message_id) = super::parse_message_url(s)?; - Some((channel_id, message_id)) - }; - - let (channel_id, message_id) = super::parse_message_id_pair(s) - .or_else(extract_from_message_id) - .or_else(extract_from_message_url) - .ok_or(MessageParseError::Malformed)?; - - channel_id.message(ctx, message_id).await.map_err(MessageParseError::Http) - } -} diff --git a/src/utils/argument_convert/mod.rs b/src/utils/argument_convert/mod.rs deleted file mode 100644 index 303d5e6a255..00000000000 --- a/src/utils/argument_convert/mod.rs +++ /dev/null @@ -1,195 +0,0 @@ -mod member; -pub use member::*; - -mod message; -pub use message::*; - -mod user; -pub use user::*; - -mod channel; -pub use channel::*; - -// From HTTP you can only get PartialGuild; for Guild you need gateway and cache -#[cfg(feature = "cache")] -mod guild; -#[cfg(feature = "cache")] -pub use guild::*; - -mod role; -pub use role::*; - -mod emoji; -pub use emoji::*; - -use super::DOMAINS; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Parse a value from a string in context of a received message. -/// -/// This trait is a superset of [`std::str::FromStr`]. The difference is that this trait aims to -/// support serenity-specific Discord types like [`Member`] or [`Message`]. -/// -/// Trait implementations may do network requests as part of their parsing procedure. -/// -/// Useful for implementing argument parsing in command frameworks. -#[async_trait::async_trait] -pub trait ArgumentConvert: Sized { - /// The associated error which can be returned from parsing. - type Err; - - /// Parses a string `s` as a command parameter of this type. - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - channel_id: Option, - s: &str, - ) -> Result; -} - -#[async_trait::async_trait] -impl ArgumentConvert for T { - type Err = ::Err; - - async fn convert( - _: impl CacheHttp, - _: Option, - _: Option, - s: &str, - ) -> Result { - T::from_str(s) - } -} - -// The following few parse_XXX methods are in here (parse.rs) because they need to be gated behind -// the model feature and it's just convenient to put them here for that - -/// Retrieves IDs from "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID"). -/// -/// If the string is invalid, None is returned. -/// -/// # Examples -/// ```rust -/// use serenity::model::prelude::*; -/// use serenity::utils::parse_message_id_pair; -/// -/// assert_eq!( -/// parse_message_id_pair("673965002805477386-842482646604972082"), -/// Some((GenericChannelId::new(673965002805477386), MessageId::new(842482646604972082))), -/// ); -/// assert_eq!( -/// parse_message_id_pair("673965002805477386-842482646604972082-472029906943868929"), -/// None, -/// ); -/// ``` -#[must_use] -pub fn parse_message_id_pair(s: &str) -> Option<(GenericChannelId, MessageId)> { - let mut parts = s.splitn(2, '-'); - let channel_id = parts.next()?.parse().ok()?; - let message_id = parts.next()?.parse().ok()?; - Some((channel_id, message_id)) -} - -/// Retrieves guild, channel, and message ID from a message URL. -/// -/// If the URL is malformed, None is returned. -/// -/// # Examples -/// ```rust -/// use serenity::model::prelude::*; -/// use serenity::utils::parse_message_url; -/// -/// assert_eq!( -/// parse_message_url( -/// "https://discord.com/channels/381880193251409931/381880193700069377/806164913558781963" -/// ), -/// Some(( -/// GuildId::new(381880193251409931), -/// GenericChannelId::new(381880193700069377), -/// MessageId::new(806164913558781963), -/// )), -/// ); -/// assert_eq!( -/// parse_message_url( -/// "https://canary.discord.com/channels/381880193251409931/381880193700069377/806164913558781963" -/// ), -/// Some(( -/// GuildId::new(381880193251409931), -/// GenericChannelId::new(381880193700069377), -/// MessageId::new(806164913558781963), -/// )), -/// ); -/// assert_eq!(parse_message_url("https://google.com"), None); -/// ``` -#[must_use] -pub fn parse_message_url(s: &str) -> Option<(GuildId, GenericChannelId, MessageId)> { - use aformat::{CapStr, aformat}; - - for domain in DOMAINS { - let prefix = aformat!("https://{}/channels/", CapStr::(domain)); - if let Some(parts) = s.strip_prefix(prefix.as_str()) { - let mut parts = parts.splitn(3, '/'); - - let guild_id = parts.next()?.parse().ok()?; - let channel_id = parts.next()?.parse().ok()?; - let message_id = parts.next()?.parse().ok()?; - return Some((guild_id, channel_id, message_id)); - } - } - None -} - -/// Retrieves guild, and channel ID from a channel URL. -/// -/// If the URL is malformed, None is returned. -/// -/// # Examples -/// ```rust -/// use serenity::model::prelude::*; -/// use serenity::utils::parse_channel_url; -/// -/// assert_eq!( -/// parse_channel_url("https://discord.com/channels/381880193251409931/381880193700069377"), -/// Some((GuildId::new(381880193251409931), GenericChannelId::new(381880193700069377),)), -/// ); -/// assert_eq!( -/// parse_channel_url( -/// "https://canary.discord.com/channels/381880193251409931/381880193700069377" -/// ), -/// Some((GuildId::new(381880193251409931), GenericChannelId::new(381880193700069377),)), -/// ); -/// assert_eq!(parse_channel_url("https://google.com"), None); -/// ``` -#[must_use] -pub fn parse_channel_url(s: &str) -> Option<(GuildId, GenericChannelId)> { - use aformat::{CapStr, aformat}; - - for domain in DOMAINS { - let prefix = aformat!("https://{}/channels/", CapStr::(domain)); - if let Some(parts) = s.strip_prefix(prefix.as_str()) { - let mut parts = parts.splitn(2, '/'); - - let guild_id = parts.next()?.parse().ok()?; - let channel_id = parts.next()?.parse().ok()?; - return Some((guild_id, channel_id)); - } - } - None -} - -const MAX_DOMAIN_LEN: usize = { - let mut max_len = 0; - let mut i = 0; - - while i < DOMAINS.len() { - let cur_len = DOMAINS[i].len(); - if cur_len > max_len { - max_len = cur_len; - } - - i += 1; - } - - max_len -}; diff --git a/src/utils/argument_convert/role.rs b/src/utils/argument_convert/role.rs deleted file mode 100644 index 294059fe440..00000000000 --- a/src/utils/argument_convert/role.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Role::convert`]. -#[non_exhaustive] -#[derive(Debug)] -pub enum RoleParseError { - /// When the operation was invoked outside a guild. - NotInGuild, - /// When the guild's roles were not found in cache. - NotInCache, - /// HTTP error while retrieving guild roles. - Http(SerenityError), - /// The provided channel string failed to parse, or the parsed result cannot be found in the - /// cache. - NotFoundOrMalformed, -} - -impl std::error::Error for RoleParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Http(e) => Some(e), - Self::NotFoundOrMalformed | Self::NotInGuild | Self::NotInCache => None, - } - } -} - -impl fmt::Display for RoleParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotInGuild => f.write_str("Must invoke this operation in a guild"), - Self::NotInCache => f.write_str("Guild's roles were not found in cache"), - Self::Http(_) => f.write_str("Failed to retrieve roles via HTTP"), - Self::NotFoundOrMalformed => f.write_str("Role not found or unknown format"), - } - } -} - -/// Look up a [`Role`] by a string case-insensitively. -/// -/// Requires the cache feature to be enabled. -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by ID -/// 2. [Lookup by mention](`crate::utils::parse_role_mention`). -/// 3. Lookup by name (case-insensitive) -#[async_trait::async_trait] -impl ArgumentConvert for Role { - type Err = RoleParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - let guild_id = guild_id.ok_or(RoleParseError::NotInGuild)?; - - #[cfg(feature = "cache")] - let guild; - - #[cfg(feature = "cache")] - let roles = { - let cache = ctx.cache().ok_or(RoleParseError::NotInCache)?; - guild = cache.guild(guild_id).ok_or(RoleParseError::NotInCache)?; - &guild.roles - }; - - #[cfg(not(feature = "cache"))] - let roles = ctx.http().get_guild_roles(guild_id).await.map_err(RoleParseError::Http)?; - - if let Some(role_id) = s.parse().ok().or_else(|| crate::utils::parse_role_mention(s)) { - #[cfg(feature = "cache")] - if let Some(role) = roles.get(&role_id) { - return Ok(role.clone()); - } - #[cfg(not(feature = "cache"))] - if let Some(role) = roles.iter().find(|role| role.id == role_id) { - return Ok(role.clone()); - } - } - - #[cfg(feature = "cache")] - if let Some(role) = roles.iter().find(|role| role.name.eq_ignore_ascii_case(s)) { - return Ok(role.clone()); - } - #[cfg(not(feature = "cache"))] - if let Some(role) = roles.into_iter().find(|role| role.name.eq_ignore_ascii_case(s)) { - return Ok(role); - } - - Err(RoleParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/argument_convert/user.rs b/src/utils/argument_convert/user.rs deleted file mode 100644 index df7e4141370..00000000000 --- a/src/utils/argument_convert/user.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`User::convert`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum UserParseError { - /// The provided user string failed to parse, or the parsed result cannot be found in the guild - /// cache data. - NotFoundOrMalformed, -} - -impl std::error::Error for UserParseError {} - -impl fmt::Display for UserParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotFoundOrMalformed => f.write_str("User not found or unknown format"), - } - } -} - -/// Look up a user by a string case-insensitively. -/// -/// Requires the cache feature to be enabled. If a user is not in cache, they will not be found! -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by ID. -/// 2. [Lookup by mention](`crate::utils::parse_user_mention`). -/// 3. [Lookup by name#discrim](`crate::utils::parse_user_tag`). -/// 4. Lookup by name -#[async_trait::async_trait] -impl ArgumentConvert for User { - type Err = UserParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - channel_id: Option, - s: &str, - ) -> Result { - // Convert as a Member which uses HTTP endpoints instead of cache - if let Ok(member) = Member::convert(&ctx, guild_id, channel_id, s).await { - return Ok(member.user); - } - - // If string is a raw user ID or a mention - if let Some(user_id) = s.parse().ok().or_else(|| crate::utils::parse_user_mention(s)) { - // Now, we can still try UserId::to_user because it works for all users from all guilds - // the bot is joined - if let Ok(user) = user_id.to_user(&ctx).await { - return Ok(user); - } - } - - Err(UserParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 7576e6cf5a6..564fdfd74e4 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,8 +1,6 @@ //! A set of utilities to help with common use cases that are not required to fully use the //! library. -#[cfg(feature = "gateway")] -mod argument_convert; #[cfg(feature = "cache")] mod content_safe; mod custom_message; @@ -11,8 +9,6 @@ mod message_builder; use std::num::NonZeroU16; -#[cfg(feature = "gateway")] -pub use argument_convert::*; #[cfg(feature = "cache")] pub use content_safe::*; pub use formatted_timestamp::*; @@ -367,6 +363,22 @@ const DOMAINS: [&str; 6] = [ "ptb.discordapp.com", ]; +const MAX_DOMAIN_LEN: usize = { + let mut max_len = 0; + let mut i = 0; + + while i < DOMAINS.len() { + let cur_len = DOMAINS[i].len(); + if cur_len > max_len { + max_len = cur_len; + } + + i += 1; + } + + max_len +}; + /// Parses the id and token from a webhook url. Expects a [`url::Url`] rather than a [`&str`]. /// /// # Examples @@ -394,6 +406,119 @@ pub fn parse_webhook(url: &Url) -> Option<(WebhookId, &str)> { Some((webhook_id.parse().ok()?, token)) } +/// Retrieves IDs from "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID"). +/// +/// If the string is invalid, None is returned. +/// +/// # Examples +/// ```rust +/// use serenity::model::prelude::*; +/// use serenity::utils::parse_message_id_pair; +/// +/// assert_eq!( +/// parse_message_id_pair("673965002805477386-842482646604972082"), +/// Some((GenericChannelId::new(673965002805477386), MessageId::new(842482646604972082))), +/// ); +/// assert_eq!( +/// parse_message_id_pair("673965002805477386-842482646604972082-472029906943868929"), +/// None, +/// ); +/// ``` +#[must_use] +pub fn parse_message_id_pair(s: &str) -> Option<(GenericChannelId, MessageId)> { + let mut parts = s.splitn(2, '-'); + let channel_id = parts.next()?.parse().ok()?; + let message_id = parts.next()?.parse().ok()?; + Some((channel_id, message_id)) +} + +/// Retrieves guild, channel, and message ID from a message URL. +/// +/// If the URL is malformed, None is returned. +/// +/// # Examples +/// ```rust +/// use serenity::model::prelude::*; +/// use serenity::utils::parse_message_url; +/// +/// assert_eq!( +/// parse_message_url( +/// "https://discord.com/channels/381880193251409931/381880193700069377/806164913558781963" +/// ), +/// Some(( +/// GuildId::new(381880193251409931), +/// GenericChannelId::new(381880193700069377), +/// MessageId::new(806164913558781963), +/// )), +/// ); +/// assert_eq!( +/// parse_message_url( +/// "https://canary.discord.com/channels/381880193251409931/381880193700069377/806164913558781963" +/// ), +/// Some(( +/// GuildId::new(381880193251409931), +/// GenericChannelId::new(381880193700069377), +/// MessageId::new(806164913558781963), +/// )), +/// ); +/// assert_eq!(parse_message_url("https://google.com"), None); +/// ``` +#[must_use] +pub fn parse_message_url(s: &str) -> Option<(GuildId, GenericChannelId, MessageId)> { + use aformat::{CapStr, aformat}; + + for domain in DOMAINS { + let prefix = aformat!("https://{}/channels/", CapStr::(domain)); + if let Some(parts) = s.strip_prefix(prefix.as_str()) { + let mut parts = parts.splitn(3, '/'); + + let guild_id = parts.next()?.parse().ok()?; + let channel_id = parts.next()?.parse().ok()?; + let message_id = parts.next()?.parse().ok()?; + return Some((guild_id, channel_id, message_id)); + } + } + None +} + +/// Retrieves guild, and channel ID from a channel URL. +/// +/// If the URL is malformed, None is returned. +/// +/// # Examples +/// ```rust +/// use serenity::model::prelude::*; +/// use serenity::utils::parse_channel_url; +/// +/// assert_eq!( +/// parse_channel_url("https://discord.com/channels/381880193251409931/381880193700069377"), +/// Some((GuildId::new(381880193251409931), GenericChannelId::new(381880193700069377),)), +/// ); +/// assert_eq!( +/// parse_channel_url( +/// "https://canary.discord.com/channels/381880193251409931/381880193700069377" +/// ), +/// Some((GuildId::new(381880193251409931), GenericChannelId::new(381880193700069377),)), +/// ); +/// assert_eq!(parse_channel_url("https://google.com"), None); +/// ``` +#[must_use] +pub fn parse_channel_url(s: &str) -> Option<(GuildId, GenericChannelId)> { + use aformat::{CapStr, aformat}; + + for domain in DOMAINS { + let prefix = aformat!("https://{}/channels/", CapStr::(domain)); + if let Some(parts) = s.strip_prefix(prefix.as_str()) { + let mut parts = parts.splitn(2, '/'); + + let guild_id = parts.next()?.parse().ok()?; + let channel_id = parts.next()?.parse().ok()?; + return Some((guild_id, channel_id)); + } + } + None +} + /// Calculates the Id of the shard responsible for a guild, given its Id and total number of shards /// used. ///