mod colour;
mod message_builder;
mod custom_message;
pub use self::{
colour::Colour,
message_builder::{
Content,
ContentModifier,
EmbedMessageBuilding,
MessageBuilder,
},
custom_message::CustomMessage
};
pub type Color = Colour;
use crate::internal::prelude::*;
use crate::model::{
misc::EmojiIdentifier,
id::EmojiId,
};
#[cfg(feature = "cache")]
use crate::model::id::{
ChannelId,
GuildId,
RoleId,
UserId,
};
use std::{
collections::HashMap,
ffi::OsStr,
fs::File,
hash::{BuildHasher, Hash},
io::Read,
path::Path,
};
#[cfg(feature = "cache")]
use crate::model::channel::Channel;
#[cfg(feature = "cache")]
use std::str::FromStr;
#[cfg(feature = "cache")]
use crate::cache::Cache;
pub fn hashmap_to_json_map<H, T>(map: HashMap<T, Value, H>)
-> Map<String, Value> where H: BuildHasher, T: Eq + Hash + ToString {
let mut json_map = Map::new();
for (key, value) in map {
json_map.insert(key.to_string(), value);
}
json_map
}
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
}
}
pub fn parse_username(mention: impl AsRef<str>) -> Option<u64> {
let mention = mention.as_ref();
if mention.len() < 4 {
return None;
}
if mention.starts_with("<@!") {
let len = mention.len() - 1;
mention[3..len].parse::<u64>().ok()
} else if mention.starts_with("<@") {
let len = mention.len() - 1;
mention[2..len].parse::<u64>().ok()
} else {
None
}
}
pub fn parse_role(mention: impl AsRef<str>) -> Option<u64> {
let mention = mention.as_ref();
if mention.len() < 4 {
return None;
}
if mention.starts_with("<@&") && mention.ends_with('>') {
let len = mention.len() - 1;
mention[3..len].parse::<u64>().ok()
} else {
None
}
}
pub fn parse_channel(mention: impl AsRef<str>) -> Option<u64> {
let mention = mention.as_ref();
if mention.len() < 4 {
return None;
}
if mention.starts_with("<#") && mention.ends_with('>') {
let len = mention.len() - 1;
mention[2..len].parse::<u64>().ok()
} else {
None
}
}
pub fn parse_mention(mention: impl AsRef<str>) -> Option<u64> {
let mention = mention.as_ref();
if mention.starts_with("<@&") {
parse_role(mention)
} else if mention.starts_with("<@") || mention.starts_with("<@!") {
parse_username(mention)
} else if mention.starts_with("<#") {
parse_channel(mention)
} else {
None
}
}
pub fn parse_emoji(mention: impl AsRef<str>) -> Option<EmojiIdentifier> {
let mention = mention.as_ref();
let len = mention.len();
if len < 6 || len > 56 {
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;
} else {
id.push(y);
}
}
break;
} else {
name.push(x);
}
}
match id.parse::<u64>() {
Ok(x) => Some(EmojiIdentifier {
animated,
name,
id: EmojiId(x),
}),
_ => None,
}
} else {
None
}
}
#[inline]
pub fn read_image<P: AsRef<Path>>(path: P) -> Result<String> {
_read_image(path.as_ref())
}
fn _read_image(path: &Path) -> Result<String> {
let mut v = Vec::default();
let mut f = File::open(path)?;
let _ = f.read_to_end(&mut v);
let b64 = base64::encode(&v);
let ext = if path.extension() == Some(OsStr::new("png")) {
"png"
} else {
"jpg"
};
Ok(format!("data:image/{};base64,{}", ext, b64))
}
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
}
#[inline]
pub fn shard_id(guild_id: impl Into<u64>, shard_count: u64) -> u64 { (guild_id.into() >> 22) % shard_count }
#[cfg(feature = "cache")]
#[derive(Clone, Debug)]
pub struct ContentSafeOptions {
clean_role: bool,
clean_user: bool,
clean_channel: bool,
clean_here: bool,
clean_everyone: bool,
show_discriminator: bool,
guild_reference: Option<GuildId>,
}
#[cfg(feature = "cache")]
impl ContentSafeOptions {
pub fn new() -> Self {
ContentSafeOptions::default()
}
pub fn clean_role(mut self, b: bool) -> Self {
self.clean_role = b;
self
}
pub fn clean_user(mut self, b: bool) -> Self {
self.clean_user = b;
self
}
pub fn clean_channel(mut self, b: bool) -> Self {
self.clean_channel = b;
self
}
pub fn show_discriminator(mut self, b: bool) -> Self {
self.show_discriminator = b;
self
}
pub fn display_as_member_from<G: Into<GuildId>>(mut self, guild: G) -> Self {
self.guild_reference = Some(guild.into());
self
}
pub fn clean_here(mut self, b: bool) -> Self {
self.clean_here = b;
self
}
pub fn clean_everyone(mut self, b: bool) -> Self {
self.clean_everyone = b;
self
}
}
#[cfg(feature = "cache")]
impl Default for ContentSafeOptions {
fn default() -> Self {
ContentSafeOptions {
clean_role: true,
clean_user: true,
clean_channel: true,
clean_here: true,
clean_everyone: true,
show_discriminator: true,
guild_reference: None,
}
}
}
#[cfg(feature = "cache")]
#[inline]
async fn clean_roles(cache: impl AsRef<Cache>, s: &mut String) {
let mut progress = 0;
while let Some(mut mention_start) = s[progress..].find("<@&") {
mention_start += progress;
if let Some(mut mention_end) = s[mention_start..].find('>') {
mention_end += mention_start;
mention_start += "<@&".len();
if let Ok(id) = RoleId::from_str(&s[mention_start..mention_end]) {
let to_replace = format!("<@&{}>", &s[mention_start..mention_end]);
*s = if let Some(role) = id.to_role_cached(&cache).await {
s.replace(&to_replace, &format!("@{}", &role.name))
} else {
s.replace(&to_replace, &"@deleted-role")
};
} else {
let id = &s[mention_start..mention_end].to_string();
if !id.is_empty() && id.as_bytes().iter().all(u8::is_ascii_digit){
let to_replace = format!("<@&{}>", id);
*s = s.replace(&to_replace, &"@deleted-role");
} else {
progress = mention_end;
}
}
} else {
break;
}
}
}
#[cfg(feature = "cache")]
#[inline]
async fn clean_channels(cache: &impl AsRef<Cache>, s: &mut String) {
let mut progress = 0;
while let Some(mut mention_start) = s[progress..].find("<#") {
mention_start += progress;
if let Some(mut mention_end) = s[mention_start..].find('>') {
mention_end += mention_start;
mention_start += "<#".len();
if let Ok(id) = ChannelId::from_str(&s[mention_start..mention_end]) {
let to_replace = format!("<#{}>", &s[mention_start..mention_end]);
*s = if let Some(Channel::Guild(channel)) = id.to_channel_cached(&cache).await {
let replacement = format!("#{}", &channel.name);
s.replace(&to_replace, &replacement)
} else {
s.replace(&to_replace, &"#deleted-channel")
};
} else {
let id = &s[mention_start..mention_end].to_string();
if !id.is_empty() && id.as_bytes().iter().all(u8::is_ascii_digit) {
let to_replace = format!("<#{}>", id);
*s = s.replace(&to_replace, &"#deleted-channel");
} else {
progress = mention_end;
}
}
} else {
break;
}
}
}
#[cfg(feature = "cache")]
#[inline]
async fn clean_users(
cache: &impl AsRef<Cache>,
s: &mut String,
show_discriminator: bool,
guild: Option<GuildId>
) {
let cache = cache.as_ref();
let mut progress = 0;
while let Some(mut mention_start) = s[progress..].find("<@") {
mention_start += progress;
if let Some(mut mention_end) = s[mention_start..].find('>') {
mention_end += mention_start;
mention_start += "<@".len();
let has_exclamation = if s[mention_start..].as_bytes().get(0).map_or(false, |c| *c == b'!') {
mention_start += "!".len();
true
} else {
false
};
if let Ok(id) = UserId::from_str(&s[mention_start..mention_end]) {
let replacement = if let Some(guild_id) = guild {
if let Some(guild) = cache.guild(&guild_id).await {
if let Some(member) = guild.members.get(&id) {
if show_discriminator {
format!("@{}", member.distinct())
} else {
format!("@{}", member.display_name())
}
} else {
"@invalid-user".to_string()
}
} else {
"@invalid-user".to_string()
}
} else if let Some(user) = cache.user(id).await {
if show_discriminator {
format!("@{}#{:04}", user.name, user.discriminator)
} else {
format!("@{}", user.name)
}
} else {
"@invalid-user".to_string()
};
let code_start = if has_exclamation { "<@!" } else { "<@" };
let to_replace = format!("{}{}>", code_start, &s[mention_start..mention_end]);
*s = s.replace(&to_replace, &replacement)
} else {
let id = &s[mention_start..mention_end].to_string();
if !id.is_empty() && id.as_bytes().iter().all(u8::is_ascii_digit) {
let code_start = if has_exclamation { "<@!" } else { "<@" };
let to_replace = format!("{}{}>", code_start, id);
*s = s.replace(&to_replace, &"@invalid-user");
} else {
progress = mention_end;
}
}
} else {
break;
}
}
}
#[cfg(feature = "cache")]
pub async fn content_safe(cache: impl AsRef<Cache>, s: impl AsRef<str>, options: &ContentSafeOptions) -> String {
let mut content = s.as_ref().to_string();
if options.clean_role {
clean_roles(&cache, &mut content).await;
}
if options.clean_channel {
clean_channels(&cache, &mut content).await;
}
if options.clean_user {
clean_users(&cache, &mut content, options.show_discriminator, options.guild_reference).await;
}
if options.clean_here {
content = content.replace("@here", "@\u{200B}here");
}
if options.clean_everyone {
content = content.replace("@everyone", "@\u{200B}everyone");
}
content
}
#[cfg(test)]
mod test {
#[cfg(feature = "cache")]
use crate::cache::Cache;
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_username("<@12345>").unwrap(), 12_345);
assert_eq!(parse_username("<@!12345>").unwrap(), 12_345);
}
#[test]
fn role_parser() {
assert_eq!(parse_role("<@&12345>").unwrap(), 12_345);
}
#[test]
fn test_channel_parser() {
assert_eq!(parse_channel("<#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"]);
}
#[cfg(feature = "cache")]
#[tokio::test]
async fn test_content_safe() {
use crate::model::{
user::User,
Permissions,
prelude::*,
};
use chrono::{DateTime, Utc};
use std::{
collections::HashMap,
sync::Arc,
};
let user = User {
id: UserId(100000000000000000),
avatar: None,
bot: false,
discriminator: 0000,
name: "Crab".to_string(),
_nonexhaustive: (),
};
let mut guild = Guild {
afk_channel_id: None,
afk_timeout: 0,
application_id: None,
channels: HashMap::new(),
default_message_notifications: DefaultMessageNotificationLevel::All,
emojis: HashMap::new(),
explicit_content_filter: ExplicitContentFilter::None,
features: Vec::new(),
icon: None,
id: GuildId(381880193251409931),
joined_at: DateTime::parse_from_str(
"1983 Apr 13 12:09:14.274 +0000",
"%Y %b %d %H:%M:%S%.3f %z").unwrap()
.with_timezone(&Utc),
large: false,
member_count: 1,
members: HashMap::new(),
mfa_level: MfaLevel::None,
name: "serenity".to_string(),
owner_id: UserId(114941315417899012),
presences: HashMap::new(),
region: "Ferris Island".to_string(),
roles: HashMap::new(),
splash: None,
system_channel_id: None,
verification_level: VerificationLevel::None,
voice_states: HashMap::new(),
description: None,
premium_tier: PremiumTier::Tier0,
premium_subscription_count: 0,
banner: None,
vanity_url_code: Some("bruhmoment1".to_string()),
preferred_locale: "en-US".to_string(),
_nonexhaustive: (),
};
let member = Member {
deaf: false,
guild_id: guild.id,
joined_at: None,
mute: false,
nick: Some("Ferris".to_string()),
roles: Vec::new(),
user: user.clone(),
_nonexhaustive: (),
};
let role = Role {
id: RoleId(333333333333333333),
colour: Colour::ORANGE,
guild_id: guild.id,
hoist: true,
managed: false,
mentionable: true,
name: "ferris-club-member".to_string(),
permissions: Permissions::all(),
position: 0,
_nonexhaustive: (),
};
let channel = GuildChannel {
id: ChannelId(111880193700067777),
bitrate: None,
category_id: None,
guild_id: guild.id,
kind: ChannelType::Text,
last_message_id: None,
last_pin_timestamp: None,
name: "general".to_string(),
permission_overwrites: Vec::new(),
position: 0,
topic: None,
user_limit: None,
nsfw: false,
slow_mode_rate: Some(0),
_nonexhaustive: (),
};
let cache = Arc::new(Cache::default());
guild.members.insert(user.id, member.clone());
guild.roles.insert(role.id, role.clone());
cache.users.write().await.insert(user.id, user.clone());
cache.guilds.write().await.insert(guild.id, guild.clone());
cache.channels.write().await.insert(channel.id, channel.clone());
let with_user_metions = "<@!100000000000000000> <@!000000000000000000> <@123> <@!123> \
<@!123123123123123123123> <@123> <@123123123123123123> <@!invalid> \
<@invalid> <@日本語 한국어$§)[/__#\\(/&2032$§#> \
<@!i)/==(<<>z/9080)> <@!1231invalid> <@invalid123> \
<@123invalid> <@> <@ ";
let without_user_mentions = "@Crab#0000 @invalid-user @invalid-user @invalid-user \
@invalid-user @invalid-user @invalid-user <@!invalid> \
<@invalid> <@日本語 한국어$§)[/__#\\(/&2032$§#> \
<@!i)/==(<<>z/9080)> <@!1231invalid> <@invalid123> \
<@123invalid> <@> <@ ";
let options = ContentSafeOptions::default();
assert_eq!(without_user_mentions, content_safe(&cache, with_user_metions, &options).await);
let options = ContentSafeOptions::default();
assert_eq!(format!("@{}#{:04}", user.name, user.discriminator),
content_safe(&cache, "<@!100000000000000000>", &options).await);
let options = ContentSafeOptions::default();
assert_eq!(format!("@{}#{:04}", user.name, user.discriminator),
content_safe(&cache, "<@100000000000000000>", &options).await);
let options = options.show_discriminator(false);
assert_eq!(format!("@{}", user.name),
content_safe(&cache, "<@!100000000000000000>", &options).await);
let options = options.show_discriminator(false);
assert_eq!(format!("@{}", user.name),
content_safe(&cache, "<@100000000000000000>", &options).await);
let options = options.display_as_member_from(guild.id);
assert_eq!(format!("@{}", member.nick.unwrap()),
content_safe(&cache, "<@!100000000000000000>", &options).await);
let options = options.clean_user(false);
assert_eq!(with_user_metions,
content_safe(&cache, with_user_metions, &options).await);
let with_channel_mentions = "<#> <#deleted-channel> #deleted-channel <#0> \
#unsafe-club <#111880193700067777> <#ferrisferrisferris> \
<#000000000000000000>";
let without_channel_mentions = "<#> <#deleted-channel> #deleted-channel \
#deleted-channel #unsafe-club #general <#ferrisferrisferris> \
#deleted-channel";
assert_eq!(without_channel_mentions,
content_safe(&cache, with_channel_mentions, &options).await);
let options = options.clean_channel(false);
assert_eq!(with_channel_mentions,
content_safe(&cache, with_channel_mentions, &options).await);
let with_role_mentions = "<@&> @deleted-role <@&9829> \
<@&333333333333333333> <@&000000000000000000>";
let without_role_mentions = "<@&> @deleted-role @deleted-role \
@ferris-club-member @deleted-role";
assert_eq!(without_role_mentions,
content_safe(&cache, with_role_mentions, &options).await);
let options = options.clean_role(false);
assert_eq!(with_role_mentions,
content_safe(&cache, with_role_mentions, &options).await);
let with_everyone_mention = "@everyone";
let without_everyone_mention = "@\u{200B}everyone";
assert_eq!(without_everyone_mention,
content_safe(&cache, with_everyone_mention, &options).await);
let options = options.clean_everyone(false);
assert_eq!(with_everyone_mention,
content_safe(&cache, with_everyone_mention, &options).await);
let with_here_mention = "@here";
let without_here_mention = "@\u{200B}here";
assert_eq!(without_here_mention,
content_safe(&cache, with_here_mention, &options).await);
let options = options.clean_here(false);
assert_eq!(with_here_mention,
content_safe(&cache, with_here_mention, &options).await);
}
}