Skip to main content

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<T: AsRawIRC> fmt::Display for RawIRCDisplay<'_, 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    #[must_use]
155    pub fn new_simple(command: String, params: Vec<String>) -> IRCMessage {
156        IRCMessage {
157            tags: IRCTags::new(),
158            prefix: None,
159            command,
160            params,
161        }
162    }
163
164    /// Create a new `IRCMessage` by specifying all fields.
165    #[must_use]
166    pub fn new(
167        tags: IRCTags,
168        prefix: Option<IRCPrefix>,
169        command: String,
170        params: Vec<String>,
171    ) -> IRCMessage {
172        IRCMessage {
173            tags,
174            prefix,
175            command,
176            params,
177        }
178    }
179
180    /// Parse a raw IRC wire-format message into an `IRCMessage`. `source` should be specified
181    /// without trailing newline character(s).
182    pub fn parse(mut source: &str) -> Result<IRCMessage, IRCParseError> {
183        if source.chars().any(|c| c == '\r' || c == '\n') {
184            return Err(IRCParseError::NewlinesInMessage);
185        }
186
187        let tags = if source.starts_with('@') {
188            // str[1..] removes the leading @ sign
189            let (tags_part, remainder) = source[1..]
190                .split_once(' ')
191                .ok_or(IRCParseError::NoSpaceAfterTags)?;
192            source = remainder;
193
194            if tags_part.is_empty() {
195                return Err(IRCParseError::EmptyTagsDeclaration);
196            }
197
198            IRCTags::parse(tags_part)
199        } else {
200            IRCTags::new()
201        };
202
203        let prefix = if source.starts_with(':') {
204            // str[1..] removes the leading : sign
205            let (prefix_part, remainder) = source[1..]
206                .split_once(' ')
207                .ok_or(IRCParseError::NoSpaceAfterPrefix)?;
208            source = remainder;
209
210            if prefix_part.is_empty() {
211                return Err(IRCParseError::EmptyPrefixDeclaration);
212            }
213
214            Some(IRCPrefix::parse(prefix_part))
215        } else {
216            None
217        };
218
219        let mut command_split = source.splitn(2, ' ');
220        let mut command = command_split.next().unwrap().to_owned();
221        command.make_ascii_uppercase();
222
223        if command.is_empty()
224            || !command.chars().all(|c| c.is_ascii_alphabetic())
225                && !command.chars().all(|c| c.is_ascii() && c.is_numeric())
226        {
227            return Err(IRCParseError::MalformedCommand);
228        }
229
230        let mut params;
231        if let Some(params_part) = command_split.next() {
232            params = vec![];
233
234            let mut rest = Some(params_part);
235            while let Some(rest_str) = rest {
236                if let Some(sub_str) = rest_str.strip_prefix(':') {
237                    // trailing param, remove : and consume the rest of the input
238                    params.push(sub_str.to_owned());
239                    rest = None;
240                } else {
241                    let mut split = rest_str.splitn(2, ' ');
242                    let param = split.next().unwrap();
243                    rest = split.next();
244
245                    if param.is_empty() {
246                        return Err(IRCParseError::TooManySpacesInMiddleParams);
247                    }
248                    params.push(param.to_owned());
249                }
250            }
251        } else {
252            params = vec![];
253        }
254
255        Ok(IRCMessage {
256            tags,
257            prefix,
258            command,
259            params,
260        })
261    }
262}
263
264impl AsRawIRC for IRCMessage {
265    fn format_as_raw_irc(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        if !self.tags.0.is_empty() {
267            f.write_char('@')?;
268            self.tags.format_as_raw_irc(f)?;
269            f.write_char(' ')?;
270        }
271
272        if let Some(prefix) = &self.prefix {
273            f.write_char(':')?;
274            prefix.format_as_raw_irc(f)?;
275            f.write_char(' ')?;
276        }
277
278        f.write_str(&self.command)?;
279
280        for param in &self.params {
281            if !param.contains(' ') && !param.is_empty() && !param.starts_with(':') {
282                // middle parameter
283                write!(f, " {param}")?;
284            } else {
285                // trailing parameter
286                write!(f, " :{param}")?;
287                // TODO should there be a panic if this is not the last parameter?
288                break;
289            }
290        }
291
292        Ok(())
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use maplit::hashmap;
300
301    #[test]
302    fn test_privmsg() {
303        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";
304        let message = IRCMessage::parse(source).unwrap();
305        assert_eq!(
306            message,
307            IRCMessage {
308                tags: IRCTags::from(hashmap! {
309                    "display-name".to_owned() => "randers".to_owned(),
310                    "tmi-sent-ts" .to_owned() => "1577040814959".to_owned(),
311                    "historical".to_owned() => "1".to_owned(),
312                    "room-id".to_owned() => "11148817".to_owned(),
313                    "emotes".to_owned() => String::new(),
314                    "color".to_owned() => "#19E6E6".to_owned(),
315                    "id".to_owned() => "6e2ccb1f-01ed-44d0-85b6-edf762524475".to_owned(),
316                    "turbo".to_owned() => "0".to_owned(),
317                    "flags".to_owned() => String::new(),
318                    "user-id".to_owned() => "40286300".to_owned(),
319                    "rm-received-ts".to_owned() => "1577040815136".to_owned(),
320                    "user-type".to_owned() => "mod".to_owned(),
321                    "subscriber".to_owned() => "1".to_owned(),
322                    "badges".to_owned() => "moderator/1,subscriber/12".to_owned(),
323                    "badge-info".to_owned() => "subscriber/16".to_owned(),
324                    "mod".to_owned() => "1".to_owned(),
325                }),
326                prefix: Some(IRCPrefix::Full {
327                    nick: "randers".to_owned(),
328                    user: Some("randers".to_owned()),
329                    host: Some("randers.tmi.twitch.tv".to_owned()),
330                }),
331                command: "PRIVMSG".to_owned(),
332                params: vec!["#pajlada".to_owned(), "Pajapains".to_owned()],
333            }
334        );
335        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
336    }
337
338    #[test]
339    fn test_confusing_prefix_trailing_param() {
340        let source = ":coolguy foo bar baz asdf";
341        let message = IRCMessage::parse(source).unwrap();
342        assert_eq!(
343            message,
344            IRCMessage {
345                tags: IRCTags::from(hashmap! {}),
346                prefix: Some(IRCPrefix::HostOnly {
347                    host: "coolguy".to_owned()
348                }),
349                command: "FOO".to_owned(),
350                params: vec!["bar".to_owned(), "baz".to_owned(), "asdf".to_owned()],
351            }
352        );
353        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
354    }
355
356    #[test]
357    fn test_pure_irc_1() {
358        let source = "foo bar baz ::asdf";
359        let message = IRCMessage::parse(source).unwrap();
360        assert_eq!(
361            message,
362            IRCMessage {
363                tags: IRCTags::from(hashmap! {}),
364                prefix: None,
365                command: "FOO".to_owned(),
366                params: vec!["bar".to_owned(), "baz".to_owned(), ":asdf".to_owned()],
367            }
368        );
369        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
370    }
371
372    #[test]
373    fn test_pure_irc_2() {
374        let source = ":coolguy foo bar baz :  asdf quux ";
375        let message = IRCMessage::parse(source).unwrap();
376        assert_eq!(
377            message,
378            IRCMessage {
379                tags: IRCTags::from(hashmap! {}),
380                prefix: Some(IRCPrefix::HostOnly {
381                    host: "coolguy".to_owned()
382                }),
383                command: "FOO".to_owned(),
384                params: vec![
385                    "bar".to_owned(),
386                    "baz".to_owned(),
387                    "  asdf quux ".to_owned()
388                ],
389            }
390        );
391        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
392    }
393
394    #[test]
395    fn test_pure_irc_3() {
396        let source = ":coolguy PRIVMSG bar :lol :) ";
397        let message = IRCMessage::parse(source).unwrap();
398        assert_eq!(
399            message,
400            IRCMessage {
401                tags: IRCTags::from(hashmap! {}),
402                prefix: Some(IRCPrefix::HostOnly {
403                    host: "coolguy".to_owned()
404                }),
405                command: "PRIVMSG".to_owned(),
406                params: vec!["bar".to_owned(), "lol :) ".to_owned()],
407            }
408        );
409        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
410    }
411
412    #[test]
413    fn test_pure_irc_4() {
414        let source = ":coolguy foo bar baz :";
415        let message = IRCMessage::parse(source).unwrap();
416        assert_eq!(
417            message,
418            IRCMessage {
419                tags: IRCTags::from(hashmap! {}),
420                prefix: Some(IRCPrefix::HostOnly {
421                    host: "coolguy".to_owned()
422                }),
423                command: "FOO".to_owned(),
424                params: vec!["bar".to_owned(), "baz".to_owned(), String::new()],
425            }
426        );
427        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
428    }
429
430    #[test]
431    fn test_pure_irc_5() {
432        let source = ":coolguy foo bar baz :  ";
433        let message = IRCMessage::parse(source).unwrap();
434        assert_eq!(
435            message,
436            IRCMessage {
437                tags: IRCTags::from(hashmap! {}),
438                prefix: Some(IRCPrefix::HostOnly {
439                    host: "coolguy".to_owned()
440                }),
441                command: "FOO".to_owned(),
442                params: vec!["bar".to_owned(), "baz".to_owned(), "  ".to_owned()],
443            }
444        );
445        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
446    }
447
448    #[test]
449    fn test_pure_irc_6() {
450        let source = "@a=b;c=32;k;rt=ql7 foo";
451        let message = IRCMessage::parse(source).unwrap();
452        assert_eq!(
453            message,
454            IRCMessage {
455                tags: IRCTags::from(hashmap! {
456                    "a".to_owned() => "b".to_owned(),
457                    "c".to_owned() => "32".to_owned(),
458                    "k".to_owned() => String::new(),
459                    "rt".to_owned() => "ql7".to_owned()
460                }),
461                prefix: None,
462                command: "FOO".to_owned(),
463                params: vec![],
464            }
465        );
466        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
467    }
468
469    #[test]
470    fn test_pure_irc_7() {
471        let source = "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo";
472        let message = IRCMessage::parse(source).unwrap();
473        assert_eq!(
474            message,
475            IRCMessage {
476                tags: IRCTags::from(hashmap! {
477                    "a".to_owned() => "b\\and\nk".to_owned(),
478                    "c".to_owned() => "72 45".to_owned(),
479                    "d".to_owned() => "gh;764".to_owned(),
480                }),
481                prefix: None,
482                command: "FOO".to_owned(),
483                params: vec![],
484            }
485        );
486        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
487    }
488
489    #[test]
490    fn test_pure_irc_8() {
491        let source = "@c;h=;a=b :quux ab cd";
492        let message = IRCMessage::parse(source).unwrap();
493        assert_eq!(
494            message,
495            IRCMessage {
496                tags: IRCTags::from(hashmap! {
497                    "c".to_owned() => String::new(),
498                    "h".to_owned() => String::new(),
499                    "a".to_owned() => "b".to_owned(),
500                }),
501                prefix: Some(IRCPrefix::HostOnly {
502                    host: "quux".to_owned()
503                }),
504                command: "AB".to_owned(),
505                params: vec!["cd".to_owned()],
506            }
507        );
508        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
509    }
510
511    #[test]
512    fn test_join_1() {
513        let source = ":src JOIN #chan";
514        let message = IRCMessage::parse(source).unwrap();
515        assert_eq!(
516            message,
517            IRCMessage {
518                tags: IRCTags::from(hashmap! {}),
519                prefix: Some(IRCPrefix::HostOnly {
520                    host: "src".to_owned()
521                }),
522                command: "JOIN".to_owned(),
523                params: vec!["#chan".to_owned()],
524            }
525        );
526        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
527    }
528
529    #[test]
530    fn test_join_2() {
531        assert_eq!(
532            IRCMessage::parse(":src JOIN #chan"),
533            IRCMessage::parse(":src JOIN :#chan"),
534        );
535    }
536
537    #[test]
538    fn test_away_1() {
539        let source = ":src AWAY";
540        let message = IRCMessage::parse(source).unwrap();
541        assert_eq!(
542            message,
543            IRCMessage {
544                tags: IRCTags::from(hashmap! {}),
545                prefix: Some(IRCPrefix::HostOnly {
546                    host: "src".to_owned()
547                }),
548                command: "AWAY".to_owned(),
549                params: vec![],
550            }
551        );
552        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
553    }
554
555    #[test]
556    fn test_away_2() {
557        let source = ":cool\tguy foo bar baz";
558        let message = IRCMessage::parse(source).unwrap();
559        assert_eq!(
560            message,
561            IRCMessage {
562                tags: IRCTags::from(hashmap! {}),
563                prefix: Some(IRCPrefix::HostOnly {
564                    host: "cool\tguy".to_owned()
565                }),
566                command: "FOO".to_owned(),
567                params: vec!["bar".to_owned(), "baz".to_owned()],
568            }
569        );
570        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
571    }
572
573    #[test]
574    fn test_complex_prefix() {
575        let source = ":coolguy!~ag@n\u{0002}et\u{0003}05w\u{000f}ork.admin PRIVMSG foo :bar baz";
576        let message = IRCMessage::parse(source).unwrap();
577        assert_eq!(
578            message,
579            IRCMessage {
580                tags: IRCTags::from(hashmap! {}),
581                prefix: Some(IRCPrefix::Full {
582                    nick: "coolguy".to_owned(),
583                    user: Some("~ag".to_owned()),
584                    host: Some("n\u{0002}et\u{0003}05w\u{000f}ork.admin".to_owned())
585                }),
586                command: "PRIVMSG".to_owned(),
587                params: vec!["foo".to_owned(), "bar baz".to_owned()],
588            }
589        );
590        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
591    }
592
593    #[test]
594    fn test_vendor_tags() {
595        let source = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 :irc.example.com COMMAND param1 param2 :param3 param3";
596        let message = IRCMessage::parse(source).unwrap();
597        assert_eq!(
598            message,
599            IRCMessage {
600                tags: IRCTags::from(hashmap! {
601                    "tag1".to_owned() => "value1".to_owned(),
602                    "tag2".to_owned() => String::new(),
603                    "vendor1/tag3".to_owned() => "value2".to_owned(),
604                    "vendor2/tag4".to_owned() => String::new()
605                }),
606                prefix: Some(IRCPrefix::HostOnly {
607                    host: "irc.example.com".to_owned()
608                }),
609                command: "COMMAND".to_owned(),
610                params: vec![
611                    "param1".to_owned(),
612                    "param2".to_owned(),
613                    "param3 param3".to_owned()
614                ],
615            }
616        );
617        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
618    }
619
620    #[test]
621    fn test_asian_characters_display_name() {
622        let source = "@display-name=테스트계정420 :tmi.twitch.tv PRIVMSG #pajlada :test";
623        let message = IRCMessage::parse(source).unwrap();
624        assert_eq!(
625            message,
626            IRCMessage {
627                tags: IRCTags::from(hashmap! {
628                    "display-name".to_owned() => "테스트계정420".to_owned(),
629                }),
630                prefix: Some(IRCPrefix::HostOnly {
631                    host: "tmi.twitch.tv".to_owned()
632                }),
633                command: "PRIVMSG".to_owned(),
634                params: vec!["#pajlada".to_owned(), "test".to_owned(),],
635            }
636        );
637        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
638    }
639
640    #[test]
641    fn test_ping_1() {
642        let source = "PING :tmi.twitch.tv";
643        let message = IRCMessage::parse(source).unwrap();
644        assert_eq!(
645            message,
646            IRCMessage {
647                tags: IRCTags::from(hashmap! {}),
648                prefix: None,
649                command: "PING".to_owned(),
650                params: vec!["tmi.twitch.tv".to_owned()],
651            }
652        );
653        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
654    }
655
656    #[test]
657    fn test_ping_2() {
658        let source = ":tmi.twitch.tv PING";
659        let message = IRCMessage::parse(source).unwrap();
660        assert_eq!(
661            message,
662            IRCMessage {
663                tags: IRCTags::from(hashmap! {}),
664                prefix: Some(IRCPrefix::HostOnly {
665                    host: "tmi.twitch.tv".to_owned()
666                }),
667                command: "PING".to_owned(),
668                params: vec![],
669            }
670        );
671        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
672    }
673
674    #[test]
675    fn test_invalid_empty_tags() {
676        let result = IRCMessage::parse("@ :tmi.twitch.tv TEST");
677        assert_eq!(result, Err(IRCParseError::EmptyTagsDeclaration));
678    }
679
680    #[test]
681    fn test_invalid_nothing_after_tags() {
682        let result = IRCMessage::parse("@key=value");
683        assert_eq!(result, Err(IRCParseError::NoSpaceAfterTags));
684    }
685
686    #[test]
687    fn test_invalid_empty_prefix() {
688        let result = IRCMessage::parse("@key=value : TEST");
689        assert_eq!(result, Err(IRCParseError::EmptyPrefixDeclaration));
690    }
691
692    #[test]
693    fn test_invalid_nothing_after_prefix() {
694        let result = IRCMessage::parse("@key=value :tmi.twitch.tv");
695        assert_eq!(result, Err(IRCParseError::NoSpaceAfterPrefix));
696    }
697
698    #[test]
699    fn test_invalid_spaces_at_start_of_line() {
700        let result = IRCMessage::parse(" @key=value :tmi.twitch.tv PING");
701        assert_eq!(result, Err(IRCParseError::MalformedCommand));
702    }
703
704    #[test]
705    fn test_invalid_empty_command_1() {
706        let result = IRCMessage::parse("@key=value :tmi.twitch.tv ");
707        assert_eq!(result, Err(IRCParseError::MalformedCommand));
708    }
709
710    #[test]
711    fn test_invalid_empty_command_2() {
712        let result = IRCMessage::parse("");
713        assert_eq!(result, Err(IRCParseError::MalformedCommand));
714    }
715
716    #[test]
717    fn test_invalid_command_1() {
718        let result = IRCMessage::parse("@key=value :tmi.twitch.tv  PING");
719        assert_eq!(result, Err(IRCParseError::MalformedCommand));
720    }
721
722    #[test]
723    fn test_invalid_command_2() {
724        let result = IRCMessage::parse("@key=value :tmi.twitch.tv P!NG");
725        assert_eq!(result, Err(IRCParseError::MalformedCommand));
726    }
727
728    #[test]
729    fn test_invalid_command_3() {
730        let result = IRCMessage::parse("@key=value :tmi.twitch.tv PØNG");
731        assert_eq!(result, Err(IRCParseError::MalformedCommand));
732    }
733
734    #[test]
735    fn test_invalid_command_4() {
736        // mix of ascii numeric and ascii alphabetic
737        let result = IRCMessage::parse("@key=value :tmi.twitch.tv P1NG");
738        assert_eq!(result, Err(IRCParseError::MalformedCommand));
739    }
740
741    #[test]
742    fn test_invalid_middle_params_space_after_command() {
743        let result = IRCMessage::parse("@key=value :tmi.twitch.tv PING ");
744        assert_eq!(result, Err(IRCParseError::TooManySpacesInMiddleParams));
745    }
746
747    #[test]
748    fn test_invalid_middle_params_too_many_spaces_between_params() {
749        let result = IRCMessage::parse("@key=value :tmi.twitch.tv PING asd  def");
750        assert_eq!(result, Err(IRCParseError::TooManySpacesInMiddleParams));
751    }
752
753    #[test]
754    fn test_invalid_middle_params_too_many_spaces_after_command() {
755        let result = IRCMessage::parse("@key=value :tmi.twitch.tv PING  asd def");
756        assert_eq!(result, Err(IRCParseError::TooManySpacesInMiddleParams));
757    }
758
759    #[test]
760    fn test_invalid_middle_params_trailing_space() {
761        let result = IRCMessage::parse("@key=value :tmi.twitch.tv PING asd def ");
762        assert_eq!(result, Err(IRCParseError::TooManySpacesInMiddleParams));
763    }
764
765    #[test]
766    fn test_empty_trailing_param_1() {
767        let source = "PING asd def :";
768        let message = IRCMessage::parse(source).unwrap();
769        assert_eq!(
770            message,
771            IRCMessage {
772                tags: IRCTags::from(hashmap! {}),
773                prefix: None,
774                command: "PING".to_owned(),
775                params: vec!["asd".to_owned(), "def".to_owned(), String::new()],
776            }
777        );
778        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
779    }
780
781    #[test]
782    fn test_empty_trailing_param_2() {
783        let source = "PING :";
784        let message = IRCMessage::parse(source).unwrap();
785        assert_eq!(
786            message,
787            IRCMessage {
788                tags: IRCTags::from(hashmap! {}),
789                prefix: None,
790                command: "PING".to_owned(),
791                params: vec![String::new()],
792            }
793        );
794        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
795    }
796
797    #[test]
798    fn test_numeric_command() {
799        let source = "500 :Internal Server Error";
800        let message = IRCMessage::parse(source).unwrap();
801        assert_eq!(
802            message,
803            IRCMessage {
804                tags: IRCTags::from(hashmap! {}),
805                prefix: None,
806                command: "500".to_owned(),
807                params: vec!["Internal Server Error".to_owned()],
808            }
809        );
810        assert_eq!(IRCMessage::parse(&message.as_raw_irc()).unwrap(), message);
811    }
812
813    #[test]
814    fn test_stringify_pass() {
815        assert_eq!(
816            irc!["PASS", "oauth:9892879487293847"].as_raw_irc(),
817            "PASS oauth:9892879487293847"
818        );
819    }
820
821    #[test]
822    fn test_newline_in_source() {
823        assert_eq!(
824            IRCMessage::parse("abc\ndef"),
825            Err(IRCParseError::NewlinesInMessage)
826        );
827        assert_eq!(
828            IRCMessage::parse("abc\rdef"),
829            Err(IRCParseError::NewlinesInMessage)
830        );
831        assert_eq!(
832            IRCMessage::parse("abc\n\rdef"),
833            Err(IRCParseError::NewlinesInMessage)
834        );
835    }
836
837    #[test]
838    fn test_lowercase_command() {
839        assert_eq!(IRCMessage::parse("ping").unwrap().command, "PING");
840    }
841
842    #[test]
843    fn test_irc_macro() {
844        assert_eq!(
845            irc!["PRIVMSG"],
846            IRCMessage {
847                tags: IRCTags::new(),
848                prefix: None,
849                command: "PRIVMSG".to_owned(),
850                params: vec![],
851            }
852        );
853        assert_eq!(
854            irc!["PRIVMSG", "#pajlada"],
855            IRCMessage {
856                tags: IRCTags::new(),
857                prefix: None,
858                command: "PRIVMSG".to_owned(),
859                params: vec!["#pajlada".to_owned()],
860            }
861        );
862        assert_eq!(
863            irc!["PRIVMSG", "#pajlada", "LUL xD"],
864            IRCMessage {
865                tags: IRCTags::new(),
866                prefix: None,
867                command: "PRIVMSG".to_owned(),
868                params: vec!["#pajlada".to_owned(), "LUL xD".to_owned()],
869            }
870        );
871    }
872}