twitch_irc/message/
mod.rs

1//! Generic and Twitch-specific IRC messages.
2
3pub(crate) mod commands;
4pub(crate) mod prefix;
5pub(crate) mod tags;
6pub(crate) mod twitch;
7
8pub use commands::clearchat::{ClearChatAction, ClearChatMessage};
9pub use commands::clearmsg::ClearMsgMessage;
10pub use commands::globaluserstate::GlobalUserStateMessage;
11pub use commands::join::JoinMessage;
12pub use commands::notice::NoticeMessage;
13pub use commands::part::PartMessage;
14pub use commands::ping::PingMessage;
15pub use commands::pong::PongMessage;
16pub use commands::privmsg::PrivmsgMessage;
17pub use commands::reconnect::ReconnectMessage;
18pub use commands::roomstate::{FollowersOnlyMode, RoomStateMessage};
19pub use commands::usernotice::{SubGiftPromo, UserNoticeEvent, UserNoticeMessage};
20pub use commands::userstate::UserStateMessage;
21pub use commands::whisper::WhisperMessage;
22pub use commands::{ServerMessage, ServerMessageParseError};
23pub use prefix::IRCPrefix;
24pub use tags::IRCTags;
25pub use twitch::*;
26
27use std::fmt;
28use std::fmt::Write;
29use thiserror::Error;
30
31#[cfg(feature = "with-serde")]
32use {serde::Deserialize, serde::Serialize};
33
34/// Error while parsing a string into an `IRCMessage`.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
36pub enum IRCParseError {
37    /// No space found after tags (no command/prefix)
38    #[error("No space found after tags (no command/prefix)")]
39    NoSpaceAfterTags,
40    /// No tags after @ sign
41    #[error("No tags after @ sign")]
42    EmptyTagsDeclaration,
43    /// No space found after prefix (no command)
44    #[error("No space found after prefix (no command)")]
45    NoSpaceAfterPrefix,
46    /// No tags after : sign
47    #[error("No tags after : sign")]
48    EmptyPrefixDeclaration,
49    /// Expected command to only consist of alphabetic or numeric characters
50    #[error("Expected command to only consist of alphabetic or numeric characters")]
51    MalformedCommand,
52    /// Expected only single spaces between middle parameters
53    #[error("Expected only single spaces between middle parameters")]
54    TooManySpacesInMiddleParams,
55    /// Newlines are not permitted in raw IRC messages
56    #[error("Newlines are not permitted in raw IRC messages")]
57    NewlinesInMessage,
58}
59
60struct RawIRCDisplay<'a, T: AsRawIRC>(&'a T);
61
62impl<'a, T: AsRawIRC> fmt::Display for RawIRCDisplay<'a, T> {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        self.0.format_as_raw_irc(f)
65    }
66}
67
68/// Anything that can be converted into the raw IRC wire format.
69pub trait AsRawIRC {
70    /// Writes the raw IRC message to the given formatter.
71    fn format_as_raw_irc(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
72    /// Creates a new string with the raw IRC message.
73    ///
74    /// The resulting output string is guaranteed to parse to the same value it was created from,
75    /// but due to protocol ambiguity it is not guaranteed to be identical to the input
76    /// the value was parsed from (if it was parsed at all).
77    ///
78    /// For example, the order of tags might differ, or the use of trailing parameters
79    /// might be different.
80    fn as_raw_irc(&self) -> String
81    where
82        Self: Sized,
83    {
84        format!("{}", RawIRCDisplay(self))
85    }
86}
87
88/// A protocol-level IRC message, with arbitrary command, parameters, tags and prefix.
89///
90/// See [RFC 2812, section 2.3.1](https://tools.ietf.org/html/rfc2812#section-2.3.1)
91/// for the message format that this is based on.
92/// Further, this implements [IRCv3 tags](https://ircv3.net/specs/extensions/message-tags.html).
93#[derive(Debug, Clone, PartialEq, Eq)]
94#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
95pub struct IRCMessage {
96    /// A map of additional key-value tags on this message.
97    pub tags: IRCTags,
98    /// The "prefix" of this message, as defined by RFC 2812. Typically specifies the sending
99    /// server and/or user.
100    pub prefix: Option<IRCPrefix>,
101    /// A command like `PRIVMSG` or `001` (see RFC 2812 for the definition).
102    pub command: String,
103    /// A list of parameters on this IRC message. See RFC 2812 for the definition.
104    ///
105    /// Middle parameters and trailing parameters are treated the same here, and as long as
106    /// there are no spaces in the last parameter, there is no way to tell if that parameter
107    /// was a middle or trailing parameter when it was parsed.
108    pub params: Vec<String>,
109}
110
111/// Allows quick creation of simple IRC messages using a command and optional parameters.
112///
113/// The given command and parameters have to implement `From<T> for String` if they are not
114/// already of type `String`.
115///
116/// # Example
117///
118/// ```
119/// use twitch_irc::irc;
120/// use twitch_irc::message::AsRawIRC;
121///
122/// # fn main() {
123/// let msg = irc!["PRIVMSG", "#sodapoppin", "Hello guys!"];
124///
125/// assert_eq!(msg.command, "PRIVMSG");
126/// assert_eq!(msg.params, vec!["#sodapoppin".to_owned(), "Hello guys!".to_owned()]);
127/// assert_eq!(msg.as_raw_irc(), "PRIVMSG #sodapoppin :Hello guys!");
128/// # }
129/// ```
130#[macro_export]
131macro_rules! irc {
132    (@replace_expr $_t:tt $sub:expr) => {
133        $sub
134    };
135    (@count_exprs $($expression:expr),*) => {
136        0usize $(+ irc!(@replace_expr $expression 1usize))*
137    };
138    ($command:expr $(, $argument:expr )* ) => {
139        {
140            let capacity = irc!(@count_exprs $($argument),*);
141            #[allow(unused_mut)]
142            let mut temp_vec: ::std::vec::Vec<String> = ::std::vec::Vec::with_capacity(capacity);
143            $(
144                temp_vec.push(::std::string::String::from($argument));
145            )*
146            $crate::message::IRCMessage::new_simple(::std::string::String::from($command), temp_vec)
147        }
148    };
149}
150
151impl IRCMessage {
152    /// Create a new `IRCMessage` with just a command and parameters, similar to the
153    /// `irc!` macro.
154    pub fn new_simple(command: String, params: Vec<String>) -> IRCMessage {
155        IRCMessage {
156            tags: IRCTags::new(),
157            prefix: None,
158            command,
159            params,
160        }
161    }
162
163    /// Create a new `IRCMessage` by specifying all fields.
164    pub fn new(
165        tags: IRCTags,
166        prefix: Option<IRCPrefix>,
167        command: String,
168        params: Vec<String>,
169    ) -> IRCMessage {
170        IRCMessage {
171            tags,
172            prefix,
173            command,
174            params,
175        }
176    }
177
178    /// Parse a raw IRC wire-format message into an `IRCMessage`. `source` should be specified
179    /// without trailing newline character(s).
180    pub fn parse(mut source: &str) -> Result<IRCMessage, IRCParseError> {
181        if source.chars().any(|c| c == '\r' || c == '\n') {
182            return Err(IRCParseError::NewlinesInMessage);
183        }
184
185        let tags = if source.starts_with('@') {
186            // str[1..] removes the leading @ sign
187            let (tags_part, remainder) = source[1..]
188                .split_once(' ')
189                .ok_or(IRCParseError::NoSpaceAfterTags)?;
190            source = remainder;
191
192            if tags_part.is_empty() {
193                return Err(IRCParseError::EmptyTagsDeclaration);
194            }
195
196            IRCTags::parse(tags_part)
197        } else {
198            IRCTags::new()
199        };
200
201        let prefix = if source.starts_with(':') {
202            // str[1..] removes the leading : sign
203            let (prefix_part, remainder) = source[1..]
204                .split_once(' ')
205                .ok_or(IRCParseError::NoSpaceAfterPrefix)?;
206            source = remainder;
207
208            if prefix_part.is_empty() {
209                return Err(IRCParseError::EmptyPrefixDeclaration);
210            }
211
212            Some(IRCPrefix::parse(prefix_part))
213        } else {
214            None
215        };
216
217        let mut command_split = source.splitn(2, ' ');
218        let mut command = command_split.next().unwrap().to_owned();
219        command.make_ascii_uppercase();
220
221        if command.is_empty()
222            || !command.chars().all(|c| c.is_ascii_alphabetic())
223                && !command.chars().all(|c| c.is_ascii() && c.is_numeric())
224        {
225            return Err(IRCParseError::MalformedCommand);
226        }
227
228        let mut params;
229        if let Some(params_part) = command_split.next() {
230            params = vec![];
231
232            let mut rest = Some(params_part);
233            while let Some(rest_str) = rest {
234                if let Some(sub_str) = rest_str.strip_prefix(':') {
235                    // trailing param, remove : and consume the rest of the input
236                    params.push(sub_str.to_owned());
237                    rest = None;
238                } else {
239                    let mut split = rest_str.splitn(2, ' ');
240                    let param = split.next().unwrap();
241                    rest = split.next();
242
243                    if param.is_empty() {
244                        return Err(IRCParseError::TooManySpacesInMiddleParams);
245                    }
246                    params.push(param.to_owned());
247                }
248            }
249        } else {
250            params = vec![];
251        };
252
253        Ok(IRCMessage {
254            tags,
255            prefix,
256            command,
257            params,
258        })
259    }
260}
261
262impl AsRawIRC for IRCMessage {
263    fn format_as_raw_irc(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        if !self.tags.0.is_empty() {
265            f.write_char('@')?;
266            self.tags.format_as_raw_irc(f)?;
267            f.write_char(' ')?;
268        }
269
270        if let Some(prefix) = &self.prefix {
271            f.write_char(':')?;
272            prefix.format_as_raw_irc(f)?;
273            f.write_char(' ')?;
274        }
275
276        f.write_str(&self.command)?;
277
278        for param in self.params.iter() {
279            if !param.contains(' ') && !param.is_empty() && !param.starts_with(':') {
280                // middle parameter
281                write!(f, " {}", param)?;
282            } else {
283                // trailing parameter
284                write!(f, " :{}", param)?;
285                // TODO should there be a panic if this is not the last parameter?
286                break;
287            }
288        }
289
290        Ok(())
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use maplit::hashmap;
298
299    #[test]
300    fn test_privmsg() {
301        let source = "@rm-received-ts=1577040815136;historical=1;badge-info=subscriber/16;badges=moderator/1,subscriber/12;color=#19E6E6;display-name=randers;emotes=;flags=;id=6e2ccb1f-01ed-44d0-85b6-edf762524475;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1577040814959;turbo=0;user-id=40286300;user-type=mod :randers!randers@randers.tmi.twitch.tv PRIVMSG #pajlada :Pajapains";
302        let message = IRCMessage::parse(source).unwrap();
303        assert_eq!(
304            message,
305            IRCMessage {
306                tags: IRCTags::from(hashmap! {
307                    "display-name".to_owned() => Some("randers".to_owned()),
308                    "tmi-sent-ts" .to_owned() => Some("1577040814959".to_owned()),
309                    "historical".to_owned() => Some("1".to_owned()),
310                    "room-id".to_owned() => Some("11148817".to_owned()),
311                    "emotes".to_owned() => Some("".to_owned()),
312                    "color".to_owned() => Some("#19E6E6".to_owned()),
313                    "id".to_owned() => Some("6e2ccb1f-01ed-44d0-85b6-edf762524475".to_owned()),
314                    "turbo".to_owned() => Some("0".to_owned()),
315                    "flags".to_owned() => Some("".to_owned()),
316                    "user-id".to_owned() => Some("40286300".to_owned()),
317                    "rm-received-ts".to_owned() => Some("1577040815136".to_owned()),
318                    "user-type".to_owned() => Some("mod".to_owned()),
319                    "subscriber".to_owned() => Some("1".to_owned()),
320                    "badges".to_owned() => Some("moderator/1,subscriber/12".to_owned()),
321                    "badge-info".to_owned() => Some("subscriber/16".to_owned()),
322                    "mod".to_owned() => Some("1".to_owned()),
323                }),
324                prefix: Some(IRCPrefix::Full {
325                    nick: "randers".to_owned(),
326                    user: Some("randers".to_owned()),
327                    host: Some("randers.tmi.twitch.tv".to_owned()),
328                }),
329                command: "PRIVMSG".to_owned(),
330                params: vec!["#pajlada".to_owned(), "Pajapains".to_owned()],
331            }
332        );
333        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
334    }
335
336    #[test]
337    fn test_confusing_prefix_trailing_param() {
338        let source = ":coolguy foo bar baz asdf";
339        let message = IRCMessage::parse(source).unwrap();
340        assert_eq!(
341            message,
342            IRCMessage {
343                tags: IRCTags::from(hashmap! {}),
344                prefix: Some(IRCPrefix::HostOnly {
345                    host: "coolguy".to_owned()
346                }),
347                command: "FOO".to_owned(),
348                params: vec!["bar".to_owned(), "baz".to_owned(), "asdf".to_owned()],
349            }
350        );
351        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
352    }
353
354    #[test]
355    fn test_pure_irc_1() {
356        let source = "foo bar baz ::asdf";
357        let message = IRCMessage::parse(source).unwrap();
358        assert_eq!(
359            message,
360            IRCMessage {
361                tags: IRCTags::from(hashmap! {}),
362                prefix: None,
363                command: "FOO".to_owned(),
364                params: vec!["bar".to_owned(), "baz".to_owned(), ":asdf".to_owned()],
365            }
366        );
367        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
368    }
369
370    #[test]
371    fn test_pure_irc_2() {
372        let source = ":coolguy foo bar baz :  asdf quux ";
373        let message = IRCMessage::parse(source).unwrap();
374        assert_eq!(
375            message,
376            IRCMessage {
377                tags: IRCTags::from(hashmap! {}),
378                prefix: Some(IRCPrefix::HostOnly {
379                    host: "coolguy".to_owned()
380                }),
381                command: "FOO".to_owned(),
382                params: vec![
383                    "bar".to_owned(),
384                    "baz".to_owned(),
385                    "  asdf quux ".to_owned()
386                ],
387            }
388        );
389        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
390    }
391
392    #[test]
393    fn test_pure_irc_3() {
394        let source = ":coolguy PRIVMSG bar :lol :) ";
395        let message = IRCMessage::parse(source).unwrap();
396        assert_eq!(
397            message,
398            IRCMessage {
399                tags: IRCTags::from(hashmap! {}),
400                prefix: Some(IRCPrefix::HostOnly {
401                    host: "coolguy".to_owned()
402                }),
403                command: "PRIVMSG".to_owned(),
404                params: vec!["bar".to_owned(), "lol :) ".to_owned()],
405            }
406        );
407        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
408    }
409
410    #[test]
411    fn test_pure_irc_4() {
412        let source = ":coolguy foo bar baz :";
413        let message = IRCMessage::parse(source).unwrap();
414        assert_eq!(
415            message,
416            IRCMessage {
417                tags: IRCTags::from(hashmap! {}),
418                prefix: Some(IRCPrefix::HostOnly {
419                    host: "coolguy".to_owned()
420                }),
421                command: "FOO".to_owned(),
422                params: vec!["bar".to_owned(), "baz".to_owned(), "".to_owned()],
423            }
424        );
425        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
426    }
427
428    #[test]
429    fn test_pure_irc_5() {
430        let source = ":coolguy foo bar baz :  ";
431        let message = IRCMessage::parse(source).unwrap();
432        assert_eq!(
433            message,
434            IRCMessage {
435                tags: IRCTags::from(hashmap! {}),
436                prefix: Some(IRCPrefix::HostOnly {
437                    host: "coolguy".to_owned()
438                }),
439                command: "FOO".to_owned(),
440                params: vec!["bar".to_owned(), "baz".to_owned(), "  ".to_owned()],
441            }
442        );
443        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
444    }
445
446    #[test]
447    fn test_pure_irc_6() {
448        let source = "@a=b;c=32;k;rt=ql7 foo";
449        let message = IRCMessage::parse(source).unwrap();
450        assert_eq!(
451            message,
452            IRCMessage {
453                tags: IRCTags::from(hashmap! {
454                    "a".to_owned() => Some("b".to_owned()),
455                    "c".to_owned() => Some("32".to_owned()),
456                    "k".to_owned() => None,
457                    "rt".to_owned() => Some("ql7".to_owned())
458                }),
459                prefix: None,
460                command: "FOO".to_owned(),
461                params: vec![],
462            }
463        );
464        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
465    }
466
467    #[test]
468    fn test_pure_irc_7() {
469        let source = "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo";
470        let message = IRCMessage::parse(source).unwrap();
471        assert_eq!(
472            message,
473            IRCMessage {
474                tags: IRCTags::from(hashmap! {
475                    "a".to_owned() => Some("b\\and\nk".to_owned()),
476                    "c".to_owned() => Some("72 45".to_owned()),
477                    "d".to_owned() => Some("gh;764".to_owned()),
478                }),
479                prefix: None,
480                command: "FOO".to_owned(),
481                params: vec![],
482            }
483        );
484        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
485    }
486
487    #[test]
488    fn test_pure_irc_8() {
489        let source = "@c;h=;a=b :quux ab cd";
490        let message = IRCMessage::parse(source).unwrap();
491        assert_eq!(
492            message,
493            IRCMessage {
494                tags: IRCTags::from(hashmap! {
495                    "c".to_owned() => None,
496                    "h".to_owned() => Some("".to_owned()),
497                    "a".to_owned() => Some("b".to_owned()),
498                }),
499                prefix: Some(IRCPrefix::HostOnly {
500                    host: "quux".to_owned()
501                }),
502                command: "AB".to_owned(),
503                params: vec!["cd".to_owned()],
504            }
505        );
506        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
507    }
508
509    #[test]
510    fn test_join_1() {
511        let source = ":src JOIN #chan";
512        let message = IRCMessage::parse(source).unwrap();
513        assert_eq!(
514            message,
515            IRCMessage {
516                tags: IRCTags::from(hashmap! {}),
517                prefix: Some(IRCPrefix::HostOnly {
518                    host: "src".to_owned()
519                }),
520                command: "JOIN".to_owned(),
521                params: vec!["#chan".to_owned()],
522            }
523        );
524        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
525    }
526
527    #[test]
528    fn test_join_2() {
529        assert_eq!(
530            IRCMessage::parse(":src JOIN #chan"),
531            IRCMessage::parse(":src JOIN :#chan"),
532        )
533    }
534
535    #[test]
536    fn test_away_1() {
537        let source = ":src AWAY";
538        let message = IRCMessage::parse(source).unwrap();
539        assert_eq!(
540            message,
541            IRCMessage {
542                tags: IRCTags::from(hashmap! {}),
543                prefix: Some(IRCPrefix::HostOnly {
544                    host: "src".to_owned()
545                }),
546                command: "AWAY".to_owned(),
547                params: vec![],
548            }
549        );
550        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
551    }
552
553    #[test]
554    fn test_away_2() {
555        let source = ":cool\tguy foo bar baz";
556        let message = IRCMessage::parse(source).unwrap();
557        assert_eq!(
558            message,
559            IRCMessage {
560                tags: IRCTags::from(hashmap! {}),
561                prefix: Some(IRCPrefix::HostOnly {
562                    host: "cool\tguy".to_owned()
563                }),
564                command: "FOO".to_owned(),
565                params: vec!["bar".to_owned(), "baz".to_owned()],
566            }
567        );
568        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
569    }
570
571    #[test]
572    fn test_complex_prefix() {
573        let source = ":coolguy!~ag@n\u{0002}et\u{0003}05w\u{000f}ork.admin PRIVMSG foo :bar baz";
574        let message = IRCMessage::parse(source).unwrap();
575        assert_eq!(
576            message,
577            IRCMessage {
578                tags: IRCTags::from(hashmap! {}),
579                prefix: Some(IRCPrefix::Full {
580                    nick: "coolguy".to_owned(),
581                    user: Some("~ag".to_owned()),
582                    host: Some("n\u{0002}et\u{0003}05w\u{000f}ork.admin".to_owned())
583                }),
584                command: "PRIVMSG".to_owned(),
585                params: vec!["foo".to_owned(), "bar baz".to_owned()],
586            }
587        );
588        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
589    }
590
591    #[test]
592    fn test_vendor_tags() {
593        let source = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 :irc.example.com COMMAND param1 param2 :param3 param3";
594        let message = IRCMessage::parse(source).unwrap();
595        assert_eq!(
596            message,
597            IRCMessage {
598                tags: IRCTags::from(hashmap! {
599                    "tag1".to_owned() => Some("value1".to_owned()),
600                    "tag2".to_owned() => None,
601                    "vendor1/tag3".to_owned() => Some("value2".to_owned()),
602                    "vendor2/tag4".to_owned() => None
603                }),
604                prefix: Some(IRCPrefix::HostOnly {
605                    host: "irc.example.com".to_owned()
606                }),
607                command: "COMMAND".to_owned(),
608                params: vec![
609                    "param1".to_owned(),
610                    "param2".to_owned(),
611                    "param3 param3".to_owned()
612                ],
613            }
614        );
615        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
616    }
617
618    #[test]
619    fn test_asian_characters_display_name() {
620        let source = "@display-name=테스트계정420 :tmi.twitch.tv PRIVMSG #pajlada :test";
621        let message = IRCMessage::parse(source).unwrap();
622        assert_eq!(
623            message,
624            IRCMessage {
625                tags: IRCTags::from(hashmap! {
626                    "display-name".to_owned() => Some("테스트계정420".to_owned()),
627                }),
628                prefix: Some(IRCPrefix::HostOnly {
629                    host: "tmi.twitch.tv".to_owned()
630                }),
631                command: "PRIVMSG".to_owned(),
632                params: vec!["#pajlada".to_owned(), "test".to_owned(),],
633            }
634        );
635        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
636    }
637
638    #[test]
639    fn test_ping_1() {
640        let source = "PING :tmi.twitch.tv";
641        let message = IRCMessage::parse(source).unwrap();
642        assert_eq!(
643            message,
644            IRCMessage {
645                tags: IRCTags::from(hashmap! {}),
646                prefix: None,
647                command: "PING".to_owned(),
648                params: vec!["tmi.twitch.tv".to_owned()],
649            }
650        );
651        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
652    }
653
654    #[test]
655    fn test_ping_2() {
656        let source = ":tmi.twitch.tv PING";
657        let message = IRCMessage::parse(source).unwrap();
658        assert_eq!(
659            message,
660            IRCMessage {
661                tags: IRCTags::from(hashmap! {}),
662                prefix: Some(IRCPrefix::HostOnly {
663                    host: "tmi.twitch.tv".to_owned()
664                }),
665                command: "PING".to_owned(),
666                params: vec![],
667            }
668        );
669        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
670    }
671
672    #[test]
673    fn test_invalid_empty_tags() {
674        let result = IRCMessage::parse("@ :tmi.twitch.tv TEST");
675        assert_eq!(result, Err(IRCParseError::EmptyTagsDeclaration))
676    }
677
678    #[test]
679    fn test_invalid_nothing_after_tags() {
680        let result = IRCMessage::parse("@key=value");
681        assert_eq!(result, Err(IRCParseError::NoSpaceAfterTags))
682    }
683
684    #[test]
685    fn test_invalid_empty_prefix() {
686        let result = IRCMessage::parse("@key=value : TEST");
687        assert_eq!(result, Err(IRCParseError::EmptyPrefixDeclaration))
688    }
689
690    #[test]
691    fn test_invalid_nothing_after_prefix() {
692        let result = IRCMessage::parse("@key=value :tmi.twitch.tv");
693        assert_eq!(result, Err(IRCParseError::NoSpaceAfterPrefix))
694    }
695
696    #[test]
697    fn test_invalid_spaces_at_start_of_line() {
698        let result = IRCMessage::parse(" @key=value :tmi.twitch.tv PING");
699        assert_eq!(result, Err(IRCParseError::MalformedCommand))
700    }
701
702    #[test]
703    fn test_invalid_empty_command_1() {
704        let result = IRCMessage::parse("@key=value :tmi.twitch.tv ");
705        assert_eq!(result, Err(IRCParseError::MalformedCommand))
706    }
707
708    #[test]
709    fn test_invalid_empty_command_2() {
710        let result = IRCMessage::parse("");
711        assert_eq!(result, Err(IRCParseError::MalformedCommand))
712    }
713
714    #[test]
715    fn test_invalid_command_1() {
716        let result = IRCMessage::parse("@key=value :tmi.twitch.tv  PING");
717        assert_eq!(result, Err(IRCParseError::MalformedCommand))
718    }
719
720    #[test]
721    fn test_invalid_command_2() {
722        let result = IRCMessage::parse("@key=value :tmi.twitch.tv P!NG");
723        assert_eq!(result, Err(IRCParseError::MalformedCommand))
724    }
725
726    #[test]
727    fn test_invalid_command_3() {
728        let result = IRCMessage::parse("@key=value :tmi.twitch.tv PØNG");
729        assert_eq!(result, Err(IRCParseError::MalformedCommand))
730    }
731
732    #[test]
733    fn test_invalid_command_4() {
734        // mix of ascii numeric and ascii alphabetic
735        let result = IRCMessage::parse("@key=value :tmi.twitch.tv P1NG");
736        assert_eq!(result, Err(IRCParseError::MalformedCommand))
737    }
738
739    #[test]
740    fn test_invalid_middle_params_space_after_command() {
741        let result = IRCMessage::parse("@key=value :tmi.twitch.tv PING ");
742        assert_eq!(result, Err(IRCParseError::TooManySpacesInMiddleParams))
743    }
744
745    #[test]
746    fn test_invalid_middle_params_too_many_spaces_between_params() {
747        let result = IRCMessage::parse("@key=value :tmi.twitch.tv PING asd  def");
748        assert_eq!(result, Err(IRCParseError::TooManySpacesInMiddleParams))
749    }
750
751    #[test]
752    fn test_invalid_middle_params_too_many_spaces_after_command() {
753        let result = IRCMessage::parse("@key=value :tmi.twitch.tv PING  asd def");
754        assert_eq!(result, Err(IRCParseError::TooManySpacesInMiddleParams))
755    }
756
757    #[test]
758    fn test_invalid_middle_params_trailing_space() {
759        let result = IRCMessage::parse("@key=value :tmi.twitch.tv PING asd def ");
760        assert_eq!(result, Err(IRCParseError::TooManySpacesInMiddleParams))
761    }
762
763    #[test]
764    fn test_empty_trailing_param_1() {
765        let source = "PING asd def :";
766        let message = IRCMessage::parse(source).unwrap();
767        assert_eq!(
768            message,
769            IRCMessage {
770                tags: IRCTags::from(hashmap! {}),
771                prefix: None,
772                command: "PING".to_owned(),
773                params: vec!["asd".to_owned(), "def".to_owned(), "".to_owned()],
774            }
775        );
776        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
777    }
778
779    #[test]
780    fn test_empty_trailing_param_2() {
781        let source = "PING :";
782        let message = IRCMessage::parse(source).unwrap();
783        assert_eq!(
784            message,
785            IRCMessage {
786                tags: IRCTags::from(hashmap! {}),
787                prefix: None,
788                command: "PING".to_owned(),
789                params: vec!["".to_owned()],
790            }
791        );
792        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
793    }
794
795    #[test]
796    fn test_numeric_command() {
797        let source = "500 :Internal Server Error";
798        let message = IRCMessage::parse(source).unwrap();
799        assert_eq!(
800            message,
801            IRCMessage {
802                tags: IRCTags::from(hashmap! {}),
803                prefix: None,
804                command: "500".to_owned(),
805                params: vec!["Internal Server Error".to_owned()],
806            }
807        );
808        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
809    }
810
811    #[test]
812    fn test_stringify_pass() {
813        assert_eq!(
814            irc!["PASS", "oauth:9892879487293847"].as_raw_irc(),
815            "PASS oauth:9892879487293847"
816        );
817    }
818
819    #[test]
820    fn test_newline_in_source() {
821        assert_eq!(
822            IRCMessage::parse("abc\ndef"),
823            Err(IRCParseError::NewlinesInMessage)
824        );
825        assert_eq!(
826            IRCMessage::parse("abc\rdef"),
827            Err(IRCParseError::NewlinesInMessage)
828        );
829        assert_eq!(
830            IRCMessage::parse("abc\n\rdef"),
831            Err(IRCParseError::NewlinesInMessage)
832        );
833    }
834
835    #[test]
836    fn test_lowercase_command() {
837        assert_eq!(IRCMessage::parse("ping").unwrap().command, "PING")
838    }
839
840    #[test]
841    fn test_irc_macro() {
842        assert_eq!(
843            irc!["PRIVMSG"],
844            IRCMessage {
845                tags: IRCTags::new(),
846                prefix: None,
847                command: "PRIVMSG".to_owned(),
848                params: vec![],
849            }
850        );
851        assert_eq!(
852            irc!["PRIVMSG", "#pajlada"],
853            IRCMessage {
854                tags: IRCTags::new(),
855                prefix: None,
856                command: "PRIVMSG".to_owned(),
857                params: vec!["#pajlada".to_owned()],
858            }
859        );
860        assert_eq!(
861            irc!["PRIVMSG", "#pajlada", "LUL xD"],
862            IRCMessage {
863                tags: IRCTags::new(),
864                prefix: None,
865                command: "PRIVMSG".to_owned(),
866                params: vec!["#pajlada".to_owned(), "LUL xD".to_owned()],
867            }
868        );
869    }
870}