aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMishio595 <[email protected]>2018-08-01 15:38:12 -0600
committerMishio595 <[email protected]>2018-08-01 15:38:12 -0600
commitc5fb7b4b331ef5a66179539b065913078e55b668 (patch)
tree99bc28270eaad9acf3da3871e72ba67dac5b87eb /src
parentMerge branch 'asref_messageid_for_message' (diff)
parentDon't delay Ready with cache enabled (diff)
downloadserenity-c5fb7b4b331ef5a66179539b065913078e55b668.tar.xz
serenity-c5fb7b4b331ef5a66179539b065913078e55b668.zip
Merge branch 'upstream'
Diffstat (limited to 'src')
-rw-r--r--src/builder/create_embed.rs89
-rw-r--r--src/cache/mod.rs172
-rw-r--r--src/client/bridge/gateway/shard_messenger.rs16
-rw-r--r--src/client/context.rs21
-rw-r--r--src/client/dispatch.rs57
-rw-r--r--src/framework/standard/args.rs433
-rw-r--r--src/framework/standard/help_commands.rs4
-rw-r--r--src/framework/standard/mod.rs30
-rw-r--r--src/http/mod.rs18
-rw-r--r--src/lib.rs4
-rw-r--r--src/model/channel/channel_id.rs6
-rw-r--r--src/model/channel/mod.rs90
-rw-r--r--src/model/gateway.rs52
-rw-r--r--src/model/guild/guild_id.rs12
-rw-r--r--src/model/guild/mod.rs106
-rw-r--r--src/model/misc.rs80
-rw-r--r--src/model/user.rs73
-rw-r--r--src/utils/colour.rs52
-rw-r--r--src/utils/message_builder.rs78
-rw-r--r--src/utils/mod.rs51
20 files changed, 1362 insertions, 82 deletions
diff --git a/src/builder/create_embed.rs b/src/builder/create_embed.rs
index 7604386..35172e1 100644
--- a/src/builder/create_embed.rs
+++ b/src/builder/create_embed.rs
@@ -515,3 +515,92 @@ impl<'a, Tz: TimeZone> From<&'a DateTime<Tz>> for Timestamp
}
}
}
+
+#[cfg(test)]
+mod test {
+ use model::channel::{Embed, EmbedField, EmbedFooter, EmbedImage, EmbedVideo};
+ use serde_json::Value;
+ use super::CreateEmbed;
+ use utils::{self, Colour};
+
+ #[test]
+ fn test_from_embed() {
+ let embed = Embed {
+ author: None,
+ colour: Colour::new(0xFF0011),
+ description: Some("This is a test description".to_string()),
+ fields: vec![
+ EmbedField {
+ inline: false,
+ name: "a".to_string(),
+ value: "b".to_string(),
+ },
+ EmbedField {
+ inline: true,
+ name: "c".to_string(),
+ value: "z".to_string(),
+ },
+ ],
+ footer: Some(EmbedFooter {
+ icon_url: Some("https://i.imgur.com/XfWpfCV.gif".to_string()),
+ proxy_icon_url: None,
+ text: "This is a hakase footer".to_string(),
+ }),
+ image: Some(EmbedImage {
+ height: 213,
+ proxy_url: "a".to_string(),
+ url: "https://i.imgur.com/XfWpfCV.gif".to_string(),
+ width: 224,
+ }),
+ kind: "rich".to_string(),
+ provider: None,
+ thumbnail: None,
+ timestamp: None,
+ title: Some("hakase".to_string()),
+ url: Some("https://i.imgur.com/XfWpfCV.gif".to_string()),
+ video: Some(EmbedVideo {
+ height: 213,
+ url: "https://i.imgur.com/XfWpfCV.mp4".to_string(),
+ width: 224,
+ }),
+ };
+
+ 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 = Value::Object(utils::vecmap_to_json_map(builder.0));
+
+ let obj = json!({
+ "color": 0xFF0011,
+ "description": "This is a hakase description",
+ "title": "still a hakase",
+ "type": "rich",
+ "url": "https://i.imgur.com/XfWpfCV.gif",
+ "fields": [
+ {
+ "inline": false,
+ "name": "a",
+ "value": "b",
+ },
+ {
+ "inline": true,
+ "name": "c",
+ "value": "z",
+ },
+ ],
+ "image": {
+ "url": "https://i.imgur.com/XfWpfCV.gif",
+ },
+ "footer": {
+ "text": "This is a hakase footer",
+ "icon_url": "https://i.imgur.com/XfWpfCV.gif",
+ }
+ });
+
+ assert_eq!(built, obj);
+ }
+}
diff --git a/src/cache/mod.rs b/src/cache/mod.rs
index 913fe21..0a09623 100644
--- a/src/cache/mod.rs
+++ b/src/cache/mod.rs
@@ -802,3 +802,175 @@ impl Default for Cache {
}
}
}
+
+#[cfg(test)]
+mod test {
+ use chrono::DateTime;
+ use serde_json::{Number, Value};
+ use std::{
+ collections::HashMap,
+ sync::Arc,
+ };
+ use {
+ cache::{Cache, CacheUpdate, Settings},
+ model::prelude::*,
+ prelude::RwLock,
+ };
+
+ #[test]
+ fn test_cache_messages() {
+ let mut settings = Settings::new();
+ settings.max_messages(2);
+ let mut cache = Cache::new_with_settings(settings);
+
+ // Test inserting one message into a channel's message cache.
+ let datetime = DateTime::parse_from_str(
+ "1983 Apr 13 12:09:14.274 +0000",
+ "%Y %b %d %H:%M:%S%.3f %z",
+ ).unwrap();
+ let mut event = MessageCreateEvent {
+ message: Message {
+ id: MessageId(3),
+ attachments: vec![],
+ author: User {
+ id: UserId(2),
+ avatar: None,
+ bot: false,
+ discriminator: 1,
+ name: "user 1".to_owned(),
+ },
+ channel_id: ChannelId(2),
+ guild_id: Some(GuildId(1)),
+ content: String::new(),
+ edited_timestamp: None,
+ embeds: vec![],
+ kind: MessageType::Regular,
+ member: None,
+ mention_everyone: false,
+ mention_roles: vec![],
+ mentions: vec![],
+ nonce: Value::Number(Number::from(1)),
+ pinned: false,
+ reactions: vec![],
+ timestamp: datetime.clone(),
+ tts: false,
+ webhook_id: None,
+ },
+ };
+ // Check that the channel cache doesn't exist.
+ assert!(!cache.messages.contains_key(&event.message.channel_id));
+ // Add first message, none because message ID 2 doesn't already exist.
+ assert!(event.update(&mut cache).is_none());
+ // None, it only returns the oldest message if the cache was already full.
+ assert!(event.update(&mut cache).is_none());
+ // Assert there's only 1 message in the channel's message cache.
+ assert_eq!(cache.messages.get(&event.message.channel_id).unwrap().len(), 1);
+
+ // Add a second message, assert that channel message cache length is 2.
+ event.message.id = MessageId(4);
+ assert!(event.update(&mut cache).is_none());
+ assert_eq!(cache.messages.get(&event.message.channel_id).unwrap().len(), 2);
+
+ // Add a third message, the first should now be removed.
+ event.message.id = MessageId(5);
+ assert!(event.update(&mut cache).is_some());
+
+ {
+ let channel = cache.messages.get(&event.message.channel_id).unwrap();
+
+ assert_eq!(channel.len(), 2);
+ // Check that the first message is now removed.
+ assert!(!channel.contains_key(&MessageId(3)));
+ }
+
+ let guild_channel = GuildChannel {
+ id: event.message.channel_id,
+ bitrate: None,
+ category_id: None,
+ guild_id: event.message.guild_id.unwrap(),
+ kind: ChannelType::Text,
+ last_message_id: None,
+ last_pin_timestamp: None,
+ name: String::new(),
+ permission_overwrites: vec![],
+ position: 0,
+ topic: None,
+ user_limit: None,
+ nsfw: false,
+ };
+
+ // Add a channel delete event to the cache, the cached messages for that
+ // channel should now be gone.
+ let mut delete = ChannelDeleteEvent {
+ channel: Channel::Guild(Arc::new(RwLock::new(guild_channel.clone()))),
+ };
+ assert!(cache.update(&mut delete).is_none());
+ assert!(!cache.messages.contains_key(&delete.channel.id()));
+
+ // Test deletion of a guild channel's message cache when a GuildDeleteEvent
+ // is received.
+ let mut guild_create = {
+ let mut channels = HashMap::new();
+ channels.insert(ChannelId(2), Arc::new(RwLock::new(guild_channel.clone())));
+
+ GuildCreateEvent {
+ guild: Guild {
+ id: GuildId(1),
+ afk_channel_id: None,
+ afk_timeout: 0,
+ application_id: None,
+ default_message_notifications: DefaultMessageNotificationLevel::All,
+ emojis: HashMap::new(),
+ explicit_content_filter: ExplicitContentFilter::None,
+ features: vec![],
+ icon: None,
+ joined_at: datetime,
+ large: false,
+ member_count: 0,
+ members: HashMap::new(),
+ mfa_level: MfaLevel::None,
+ name: String::new(),
+ owner_id: UserId(3),
+ presences: HashMap::new(),
+ region: String::new(),
+ roles: HashMap::new(),
+ splash: None,
+ system_channel_id: None,
+ verification_level: VerificationLevel::Low,
+ voice_states: HashMap::new(),
+ channels,
+ },
+ }
+ };
+ assert!(cache.update(&mut guild_create).is_none());
+ assert!(cache.update(&mut event).is_none());
+
+ let mut guild_delete = GuildDeleteEvent {
+ guild: PartialGuild {
+ id: GuildId(1),
+ afk_channel_id: None,
+ afk_timeout: 0,
+ default_message_notifications: DefaultMessageNotificationLevel::All,
+ embed_channel_id: None,
+ embed_enabled: false,
+ emojis: HashMap::new(),
+ features: vec![],
+ icon: None,
+ mfa_level: MfaLevel::None,
+ name: String::new(),
+ owner_id: UserId(3),
+ region: String::new(),
+ roles: HashMap::new(),
+ splash: None,
+ verification_level: VerificationLevel::Low,
+ },
+ };
+
+ // The guild existed in the cache, so the cache's guild is returned by the
+ // update.
+ assert!(cache.update(&mut guild_delete).is_some());
+
+ // Assert that the channel's message cache no longer exists.
+ assert!(!cache.messages.contains_key(&ChannelId(2)));
+ }
+}
diff --git a/src/client/bridge/gateway/shard_messenger.rs b/src/client/bridge/gateway/shard_messenger.rs
index 2331d4a..6d625b6 100644
--- a/src/client/bridge/gateway/shard_messenger.rs
+++ b/src/client/bridge/gateway/shard_messenger.rs
@@ -157,7 +157,11 @@ impl ShardMessenger {
/// # try_main().unwrap();
/// # }
/// ```
- pub fn set_game(&self, game: Option<Game>) {
+ pub fn set_game<T: Into<Game>>(&self, game: Option<T>) {
+ self._set_game(game.map(Into::into))
+ }
+
+ fn _set_game(&self, game: Option<Game>) {
let _ = self.send(ShardRunnerMessage::SetGame(game));
}
@@ -195,7 +199,15 @@ impl ShardMessenger {
/// # try_main().unwrap();
/// # }
/// ```
- pub fn set_presence(&self, game: Option<Game>, mut status: OnlineStatus) {
+ pub fn set_presence<T: Into<Game>>(
+ &self,
+ game: Option<T>,
+ status: OnlineStatus,
+ ) {
+ self._set_presence(game.map(Into::into), status)
+ }
+
+ fn _set_presence(&self, game: Option<Game>, mut status: OnlineStatus) {
if status == OnlineStatus::Offline {
status = OnlineStatus::Invisible;
}
diff --git a/src/client/context.rs b/src/client/context.rs
index d581ffb..61a1925 100644
--- a/src/client/context.rs
+++ b/src/client/context.rs
@@ -87,6 +87,7 @@ impl Context {
/// client.start().unwrap();
/// ```
#[cfg(feature = "builder")]
+ #[deprecated(since = "0.5.6", note = "Use the http module instead.")]
pub fn edit_profile<F: FnOnce(EditProfile) -> EditProfile>(&self, f: F) -> Result<CurrentUser> {
let mut map = VecMap::with_capacity(2);
@@ -273,7 +274,7 @@ impl Context {
/// [`set_presence`]: #method.set_presence
#[inline]
pub fn reset_presence(&self) {
- self.shard.set_presence(None, OnlineStatus::Online);
+ self.shard.set_presence(None::<Game>, OnlineStatus::Online);
}
/// Sets the current game, defaulting to an online status of [`Online`].
@@ -316,7 +317,11 @@ impl Context {
///
/// [`Online`]: ../model/user/enum.OnlineStatus.html#variant.Online
#[inline]
- pub fn set_game(&self, game: Game) {
+ pub fn set_game<T: Into<Game>>(&self, game: T) {
+ self._set_game(game.into())
+ }
+
+ fn _set_game(&self, game: Game) {
self.shard.set_presence(Some(game), OnlineStatus::Online);
}
@@ -356,14 +361,10 @@ impl Context {
/// [`Playing`]: ../model/gateway/enum.GameType.html#variant.Playing
/// [`reset_presence`]: #method.reset_presence
/// [`set_presence`]: #method.set_presence
- pub fn set_game_name(&self, game_name: &str) {
- let game = Game {
- kind: GameType::Playing,
- name: game_name.to_string(),
- url: None,
- };
-
- self.shard.set_presence(Some(game), OnlineStatus::Online);
+ #[deprecated(since = "0.5.5", note = "Use Context::set_game")]
+ #[inline]
+ pub fn set_game_name<T: Into<String>>(&self, game_name: T) {
+ self.set_game(game_name.into())
}
/// Sets the current user's presence, providing all fields to be passed.
diff --git a/src/client/dispatch.rs b/src/client/dispatch.rs
index dd5b4fe..663a283 100644
--- a/src/client/dispatch.rs
+++ b/src/client/dispatch.rs
@@ -15,14 +15,10 @@ use std::sync::mpsc::Sender;
use threadpool::ThreadPool;
use typemap::ShareMap;
-#[cfg(feature = "cache")]
-use chrono::{Timelike, Utc};
#[cfg(feature = "framework")]
use framework::Framework;
#[cfg(feature = "cache")]
use model::id::GuildId;
-#[cfg(feature = "cache")]
-use std::{thread, time};
#[cfg(feature = "cache")]
use super::CACHE;
@@ -38,11 +34,6 @@ macro_rules! update {
};
}
-#[cfg(feature = "cache")]
-macro_rules! now {
- () => (Utc::now().time().second() * 1000)
-}
-
fn context(
data: &Arc<Mutex<ShareMap>>,
runner_tx: &Sender<InterMessage>,
@@ -149,20 +140,6 @@ fn handle_event<H: EventHandler + Send + Sync + 'static>(
threadpool: &ThreadPool,
shard_id: u64,
) {
- #[cfg(feature = "cache")]
- let mut last_guild_create_time = now!();
-
- #[cfg(feature = "cache")]
- let wait_for_guilds = move || -> ::Result<()> {
- let unavailable_guilds = CACHE.read().unavailable_guilds.len();
-
- while unavailable_guilds != 0 && (now!() < last_guild_create_time + 2000) {
- thread::sleep(time::Duration::from_millis(500));
- }
-
- Ok(())
- };
-
match event {
DispatchEvent::Client(ClientEvent::ShardStageUpdate(event)) => {
let context = context(data, runner_tx, shard_id);
@@ -177,9 +154,9 @@ fn handle_event<H: EventHandler + Send + Sync + 'static>(
let context = context(data, runner_tx, shard_id);
- // This different channel_create dispatching is only due to the fact that
- // each time the bot receives a dm, this event is also fired.
- // So in short, only exists to reduce unnecessary clutter.
+ // Discord sends both a MessageCreate and a ChannelCreate upon a new message in a private channel.
+ // This could potentionally be annoying to handle when otherwise wanting to normally take care of a new channel.
+ // So therefore, private channels are dispatched to their own handler code.
match event.channel {
Channel::Private(channel) => {
let event_handler = Arc::clone(event_handler);
@@ -309,8 +286,6 @@ fn handle_event<H: EventHandler + Send + Sync + 'static>(
#[cfg(feature = "cache")]
{
- last_guild_create_time = now!();
-
let cache = CACHE.read();
if cache.unavailable_guilds.is_empty() {
@@ -578,28 +553,12 @@ fn handle_event<H: EventHandler + Send + Sync + 'static>(
DispatchEvent::Model(Event::Ready(mut event)) => {
update!(event);
- let event_handler = Arc::clone(event_handler);
-
- feature_cache! {{
- last_guild_create_time = now!();
-
- let _ = wait_for_guilds()
- .map(move |_| {
- let context = context(data, runner_tx, shard_id);
- let event_handler = Arc::clone(&event_handler);
-
- threadpool.execute(move || {
- event_handler.ready(context, event.ready);
- });
- });
- } else {
- let context = context(data, runner_tx, shard_id);
- let event_handler = Arc::clone(&event_handler);
+ let context = context(data, runner_tx, shard_id);
+ let event_handler = Arc::clone(&event_handler);
- threadpool.execute(move || {
- event_handler.ready(context, event.ready);
- });
- }}
+ threadpool.execute(move || {
+ event_handler.ready(context, event.ready);
+ });
},
DispatchEvent::Model(Event::Resumed(mut event)) => {
let context = context(data, runner_tx, shard_id);
diff --git a/src/framework/standard/args.rs b/src/framework/standard/args.rs
index bf3056b..0b915b6 100644
--- a/src/framework/standard/args.rs
+++ b/src/framework/standard/args.rs
@@ -944,3 +944,436 @@ fn quotes_extract(token: &Token) -> &str {
&token.lit
}
}
+
+#[cfg(test)]
+mod test {
+ use super::{Args, Error as ArgError};
+
+ #[test]
+ fn single_with_empty_message() {
+ let mut args = Args::new("", &["".to_string()]);
+ assert_matches!(args.single::<String>().unwrap_err(), ArgError::Eos);
+
+ let mut args = Args::new("", &[",".to_string()]);
+ assert_matches!(args.single::<String>().unwrap_err(), ArgError::Eos);
+ }
+
+ #[test]
+ fn single_n_with_empty_message() {
+ let args = Args::new("", &["".to_string()]);
+ assert_matches!(args.single_n::<String>().unwrap_err(), ArgError::Eos);
+
+ let args = Args::new("", &[",".to_string()]);
+ assert_matches!(args.single_n::<String>().unwrap_err(), ArgError::Eos);
+ }
+
+ #[test]
+ fn single_quoted_with_empty_message() {
+ let mut args = Args::new("", &["".to_string()]);
+ assert_matches!(args.single_quoted::<String>().unwrap_err(), ArgError::Eos);
+
+ let mut args = Args::new("", &[",".to_string()]);
+ assert_matches!(args.single_quoted::<String>().unwrap_err(), ArgError::Eos);
+ }
+
+ #[test]
+ fn multiple_with_empty_message() {
+ let args = Args::new("", &["".to_string()]);
+ assert_matches!(args.multiple::<String>().unwrap_err(), ArgError::Eos);
+
+ let args = Args::new("", &[",".to_string()]);
+ assert_matches!(args.multiple::<String>().unwrap_err(), ArgError::Eos);
+ }
+
+ #[test]
+ fn multiple_quoted_with_empty_message() {
+ let args = Args::new("", &["".to_string()]);
+ assert_matches!(args.multiple_quoted::<String>().unwrap_err(), ArgError::Eos);
+
+ let args = Args::new("", &[",".to_string()]);
+ assert_matches!(args.multiple_quoted::<String>().unwrap_err(), ArgError::Eos);
+ }
+
+ #[test]
+ fn skip_with_empty_message() {
+ let mut args = Args::new("", &["".to_string()]);
+ assert_matches!(args.skip(), None);
+
+ let mut args = Args::new("", &[",".to_string()]);
+ assert_matches!(args.skip(), None);
+ }
+
+ #[test]
+ fn skip_for_with_empty_message() {
+ let mut args = Args::new("", &["".to_string()]);
+ assert_matches!(args.skip_for(0), None);
+
+ let mut args = Args::new("", &["".to_string()]);
+ assert_matches!(args.skip_for(5), None);
+
+ let mut args = Args::new("", &[",".to_string()]);
+ assert_matches!(args.skip_for(0), None);
+
+ let mut args = Args::new("", &[",".to_string()]);
+ assert_matches!(args.skip_for(5), None);
+ }
+
+ #[test]
+ fn single_i32_with_2_bytes_long_delimiter() {
+ let mut args = Args::new("1, 2", &[", ".to_string()]);
+
+ assert_eq!(args.single::<i32>().unwrap(), 1);
+ assert_eq!(args.single::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn single_i32_with_1_byte_long_delimiter_i32() {
+ let mut args = Args::new("1,2", &[",".to_string()]);
+
+ assert_eq!(args.single::<i32>().unwrap(), 1);
+ assert_eq!(args.single::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn single_i32_with_wrong_char_after_first_arg() {
+ let mut args = Args::new("1, 2", &[",".to_string()]);
+
+ assert_eq!(args.single::<i32>().unwrap(), 1);
+ assert!(args.single::<i32>().is_err());
+ }
+
+ #[test]
+ fn single_i32_with_one_character_being_3_bytes_long() {
+ let mut args = Args::new("1★2", &["★".to_string()]);
+
+ assert_eq!(args.single::<i32>().unwrap(), 1);
+ assert_eq!(args.single::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn single_i32_with_untrimmed_whitespaces() {
+ let mut args = Args::new(" 1, 2 ", &[",".to_string()]);
+
+ assert!(args.single::<i32>().is_err());
+ }
+
+ #[test]
+ fn single_i32_n() {
+ let args = Args::new("1,2", &[",".to_string()]);
+
+ assert_eq!(args.single_n::<i32>().unwrap(), 1);
+ assert_eq!(args.single_n::<i32>().unwrap(), 1);
+ }
+
+ #[test]
+ fn single_quoted_chaining() {
+ let mut args = Args::new(r#""1, 2" "2" """#, &[" ".to_string()]);
+
+ assert_eq!(args.single_quoted::<String>().unwrap(), "1, 2");
+ assert_eq!(args.single_quoted::<String>().unwrap(), "2");
+ assert_eq!(args.single_quoted::<String>().unwrap(), "");
+ }
+
+ #[test]
+ fn single_quoted_and_single_chaining() {
+ let mut args = Args::new(r#""1, 2" "2" "3" 4"#, &[" ".to_string()]);
+
+ assert_eq!(args.single_quoted::<String>().unwrap(), "1, 2");
+ assert!(args.single_n::<i32>().is_err());
+ assert_eq!(args.single::<String>().unwrap(), "\"2\"");
+ assert_eq!(args.single_quoted::<i32>().unwrap(), 3);
+ assert_eq!(args.single::<i32>().unwrap(), 4);
+ }
+
+ #[test]
+ fn full_on_args() {
+ let test_text = "Some text to ensure `full()` works.";
+ let args = Args::new(test_text, &[" ".to_string()]);
+
+ assert_eq!(args.full(), test_text);
+ }
+
+ #[test]
+ fn multiple_quoted_strings_one_delimiter() {
+ let args = Args::new(r#""1, 2" "a" "3" 4 "5"#, &[" ".to_string()]);
+
+ assert_eq!(args.multiple_quoted::<String>().unwrap(), ["1, 2", "a", "3", "4", "\"5"]);
+ }
+
+ #[test]
+ fn multiple_quoted_strings_with_multiple_delimiter() {
+ let args = Args::new(r#""1, 2" "a","3"4 "5"#, &[" ".to_string(), ",".to_string()]);
+
+ assert_eq!(args.multiple_quoted::<String>().unwrap(), ["1, 2", "a", "3", "4", "\"5"]);
+ }
+
+ #[test]
+ fn multiple_quoted_strings_with_multiple_delimiters() {
+ let args = Args::new(r#""1, 2" "a","3" """#, &[" ".to_string(), ",".to_string()]);
+
+ assert_eq!(args.multiple_quoted::<String>().unwrap(), ["1, 2", "a", "3", ""]);
+ }
+
+ #[test]
+ fn multiple_quoted_i32() {
+ let args = Args::new(r#""1" "2" 3"#, &[" ".to_string()]);
+
+ assert_eq!(args.multiple_quoted::<i32>().unwrap(), [1, 2, 3]);
+ }
+
+ #[test]
+ fn multiple_quoted_quote_appears_without_delimiter_in_front() {
+ let args = Args::new(r#"hello, my name is cake" 2"#, &[",".to_string(), " ".to_string()]);
+
+ assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello", "my", "name", "is", "cake\"", "2"]);
+ }
+
+ #[test]
+ fn multiple_quoted_single_quote() {
+ let args = Args::new(r#"hello "2 b"#, &[",".to_string(), " ".to_string()]);
+
+ assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello", "\"2 b"]);
+ }
+
+ #[test]
+ fn multiple_quoted_one_quote_pair() {
+ let args = Args::new(r#"hello "2 b""#, &[",".to_string(), " ".to_string()]);
+
+ assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello", "2 b"]);
+ }
+
+
+ #[test]
+ fn delimiter_before_multiple_quoted() {
+ let args = Args::new(r#","hello, my name is cake" "2""#, &[",".to_string(), " ".to_string()]);
+
+ assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello, my name is cake", "2"]);
+ }
+
+ #[test]
+ fn no_quote() {
+ let args = Args::new("hello, my name is cake", &[",".to_string(), " ".to_string()]);
+
+ assert_eq!(args.single_quoted_n::<String>().unwrap(), "hello");
+ }
+
+ #[test]
+ fn single_quoted_n() {
+ let args = Args::new(r#""hello, my name is cake","test"#, &[",".to_string()]);
+
+ assert_eq!(args.single_quoted_n::<String>().unwrap(), "hello, my name is cake");
+ assert_eq!(args.single_quoted_n::<String>().unwrap(), "hello, my name is cake");
+ }
+
+ #[test]
+ fn multiple_quoted_starting_with_wrong_delimiter_in_first_quote() {
+ let args = Args::new(r#""hello, my name is cake" "2""#, &[",".to_string(), " ".to_string()]);
+
+ assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello, my name is cake", "2"]);
+ }
+
+ #[test]
+ fn multiple_quoted_with_one_correct_and_one_invalid_quote() {
+ let args = Args::new(r#""hello, my name is cake" "2""#, &[",".to_string(), " ".to_string()]);
+
+ assert_eq!(args.multiple_quoted::<String>().unwrap(), ["hello, my name is cake", "2"]);
+ }
+
+ #[test]
+ fn find_i32_one_one_byte_delimiter() {
+ let mut args = Args::new("hello,my name is cake 2", &[" ".to_string()]);
+
+ assert_eq!(args.find::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn find_i32_one_three_byte_delimiter() {
+ let mut args = Args::new("hello,my name is cakeé2", &["é".to_string()]);
+
+ assert_eq!(args.find::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn find_i32_multiple_delimiter_but_i32_not_last() {
+ let mut args = Args::new("hello,my name is 2 cake", &[" ".to_string(), ",".to_string()]);
+
+ assert_eq!(args.find::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn find_i32_multiple_delimiter() {
+ let mut args = Args::new("hello,my name is cake 2", &[" ".to_string(), ",".to_string()]);
+
+ assert_eq!(args.find::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn find_n_i32() {
+ let mut args = Args::new("a 2", &[" ".to_string()]);
+
+ assert_eq!(args.find_n::<i32>().unwrap(), 2);
+ assert_eq!(args.find_n::<i32>().unwrap(), 2);
+ }
+
+ #[test]
+ fn skip() {
+ let mut args = Args::new("1 2", &[" ".to_string()]);
+
+ assert_eq!(args.skip().unwrap(), "1");
+ assert_eq!(args.remaining(), 1);
+ assert_eq!(args.single::<String>().unwrap(), "2");
+ }
+
+ #[test]
+ fn skip_for() {
+ let mut args = Args::new("1 2 neko 100", &[" ".to_string()]);
+
+ assert_eq!(args.skip_for(2).unwrap(), ["1", "2"]);
+ assert_eq!(args.remaining(), 2);
+ assert_eq!(args.single::<String>().unwrap(), "neko");
+ assert_eq!(args.single::<String>().unwrap(), "100");
+ }
+
+ #[test]
+ fn len_with_one_delimiter() {
+ let args = Args::new("1 2 neko 100", &[" ".to_string()]);
+
+ assert_eq!(args.len(), 4);
+ assert_eq!(args.remaining(), 4);
+ }
+
+ #[test]
+ fn len_multiple_quoted() {
+ let args = Args::new(r#""hello, my name is cake" "2""#, &[" ".to_string()]);
+
+ assert_eq!(args.len(), 2);
+ }
+
+ #[test]
+ fn remaining_len_before_and_after_single() {
+ let mut args = Args::new("1 2", &[" ".to_string()]);
+
+ assert_eq!(args.remaining(), 2);
+ assert_eq!(args.single::<i32>().unwrap(), 1);
+ assert_eq!(args.remaining(), 1);
+ assert_eq!(args.single::<i32>().unwrap(), 2);
+ assert_eq!(args.remaining(), 0);
+ }
+
+ #[test]
+ fn remaining_len_before_and_after_single_quoted() {
+ let mut args = Args::new(r#""1" "2" "3""#, &[" ".to_string()]);
+
+ assert_eq!(args.remaining(), 3);
+ assert_eq!(args.single_quoted::<i32>().unwrap(), 1);
+ assert_eq!(args.remaining(), 2);
+ assert_eq!(args.single_quoted::<i32>().unwrap(), 2);
+ assert_eq!(args.remaining(), 1);
+ assert_eq!(args.single_quoted::<i32>().unwrap(), 3);
+ assert_eq!(args.remaining(), 0);
+ }
+
+ #[test]
+ fn remaining_len_before_and_after_skip() {
+ let mut args = Args::new("1 2", &[" ".to_string()]);
+
+ assert_eq!(args.remaining(), 2);
+ assert_eq!(args.skip().unwrap(), "1");
+ assert_eq!(args.remaining(), 1);
+ assert_eq!(args.skip().unwrap(), "2");
+ assert_eq!(args.remaining(), 0);
+ }
+
+ #[test]
+ fn remaining_len_before_and_after_skip_empty_string() {
+ let mut args = Args::new("", &[" ".to_string()]);
+
+ assert_eq!(args.remaining(), 0);
+ assert_eq!(args.skip(), None);
+ assert_eq!(args.remaining(), 0);
+ }
+
+ #[test]
+ fn remaining_len_before_and_after_skip_for() {
+ let mut args = Args::new("1 2", &[" ".to_string()]);
+
+ assert_eq!(args.remaining(), 2);
+ assert_eq!(args.skip_for(2), Some(vec!["1".to_string(), "2".to_string()]));
+ assert_eq!(args.skip_for(2), None);
+ assert_eq!(args.remaining(), 0);
+ }
+
+ #[test]
+ fn remaining_len_before_and_after_find() {
+ let mut args = Args::new("a 2 6", &[" ".to_string()]);
+
+ assert_eq!(args.remaining(), 3);
+ assert_eq!(args.find::<i32>().unwrap(), 2);
+ assert_eq!(args.remaining(), 2);
+ assert_eq!(args.find::<i32>().unwrap(), 6);
+ assert_eq!(args.remaining(), 1);
+ assert_eq!(args.find::<String>().unwrap(), "a");
+ assert_eq!(args.remaining(), 0);
+ assert_matches!(args.find::<String>().unwrap_err(), ArgError::Eos);
+ assert_eq!(args.remaining(), 0);
+ }
+
+ #[test]
+ fn remaining_len_before_and_after_find_n() {
+ let mut args = Args::new("a 2 6", &[" ".to_string()]);
+
+ assert_eq!(args.remaining(), 3);
+ assert_eq!(args.find_n::<i32>().unwrap(), 2);
+ assert_eq!(args.remaining(), 3);
+ }
+
+
+ #[test]
+ fn multiple_strings_with_one_delimiter() {
+ let args = Args::new("hello, my name is cake 2", &[" ".to_string()]);
+
+ assert_eq!(args.multiple::<String>().unwrap(), ["hello,", "my", "name", "is", "cake", "2"]);
+ }
+
+ #[test]
+ fn multiple_i32_with_one_delimiter() {
+ let args = Args::new("1 2 3", &[" ".to_string()]);
+
+ assert_eq!(args.multiple::<i32>().unwrap(), [1, 2, 3]);
+ }
+
+ #[test]
+ fn multiple_i32_with_one_delimiter_and_parse_error() {
+ let args = Args::new("1 2 3 abc", &[" ".to_string()]);
+
+ assert_matches!(args.multiple::<i32>().unwrap_err(), ArgError::Parse(_));
+ }
+
+ #[test]
+ fn multiple_i32_with_three_delimiters() {
+ let args = Args::new("1 2 3", &[" ".to_string(), ",".to_string()]);
+
+ assert_eq!(args.multiple::<i32>().unwrap(), [1, 2, 3]);
+ }
+
+ #[test]
+ fn single_after_failed_single() {
+ let mut args = Args::new("b 2", &[" ".to_string()]);
+
+ assert_matches!(args.single::<i32>().unwrap_err(), ArgError::Parse(_));
+ // Test that `single` short-circuts on an error and leaves the source as is.
+ assert_eq!(args.remaining(), 2);
+ assert_eq!(args.single::<String>().unwrap(), "b");
+ assert_eq!(args.single::<String>().unwrap(), "2");
+ }
+
+ #[test]
+ fn remaining_len_after_failed_single_quoted() {
+ let mut args = Args::new("b a", &[" ".to_string()]);
+
+ assert_eq!(args.remaining(), 2);
+ // Same goes for `single_quoted` and the alike.
+ assert_matches!(args.single_quoted::<i32>().unwrap_err(), ArgError::Parse(_));
+ assert_eq!(args.remaining(), 2);
+ }
+}
diff --git a/src/framework/standard/help_commands.rs b/src/framework/standard/help_commands.rs
index fd74ace..e891ff5 100644
--- a/src/framework/standard/help_commands.rs
+++ b/src/framework/standard/help_commands.rs
@@ -266,7 +266,7 @@ pub fn with_embeds<H: BuildHasher>(
&help_options.striked_commands_tip_in_dm
};
- if let Some(ref striked_command_text) = striked_command_tip {
+ if let &Some(ref striked_command_text) = striked_command_tip {
e = e.colour(help_options.embed_success_colour).description(
format!("{}\n{}", &help_options.individual_command_tip, striked_command_text),
);
@@ -510,7 +510,7 @@ pub fn plain<H: BuildHasher>(
&help_options.striked_commands_tip_in_dm
};
- if let Some(ref striked_command_text) = striked_command_tip {
+ if let &Some(ref striked_command_text) = striked_command_tip {
let _ = writeln!(result, "{}\n{}\n", &help_options.individual_command_tip, striked_command_text);
} else {
let _ = writeln!(result, "{}\n", &help_options.individual_command_tip);
diff --git a/src/framework/standard/mod.rs b/src/framework/standard/mod.rs
index d947bfd..0bb0309 100644
--- a/src/framework/standard/mod.rs
+++ b/src/framework/standard/mod.rs
@@ -325,12 +325,10 @@ impl StandardFramework {
///
/// client.with_framework(StandardFramework::new()
/// .complex_bucket("basic", 2, 10, 3, |_, guild_id, channel_id, user_id| {
- /// // check if the guild is `123` and the channel where the command(s) was called:
- /// // `456`
- /// // and if the user who called the command(s) is `789`
- /// // otherwise don't apply the bucket at all.
- /// guild_id.is_some() && guild_id.unwrap() == 123 && channel_id == 456
- /// && user_id == 789
+ /// // Our bucket is very strict. It cannot apply in DMs.
+ /// // And can only apply if it's in the specific guild, channel and by the specific user.
+ /// guild_id.is_some() && guild_id.unwrap() == 123 && channel_id == 456
+ /// && user_id == 789
/// })
/// .command("ping", |c| c
/// .bucket("basic")
@@ -338,7 +336,9 @@ impl StandardFramework {
/// msg.channel_id.say("pong!")?;
///
/// Ok(())
- /// })));
+ /// })
+ /// )
+ /// );
/// ```
///
/// [`bucket`]: #method.bucket
@@ -384,14 +384,18 @@ impl StandardFramework {
///
/// client.with_framework(StandardFramework::new()
/// .complex_bucket("basic", 2, 10, 3, |_, channel_id, user_id| {
- /// // check if the channel's id where the command(s) was called is `456`
- /// // and if the user who called the command(s) is `789`
- /// // otherwise don't apply the bucket at all.
+ /// Our bucket is somewhat strict. It can only apply in the specific channel and by the specific user.
/// channel_id == 456 && user_id == 789
/// })
/// .command("ping", |c| c
/// .bucket("basic")
- /// .exec_str("pong!")));
+ /// .exec(|_, msg, _| {
+ /// msg.channel_id.say("pong!")?;
+ ///
+ /// Ok(())
+ /// })
+ /// )
+ /// );
/// ```
///
/// [`bucket`]: #method.bucket
@@ -1142,7 +1146,7 @@ impl Framework for StandardFramework {
}
if check_contains_group_prefix {
- if let Some(CommandOrAlias::Command(ref command)) = &group.default_command {
+ if let &Some(CommandOrAlias::Command(ref command)) = &group.default_command {
let command = Arc::clone(command);
threadpool.execute(move || {
@@ -1172,7 +1176,7 @@ impl Framework for StandardFramework {
}
}
- if let Some(unrecognised_command) = &self.unrecognised_command {
+ if let &Some(ref unrecognised_command) = &self.unrecognised_command {
let unrecognised_command = unrecognised_command.clone();
threadpool.execute(move || {
(unrecognised_command)(&mut context, &message, &unrecognised_command_name);
diff --git a/src/http/mod.rs b/src/http/mod.rs
index 71c57c6..c50679a 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -2008,3 +2008,21 @@ pub enum GuildPagination {
/// The Id to get the guilds before.
Before(GuildId),
}
+
+#[cfg(test)]
+mod test {
+ use super::AttachmentType;
+ use std::path::Path;
+
+ #[test]
+ fn test_attachment_type() {
+ assert!(match AttachmentType::from(Path::new("./dogs/corgis/kona.png")) {
+ AttachmentType::Path(_) => true,
+ _ => false,
+ });
+ assert!(match AttachmentType::from("./cats/copycat.png") {
+ AttachmentType::Path(_) => true,
+ _ => false,
+ });
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 74f050a..0e4577d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -140,6 +140,10 @@ extern crate typemap;
#[cfg(feature = "evzht9h3nznqzwl")]
extern crate evzht9h3nznqzwl as websocket;
+#[cfg(test)]
+#[macro_use]
+extern crate matches;
+
#[macro_use]
mod internal;
diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs
index 48003ef..ceb05c3 100644
--- a/src/model/channel/channel_id.rs
+++ b/src/model/channel/channel_id.rs
@@ -557,9 +557,9 @@ impl ChannelId {
/// over the limit.
///
/// [`Channel`]: enum.Channel.html
- /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong
- /// [`CreateMessage`]: ../builder/struct.CreateMessage.html
- /// [Send Messages]: permissions/constant.SEND_MESSAGES.html
+ /// [`ModelError::MessageTooLong`]: ../error/enum.Error.html#variant.MessageTooLong
+ /// [`CreateMessage`]: ../../builder/struct.CreateMessage.html
+ /// [Send Messages]: ../permissions/struct.Permissions.html#associatedconstant.SEND_MESSAGES
#[cfg(feature = "utils")]
pub fn send_message<F>(&self, f: F) -> Result<Message>
where F: FnOnce(CreateMessage) -> CreateMessage {
diff --git a/src/model/channel/mod.rs b/src/model/channel/mod.rs
index 8d586a6..e0ef085 100644
--- a/src/model/channel/mod.rs
+++ b/src/model/channel/mod.rs
@@ -677,3 +677,93 @@ pub enum PermissionOverwriteType {
/// A role which is having its permission overwrites edited.
Role(RoleId),
}
+
+#[cfg(test)]
+mod test {
+ #[cfg(feature = "utils")]
+ mod utils {
+ use model::prelude::*;
+ use parking_lot::RwLock;
+ use std::collections::HashMap;
+ use std::sync::Arc;
+
+ fn group() -> Group {
+ Group {
+ channel_id: ChannelId(1),
+ icon: None,
+ last_message_id: None,
+ last_pin_timestamp: None,
+ name: None,
+ owner_id: UserId(2),
+ recipients: HashMap::new(),
+ }
+ }
+
+ fn guild_channel() -> GuildChannel {
+ GuildChannel {
+ id: ChannelId(1),
+ bitrate: None,
+ category_id: None,
+ guild_id: GuildId(2),
+ kind: ChannelType::Text,
+ last_message_id: None,
+ last_pin_timestamp: None,
+ name: "nsfw-stuff".to_string(),
+ permission_overwrites: vec![],
+ position: 0,
+ topic: None,
+ user_limit: None,
+ nsfw: false,
+ }
+ }
+
+ fn private_channel() -> PrivateChannel {
+ PrivateChannel {
+ id: ChannelId(1),
+ last_message_id: None,
+ last_pin_timestamp: None,
+ kind: ChannelType::Private,
+ recipient: Arc::new(RwLock::new(User {
+ id: UserId(2),
+ avatar: None,
+ bot: false,
+ discriminator: 1,
+ name: "ab".to_string(),
+ })),
+ }
+ }
+
+ #[test]
+ fn nsfw_checks() {
+ let mut channel = guild_channel();
+ assert!(channel.is_nsfw());
+ channel.kind = ChannelType::Voice;
+ assert!(!channel.is_nsfw());
+
+ channel.kind = ChannelType::Text;
+ channel.name = "nsfw-".to_string();
+ assert!(!channel.is_nsfw());
+
+ channel.name = "nsfw".to_string();
+ assert!(channel.is_nsfw());
+ channel.kind = ChannelType::Voice;
+ assert!(!channel.is_nsfw());
+ channel.kind = ChannelType::Text;
+
+ channel.name = "nsf".to_string();
+ channel.nsfw = true;
+ assert!(channel.is_nsfw());
+ channel.nsfw = false;
+ assert!(!channel.is_nsfw());
+
+ let channel = Channel::Guild(Arc::new(RwLock::new(channel)));
+ assert!(!channel.is_nsfw());
+
+ let group = group();
+ assert!(!group.is_nsfw());
+
+ let private_channel = private_channel();
+ assert!(!private_channel.is_nsfw());
+ }
+ }
+}
diff --git a/src/model/gateway.rs b/src/model/gateway.rs
index b73d900..9af4a7e 100644
--- a/src/model/gateway.rs
+++ b/src/model/gateway.rs
@@ -131,6 +131,58 @@ impl Game {
}
}
+impl<'a> From<&'a str> for Game {
+ fn from(name: &'a str) -> Self {
+ Game::playing(name)
+ }
+}
+
+impl From<String> for Game {
+ fn from(name: String) -> Self {
+ Game::playing(&name)
+ }
+}
+
+impl<'a> From<(String, GameType)> for Game {
+ fn from((name, kind): (String, GameType)) -> Self {
+ Self {
+ url: None,
+ kind,
+ name,
+ }
+ }
+}
+
+impl<'a> From<(&'a str, &'a str)> for Game {
+ fn from((name, url): (&'a str, &'a str)) -> Self {
+ Self {
+ kind: GameType::Streaming,
+ name: name.to_owned(),
+ url: Some(url.to_owned()),
+ }
+ }
+}
+
+impl From<(String, String)> for Game {
+ fn from((name, url): (String, String)) -> Self {
+ Self {
+ kind: GameType::Streaming,
+ url: Some(url),
+ name,
+ }
+ }
+}
+
+impl From<(String, GameType, String)> for Game {
+ fn from((name, kind, url): (String, GameType, String)) -> Self {
+ Self {
+ url: Some(url),
+ kind,
+ name,
+ }
+ }
+}
+
impl<'de> Deserialize<'de> for Game {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
let mut map = JsonMap::deserialize(deserializer)?;
diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs
index 9c3120a..ebe930e 100644
--- a/src/model/guild/guild_id.rs
+++ b/src/model/guild/guild_id.rs
@@ -444,7 +444,10 @@ impl GuildId {
#[inline]
pub fn leave(&self) -> Result<()> { http::leave_guild(self.0) }
- /// Gets a user's [`Member`] for the guild by Id.
+ /// Gets a user's [`Member`] for the guild by Id.
+ ///
+ /// If the cache feature is enabled the cache will be checked
+ /// first. If not found it will resort to an http request.
///
/// [`Guild`]: struct.Guild.html
/// [`Member`]: struct.Member.html
@@ -454,6 +457,13 @@ impl GuildId {
}
fn _member(&self, user_id: UserId) -> Result<Member> {
+ #[cfg(feature = "cache")]
+ {
+ if let Some(member) = CACHE.read().member(self.0, user_id) {
+ return Ok(member);
+ }
+ }
+
http::get_member(self.0, user_id.0)
}
diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs
index 67b0f0e..30651d6 100644
--- a/src/model/guild/mod.rs
+++ b/src/model/guild/mod.rs
@@ -2015,3 +2015,109 @@ impl VerificationLevel {
}
}
}
+
+#[cfg(test)]
+mod test {
+ #[cfg(feature = "model")]
+ mod model {
+ use chrono::prelude::*;
+ use model::prelude::*;
+ use std::collections::*;
+ use std::sync::Arc;
+
+ fn gen_user() -> User {
+ User {
+ id: UserId(210),
+ avatar: Some("abc".to_string()),
+ bot: true,
+ discriminator: 1432,
+ name: "test".to_string(),
+ }
+ }
+
+ fn gen_member() -> Member {
+ let dt: DateTime<FixedOffset> = FixedOffset::east(5 * 3600)
+ .ymd(2016, 11, 08)
+ .and_hms(0, 0, 0);
+ let vec1 = Vec::new();
+ let u = Arc::new(RwLock::new(gen_user()));
+
+ Member {
+ deaf: false,
+ guild_id: GuildId(1),
+ joined_at: Some(dt),
+ mute: false,
+ nick: Some("aaaa".to_string()),
+ roles: vec1,
+ user: u,
+ }
+ }
+
+ fn gen() -> Guild {
+ let u = gen_user();
+ let m = gen_member();
+
+ let hm1 = HashMap::new();
+ let hm2 = HashMap::new();
+ let vec1 = Vec::new();
+ let dt: DateTime<FixedOffset> = FixedOffset::east(5 * 3600)
+ .ymd(2016, 11, 08)
+ .and_hms(0, 0, 0);
+ let mut hm3 = HashMap::new();
+ let hm4 = HashMap::new();
+ let hm5 = HashMap::new();
+ let hm6 = HashMap::new();
+
+ hm3.insert(u.id, m);
+
+ let notifications = DefaultMessageNotificationLevel::All;
+
+ Guild {
+ afk_channel_id: Some(ChannelId(0)),
+ afk_timeout: 0,
+ channels: hm1,
+ default_message_notifications: notifications,
+ emojis: hm2,
+ features: vec1,
+ icon: Some("/avatars/210/a_aaa.webp?size=1024".to_string()),
+ id: GuildId(1),
+ joined_at: dt,
+ large: false,
+ member_count: 1,
+ members: hm3,
+ mfa_level: MfaLevel::Elevated,
+ name: "Spaghetti".to_string(),
+ owner_id: UserId(210),
+ presences: hm4,
+ region: "NA".to_string(),
+ roles: hm5,
+ splash: Some("asdf".to_string()),
+ verification_level: VerificationLevel::None,
+ voice_states: hm6,
+ application_id: Some(ApplicationId(0)),
+ explicit_content_filter: ExplicitContentFilter::None,
+ system_channel_id: Some(ChannelId(0)),
+ }
+ }
+
+
+ #[test]
+ fn member_named_username() {
+ let guild = gen();
+ let lhs = guild
+ .member_named("test#1432")
+ .unwrap()
+ .display_name();
+
+ assert_eq!(lhs, gen_member().display_name());
+ }
+
+ #[test]
+ fn member_named_nickname() {
+ let guild = gen();
+ let lhs = guild.member_named("aaaa").unwrap().display_name();
+
+ assert_eq!(lhs, gen_member().display_name());
+ }
+ }
+}
diff --git a/src/model/misc.rs b/src/model/misc.rs
index 3195c55..62a002a 100644
--- a/src/model/misc.rs
+++ b/src/model/misc.rs
@@ -308,3 +308,83 @@ pub struct Maintenance {
pub start: String,
pub stop: String,
}
+
+#[cfg(test)]
+mod test {
+ use model::prelude::*;
+ use parking_lot::RwLock;
+ use std::sync::Arc;
+ use utils::Colour;
+
+ #[test]
+ fn test_formatters() {
+ assert_eq!(ChannelId(1).to_string(), "1");
+ assert_eq!(EmojiId(2).to_string(), "2");
+ assert_eq!(GuildId(3).to_string(), "3");
+ assert_eq!(RoleId(4).to_string(), "4");
+ assert_eq!(UserId(5).to_string(), "5");
+ }
+
+ #[cfg(feature = "utils")]
+ #[test]
+ fn test_mention() {
+ let channel = Channel::Guild(Arc::new(RwLock::new(GuildChannel {
+ bitrate: None,
+ category_id: None,
+ guild_id: GuildId(1),
+ kind: ChannelType::Text,
+ id: ChannelId(4),
+ last_message_id: None,
+ last_pin_timestamp: None,
+ name: "a".to_string(),
+ permission_overwrites: vec![],
+ position: 1,
+ topic: None,
+ user_limit: None,
+ nsfw: false,
+ })));
+ let emoji = Emoji {
+ animated: false,
+ id: EmojiId(5),
+ name: "a".to_string(),
+ managed: true,
+ require_colons: true,
+ roles: vec![],
+ };
+ let role = Role {
+ id: RoleId(2),
+ colour: Colour::ROSEWATER,
+ hoist: false,
+ managed: false,
+ mentionable: false,
+ name: "fake role".to_string(),
+ permissions: Permissions::empty(),
+ position: 1,
+ };
+ let user = User {
+ id: UserId(6),
+ avatar: None,
+ bot: false,
+ discriminator: 4132,
+ name: "fake".to_string(),
+ };
+ let member = Member {
+ deaf: false,
+ guild_id: GuildId(2),
+ joined_at: None,
+ mute: false,
+ nick: None,
+ roles: vec![],
+ user: Arc::new(RwLock::new(user.clone())),
+ };
+
+ assert_eq!(ChannelId(1).mention(), "<#1>");
+ assert_eq!(channel.mention(), "<#4>");
+ assert_eq!(emoji.mention(), "<:a:5>");
+ assert_eq!(member.mention(), "<@6>");
+ assert_eq!(role.mention(), "<@&2>");
+ assert_eq!(role.id.mention(), "<@&2>");
+ assert_eq!(user.mention(), "<@6>");
+ assert_eq!(user.id.mention(), "<@6>");
+ }
+}
diff --git a/src/model/user.rs b/src/model/user.rs
index a378be5..d76b135 100644
--- a/src/model/user.rs
+++ b/src/model/user.rs
@@ -738,9 +738,10 @@ impl UserId {
#[cfg(feature = "cache")]
pub fn find(&self) -> Option<Arc<RwLock<User>>> { CACHE.read().user(*self) }
- /// Gets a user by its Id over the REST API.
+ /// Gets a user by its Id from either the cache or the REST API.
///
- /// **Note**: The current user must be a bot user.
+ /// Searches the cache for the user first, if the cache is enabled. If the
+ /// user was not found, then the user is searched via the REST API.
#[inline]
pub fn get(&self) -> Result<User> {
#[cfg(feature = "cache")]
@@ -843,3 +844,71 @@ fn tag(name: &str, discriminator: u16) -> String {
tag
}
+
+#[cfg(test)]
+mod test {
+ #[cfg(feature = "model")]
+ mod model {
+ use model::id::UserId;
+ use model::user::User;
+
+ fn gen() -> User {
+ User {
+ id: UserId(210),
+ avatar: Some("abc".to_string()),
+ bot: true,
+ discriminator: 1432,
+ name: "test".to_string(),
+ }
+ }
+
+ #[test]
+ fn test_core() {
+ let mut user = gen();
+
+ assert!(
+ user.avatar_url()
+ .unwrap()
+ .ends_with("/avatars/210/abc.webp?size=1024")
+ );
+ assert!(
+ user.static_avatar_url()
+ .unwrap()
+ .ends_with("/avatars/210/abc.webp?size=1024")
+ );
+
+ user.avatar = Some("a_aaa".to_string());
+ assert!(
+ user.avatar_url()
+ .unwrap()
+ .ends_with("/avatars/210/a_aaa.gif?size=1024")
+ );
+ assert!(
+ user.static_avatar_url()
+ .unwrap()
+ .ends_with("/avatars/210/a_aaa.webp?size=1024")
+ );
+
+ user.avatar = None;
+ assert!(user.avatar_url().is_none());
+
+ assert_eq!(user.tag(), "test#1432");
+ }
+
+ #[test]
+ fn default_avatars() {
+ let mut user = gen();
+
+ user.discriminator = 0;
+ assert!(user.default_avatar_url().ends_with("0.png"));
+ user.discriminator = 1;
+ assert!(user.default_avatar_url().ends_with("1.png"));
+ user.discriminator = 2;
+ assert!(user.default_avatar_url().ends_with("2.png"));
+ user.discriminator = 3;
+ assert!(user.default_avatar_url().ends_with("3.png"));
+ user.discriminator = 4;
+ assert!(user.default_avatar_url().ends_with("4.png"));
+ }
+ }
+}
diff --git a/src/utils/colour.rs b/src/utils/colour.rs
index b5c2e05..54e74b0 100644
--- a/src/utils/colour.rs
+++ b/src/utils/colour.rs
@@ -323,3 +323,55 @@ impl Default for Colour {
/// Creates a default value for a `Colour`, setting the inner value to `0`.
fn default() -> Colour { Colour(0) }
}
+
+#[cfg(test)]
+mod test {
+ use super::Colour;
+ use std::u32;
+
+ #[test]
+ fn new() {
+ assert_eq!(Colour::new(1).0, 1);
+ assert_eq!(Colour::new(u32::MIN).0, u32::MIN);
+ assert_eq!(Colour::new(u32::MAX).0, u32::MAX);
+ }
+
+ #[test]
+ fn from_rgb() {
+ assert_eq!(Colour::from_rgb(255, 0, 0).0, 0xFF0000);
+ assert_eq!(Colour::from_rgb(0, 255, 0).0, 0x00FF00);
+ assert_eq!(Colour::from_rgb(0, 0, 255).0, 0x0000FF);
+ }
+
+ #[test]
+ fn r() {
+ assert_eq!(Colour::new(0x336123).r(), 0x33);
+ }
+
+ #[test]
+ fn g() {
+ assert_eq!(Colour::new(0x336123).g(), 0x61);
+ }
+
+ #[test]
+ fn b() {
+ assert_eq!(Colour::new(0x336123).b(), 0x23);
+ }
+
+ #[test]
+ fn tuple() {
+ assert_eq!(Colour::new(0x336123).tuple(), (0x33, 0x61, 0x23));
+ }
+
+ #[test]
+ fn default() {
+ assert_eq!(Colour::default().0, 0);
+ }
+
+ #[test]
+ fn from() {
+ assert_eq!(Colour::from(7i32).0, 7);
+ assert_eq!(Colour::from(7u32).0, 7);
+ assert_eq!(Colour::from(7u64).0, 7);
+ }
+}
diff --git a/src/utils/message_builder.rs b/src/utils/message_builder.rs
index 7bbfddf..c609c28 100644
--- a/src/utils/message_builder.rs
+++ b/src/utils/message_builder.rs
@@ -988,3 +988,81 @@ fn normalize(text: &str) -> String {
.replace("@everyone", "@\u{200B}everyone")
.replace("@here", "@\u{200B}here")
}
+
+#[cfg(test)]
+mod test {
+ use model::prelude::*;
+ use super::{
+ ContentModifier::*,
+ MessageBuilder,
+ };
+
+ #[test]
+ fn code_blocks() {
+ let content = MessageBuilder::new()
+ .push_codeblock("test", Some("rb"))
+ .build();
+ assert_eq!(content, "```rb\ntest\n```");
+ }
+
+ #[test]
+ fn safe_content() {
+ let content = MessageBuilder::new()
+ .push_safe("@everyone discord.gg/discord-api")
+ .build();
+ assert_ne!(content, "@everyone discord.gg/discord-api");
+ }
+
+ #[test]
+ fn no_free_formatting() {
+ let content = MessageBuilder::new().push_bold_safe("test**test").build();
+ assert_ne!(content, "**test**test**");
+ }
+
+ #[test]
+ fn mentions() {
+ let content_emoji = MessageBuilder::new()
+ .emoji(&Emoji {
+ animated: false,
+ id: EmojiId(32),
+ name: "Rohrkatze".to_string(),
+ managed: false,
+ require_colons: true,
+ roles: vec![],
+ })
+ .build();
+ let content_mentions = MessageBuilder::new()
+ .channel(1)
+ .mention(&UserId(2))
+ .role(3)
+ .user(4)
+ .build();
+ assert_eq!(content_mentions, "<#1><@2><@&3><@4>");
+ assert_eq!(content_emoji, "<:Rohrkatze:32>");
+ }
+
+ #[test]
+ fn content() {
+ let content = Bold + Italic + Code + "Fun!";
+
+ assert_eq!(content.to_string(), "***`Fun!`***");
+ }
+
+ #[test]
+ fn message_content() {
+ let message_content = MessageBuilder::new()
+ .push(Bold + Italic + Code + "Fun!")
+ .build();
+
+ assert_eq!(message_content, "***`Fun!`***");
+ }
+
+ #[test]
+ fn message_content_safe() {
+ let message_content = MessageBuilder::new()
+ .push_safe(Bold + Italic + "test**test")
+ .build();
+
+ assert_eq!(message_content, "***test\\*\\*test***");
+ }
+}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index e20dd22..6c84384 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -500,3 +500,54 @@ pub fn with_cache_mut<T, F>(mut f: F) -> T
let mut cache = CACHE.write();
f(&mut cache)
}
+
+#[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");
+ }
+
+ #[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"]);
+ }
+
+ #[test]
+ fn test_is_nsfw() {
+ assert!(!is_nsfw("general"));
+ assert!(is_nsfw("nsfw"));
+ assert!(is_nsfw("nsfw-test"));
+ assert!(!is_nsfw("nsfw-"));
+ assert!(!is_nsfw("général"));
+ assert!(is_nsfw("nsfw-général"));
+ }
+}