From 626ffb25af35f5b91a76fdccf6788382a1c39455 Mon Sep 17 00:00:00 2001 From: Illia Date: Wed, 7 Dec 2016 20:09:04 +0200 Subject: [PATCH] Allow mentionable structs to be used as command arguments Add EmojiIdentifier, allow User, UserId, Role, RoleId, EmojiIdentifier, Channel and ChannelId to be used as arguments for commands and add more parsing functions to utils --- definitions/structs/emoji_identifier.yml | 11 ++++ src/ext/cache/mod.rs | 14 ++++ src/model/id.rs | 8 +++ src/model/misc.rs | 82 ++++++++++++++++++++++- src/utils/mod.rs | 83 ++++++++++++++++++++++++ tests/test_parsers.rs | 33 ++++++++++ 6 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 definitions/structs/emoji_identifier.yml create mode 100644 tests/test_parsers.rs diff --git a/definitions/structs/emoji_identifier.yml b/definitions/structs/emoji_identifier.yml new file mode 100644 index 00000000000..01d14dc3e45 --- /dev/null +++ b/definitions/structs/emoji_identifier.yml @@ -0,0 +1,11 @@ +--- +name: EmojiIdentifier +description: Version of emoji struct used only when Id and name are known. +fields: + - name: id + description: "The Id of the emoji." + type: EmojiId + - name: name + description: "The name of the emoji. It must be at least 2 characters + long and can only contain alphanumeric characters and underscores." + type: string diff --git a/src/ext/cache/mod.rs b/src/ext/cache/mod.rs index 39baaf05adc..83ff46f0fd4 100644 --- a/src/ext/cache/mod.rs +++ b/src/ext/cache/mod.rs @@ -411,6 +411,20 @@ impl Cache { }) } + /// Retrieves a reference to a `User` based on appearance in + /// the first server they are in. + pub fn get_user(&self, user_id: U) -> Option<&User> + where U: Into + Clone { + for v in self.guilds.values() { + match v.members.get(&user_id.clone().into()) { + Some(x) => { return Some(&x.user) } + None => {} + } + } + + None + } + /// Retrieves a reference to a [`Guild`]'s role by their Ids. /// /// [`Guild`]: ../../model/struct.Guild.html diff --git a/src/model/id.rs b/src/model/id.rs index 032e7222b65..cbbd796732e 100644 --- a/src/model/id.rs +++ b/src/model/id.rs @@ -172,6 +172,14 @@ impl RoleId { } } +impl UserId { + /// Search the cache for the channel with the Id. + #[cfg(all(feature = "cache", feature = "methods"))] + pub fn find(&self) -> Option { + CACHE.read().unwrap().get_user(*self).map(|x| x.clone()) + } +} + impl From for UserId { /// Gets the Id of a `CurrentUser` struct. fn from(current_user: CurrentUser) -> UserId { diff --git a/src/model/misc.rs b/src/model/misc.rs index 6e208bf25d4..e078bac03df 100644 --- a/src/model/misc.rs +++ b/src/model/misc.rs @@ -7,9 +7,13 @@ use super::{ Role, UserId, User, - IncidentStatus + IncidentStatus, + EmojiIdentifier }; use ::internal::prelude::*; +use std::str::FromStr; +use std::result::Result as StdResult; +use ::utils; /// Allows something - such as a channel or role - to be mentioned in a message. pub trait Mentionable { @@ -74,6 +78,82 @@ impl Mentionable for User { } } +#[cfg(feature = "cache")] +impl FromStr for User { + type Err = (); + fn from_str(s: &str) -> StdResult { + match utils::parse_username(s) { + Some(x) => { + match UserId(x as u64).find() { + Some(user) => Ok(user), + _ => Err(()) + } + }, + _ => Err(()) + } + } +} + +impl FromStr for UserId { + type Err = (); + fn from_str(s: &str) -> StdResult { + utils::parse_username(s).ok_or_else(|| ()).map(|x| UserId(x)) + } +} + +#[cfg(feature = "cache")] +impl FromStr for Role { + type Err = (); + fn from_str(s: &str) -> StdResult { + match utils::parse_role(s) { + Some(x) => { + match RoleId(x).find() { + Some(user) => Ok(user), + _ => Err(()) + } + }, + _ => Err(()) + } + } +} + +impl FromStr for RoleId { + type Err = (); + fn from_str(s: &str) -> StdResult { + utils::parse_role(s).ok_or_else(|| ()).map(|x| RoleId(x)) + } +} + +impl FromStr for EmojiIdentifier { + type Err = (); + fn from_str(s: &str) -> StdResult { + utils::parse_emoji(s).ok_or_else(|| ()) + } +} + +impl FromStr for ChannelId { + type Err = (); + fn from_str(s: &str) -> StdResult { + utils::parse_channel(s).ok_or_else(|| ()).map(|x| ChannelId(x)) + } +} + +#[cfg(feature = "cache")] +impl FromStr for Channel { + type Err = (); + fn from_str(s: &str) -> StdResult { + match utils::parse_channel(s) { + Some(x) => { + match ChannelId(x).find() { + Some(channel) => Ok(channel), + _ => Err(()) + } + }, + _ => Err(()) + } + } +} + impl IncidentStatus { #[doc(hidden)] pub fn decode(value: Value) -> Result { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index aa97f4ab6e0..2401a1b85cc 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -18,6 +18,7 @@ use std::fs::File; use std::io::Read; use std::path::Path; use ::internal::prelude::*; +use ::model::{EmojiIdentifier, EmojiId}; pub use self::message_builder::MessageBuilder; @@ -83,6 +84,88 @@ pub fn parse_invite(code: &str) -> &str { } } +/// Retreives Id from a username mention. +pub fn parse_username(mention: &str) -> Option { + if mention.len() < 4 { + return None; + } + + if mention.starts_with("<@!") { + let len = mention.len() - 1; + mention[3..len].parse::().ok() + } else if mention.starts_with("<@") { + let len = mention.len() - 1; + mention[2..len].parse::().ok() + } else { + None + } +} + +/// Retreives Id from a role mention. +pub fn parse_role(mention: &str) -> Option { + if mention.len() < 4 { + return None; + } + + if mention.starts_with("<@&") { + let len = mention.len() - 1; + mention[3..len].parse::().ok() + } else { + None + } +} + +/// Retreives Id from a channel mention. +pub fn parse_channel(mention: &str) -> Option { + if mention.len() < 4 { + return None; + } + + if mention.starts_with("<#") { + let len = mention.len() - 1; + mention[2..len].parse::().ok() + } else { + None + } +} + +/// Retreives name and Id from an emoji mention. +pub fn parse_emoji(mention: &str) -> Option { + let len = mention.len(); + if len < 6 || len > 56 { + return None; + } + + if mention.starts_with("<:") { + let mut name = String::default(); + let mut id = String::default(); + for (i, x) in mention[2..].chars().enumerate() { + if x == ':' { + let from = i + 3; + for y in mention[from..].chars() { + if y == '>' { + break; + } else { + id.push(y); + } + } + break; + } else { + name.push(x); + } + } + match id.parse::() { + Ok(x) => Some(EmojiIdentifier { + name: name, + id: EmojiId(x) + }), + _ => None + } + } else { + None + } +} + /// Reads an image from a path and encodes it into base64. /// /// This can be used for methods like [`EditProfile::avatar`]. diff --git a/tests/test_parsers.rs b/tests/test_parsers.rs new file mode 100644 index 00000000000..479ac93df6c --- /dev/null +++ b/tests/test_parsers.rs @@ -0,0 +1,33 @@ +extern crate serenity; + +use serenity::utils::*; + +#[test] +fn 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"); +} + +#[test] +fn username_parser() { + assert_eq!(parse_username("<@12345>").unwrap(), 12345); + assert_eq!(parse_username("<@!12345>").unwrap(), 12345); +} + +#[test] +fn role_parser() { + assert_eq!(parse_role("<@&12345>").unwrap(), 12345); +} + +#[test] +fn channel_parser() { + assert_eq!(parse_channel("<#12345>").unwrap(), 12345); +} + +#[test] +fn emoji_parser() { + let emoji = parse_emoji("<:name:12345>").unwrap(); + assert_eq!(emoji.name, "name"); + assert_eq!(emoji.id, 12345); +}