#[cfg(feature = "client")]
mod argument_convert;
#[cfg(feature = "cache")]
mod content_safe;
mod custom_message;
mod formatted_timestamp;
mod message_builder;
#[cfg(feature = "collector")]
mod quick_modal;
pub mod token;
use std::num::NonZeroU16;
#[cfg(feature = "client")]
pub use argument_convert::*;
#[cfg(feature = "cache")]
pub use content_safe::*;
pub use formatted_timestamp::*;
#[cfg(feature = "collector")]
pub use quick_modal::*;
use url::Url;
pub use self::custom_message::CustomMessage;
pub use self::message_builder::{Content, ContentModifier, EmbedMessageBuilding, MessageBuilder};
#[doc(inline)]
pub use self::token::validate as validate_token;
#[cfg(all(feature = "cache", feature = "model"))]
use crate::cache::Cache;
#[cfg(all(feature = "cache", feature = "model"))]
use crate::http::CacheHttp;
use crate::model::prelude::*;
#[must_use]
pub fn parse_invite(code: &str) -> &str {
let code = code.trim_start_matches("http://").trim_start_matches("https://");
let lower = code.to_lowercase();
if lower.starts_with("discord.gg/") {
&code[11..]
} else if lower.starts_with("discord.com/invite/") {
&code[19..]
} else {
code
}
}
#[must_use]
pub fn parse_user_tag(s: &str) -> Option<(&str, Option<NonZeroU16>)> {
if let Some((name, discrim)) = s.split_once('#') {
let discrim: u16 = discrim.parse().ok()?;
if discrim > 9999 {
return None;
}
Some((name, NonZeroU16::new(discrim)))
} else {
Some((s, None))
}
}
#[must_use]
pub fn parse_user_mention(mention: &str) -> Option<UserId> {
if mention.len() < 4 {
return None;
}
let len = mention.len() - 1;
if mention.starts_with("<@!") {
mention[3..len].parse().ok()
} else if mention.starts_with("<@") {
mention[2..len].parse().ok()
} else {
None
}
}
#[deprecated = "use `utils::parse_user_mention` instead"]
pub fn parse_username(mention: impl AsRef<str>) -> Option<UserId> {
parse_user_mention(mention.as_ref())
}
#[must_use]
pub fn parse_role_mention(mention: &str) -> Option<RoleId> {
if mention.len() < 4 {
return None;
}
if mention.starts_with("<@&") && mention.ends_with('>') {
let len = mention.len() - 1;
mention[3..len].parse().ok()
} else {
None
}
}
#[deprecated = "use `utils::parse_role_mention` instead"]
pub fn parse_role(mention: impl AsRef<str>) -> Option<RoleId> {
parse_role_mention(mention.as_ref())
}
#[must_use]
pub fn parse_channel_mention(mention: &str) -> Option<ChannelId> {
if mention.len() < 4 {
return None;
}
if mention.starts_with("<#") && mention.ends_with('>') {
let len = mention.len() - 1;
mention[2..len].parse().ok()
} else {
None
}
}
#[deprecated = "use `utils::parse_channel_mention` instead"]
pub fn parse_channel(mention: impl AsRef<str>) -> Option<ChannelId> {
parse_channel_mention(mention.as_ref())
}
pub fn parse_emoji(mention: impl AsRef<str>) -> Option<EmojiIdentifier> {
let mention = mention.as_ref();
let len = mention.len();
if !(6..=56).contains(&len) {
return None;
}
if (mention.starts_with("<:") || mention.starts_with("<a:")) && mention.ends_with('>') {
let mut name = String::default();
let mut id = String::default();
let animated = &mention[1..3] == "a:";
let start = if animated { 3 } else { 2 };
for (i, x) in mention[start..].chars().enumerate() {
if x == ':' {
let from = i + start + 1;
for y in mention[from..].chars() {
if y == '>' {
break;
}
id.push(y);
}
break;
}
name.push(x);
}
id.parse().ok().map(|id| EmojiIdentifier {
animated,
id,
name,
})
} else {
None
}
}
pub fn parse_quotes(s: impl AsRef<str>) -> Vec<String> {
let s = s.as_ref();
let mut args = vec![];
let mut in_string = false;
let mut escaping = false;
let mut current_str = String::default();
for x in s.chars() {
if in_string {
if x == '\\' && !escaping {
escaping = true;
} else if x == '"' && !escaping {
if !current_str.is_empty() {
args.push(current_str);
}
current_str = String::default();
in_string = false;
} else {
current_str.push(x);
escaping = false;
}
} else if x == ' ' {
if !current_str.is_empty() {
args.push(current_str.clone());
}
current_str = String::default();
} else if x == '"' {
if !current_str.is_empty() {
args.push(current_str.clone());
}
in_string = true;
current_str = String::default();
} else {
current_str.push(x);
}
}
if !current_str.is_empty() {
args.push(current_str);
}
args
}
const DOMAINS: &[&str] = &[
"discord.com",
"canary.discord.com",
"ptb.discord.com",
"discordapp.com",
"canary.discordapp.com",
"ptb.discordapp.com",
];
#[must_use]
pub fn parse_webhook(url: &Url) -> Option<(WebhookId, &str)> {
let (webhook_id, token) = url.path().strip_prefix("/api/webhooks/")?.split_once('/')?;
if !["http", "https"].contains(&url.scheme())
|| !DOMAINS.contains(&url.domain()?)
|| !(17..=20).contains(&webhook_id.len())
|| !(60..=68).contains(&token.len())
{
return None;
}
Some((webhook_id.parse().ok()?, token))
}
#[cfg(all(feature = "cache", feature = "model"))]
pub(crate) fn user_has_guild_perms(
cache_http: impl CacheHttp,
guild_id: GuildId,
permissions: Permissions,
) -> Result<()> {
if let Some(cache) = cache_http.cache() {
if let Some(guild) = cache.guild(guild_id) {
guild.require_perms(cache, permissions)?;
}
}
Ok(())
}
#[cfg(all(feature = "cache", feature = "model"))]
#[inline]
pub(crate) fn user_has_perms_cache(
cache: impl AsRef<Cache>,
channel_id: ChannelId,
required_permissions: Permissions,
) -> Result<()> {
match user_perms(cache, channel_id) {
Ok(perms) => {
if perms.contains(required_permissions) {
Ok(())
} else {
Err(Error::Model(ModelError::InvalidPermissions {
required: required_permissions,
present: perms,
}))
}
},
Err(Error::Model(err)) if err.is_cache_err() => Ok(()),
Err(other) => Err(other),
}
}
#[cfg(all(feature = "cache", feature = "model"))]
pub(crate) fn user_perms(cache: impl AsRef<Cache>, channel_id: ChannelId) -> Result<Permissions> {
let cache = cache.as_ref();
let Some(guild_id) = cache.channels.get(&channel_id).map(|c| *c) else {
return Err(Error::Model(ModelError::ChannelNotFound));
};
let Some(guild) = cache.guild(guild_id) else {
return Err(Error::Model(ModelError::GuildNotFound));
};
let Some(channel) = guild.channels.get(&channel_id) else {
return Err(Error::Model(ModelError::ChannelNotFound));
};
let Some(member) = guild.members.get(&cache.current_user().id) else {
return Err(Error::Model(ModelError::MemberNotFound));
};
Ok(guild.user_permissions_in(channel, member))
}
#[inline]
#[must_use]
pub fn shard_id(guild_id: GuildId, shard_count: u32) -> u32 {
((guild_id.get() >> 22) % (shard_count as u64)) as u32
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_invite_parser() {
assert_eq!(parse_invite("https://discord.gg/abc"), "abc");
assert_eq!(parse_invite("http://discord.gg/abc"), "abc");
assert_eq!(parse_invite("discord.gg/abc"), "abc");
assert_eq!(parse_invite("DISCORD.GG/ABC"), "ABC");
assert_eq!(parse_invite("https://discord.com/invite/abc"), "abc");
assert_eq!(parse_invite("http://discord.com/invite/abc"), "abc");
assert_eq!(parse_invite("discord.com/invite/abc"), "abc");
}
#[test]
fn test_username_parser() {
assert_eq!(parse_user_mention("<@12345>").unwrap(), 12_345);
assert_eq!(parse_user_mention("<@!12345>").unwrap(), 12_345);
}
#[test]
fn role_parser() {
assert_eq!(parse_role_mention("<@&12345>").unwrap(), 12_345);
}
#[test]
fn test_channel_parser() {
assert_eq!(parse_channel_mention("<#12345>").unwrap(), 12_345);
}
#[test]
fn test_emoji_parser() {
let emoji = parse_emoji("<:name:12345>").unwrap();
assert_eq!(emoji.name, "name");
assert_eq!(emoji.id, 12_345);
}
#[test]
fn test_quote_parser() {
let parsed = parse_quotes("a \"b c\" d\"e f\" g");
assert_eq!(parsed, ["a", "b c", "d", "e f", "g"]);
}
#[test]
fn test_webhook_parser() {
for domain in DOMAINS {
let url = format!("https://{domain}/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV").parse().unwrap();
let (id, token) = parse_webhook(&url).unwrap();
assert_eq!(id, 245037420704169985);
assert_eq!(
token,
"ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"
);
}
}
}