1pub(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#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
36pub enum IRCParseError {
37 #[error("No space found after tags (no command/prefix)")]
39 NoSpaceAfterTags,
40 #[error("No tags after @ sign")]
42 EmptyTagsDeclaration,
43 #[error("No space found after prefix (no command)")]
45 NoSpaceAfterPrefix,
46 #[error("No tags after : sign")]
48 EmptyPrefixDeclaration,
49 #[error("Expected command to only consist of alphabetic or numeric characters")]
51 MalformedCommand,
52 #[error("Expected only single spaces between middle parameters")]
54 TooManySpacesInMiddleParams,
55 #[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
68pub trait AsRawIRC {
70 fn format_as_raw_irc(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
72 fn as_raw_irc(&self) -> String
81 where
82 Self: Sized,
83 {
84 format!("{}", RawIRCDisplay(self))
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
94#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
95pub struct IRCMessage {
96 pub tags: IRCTags,
98 pub prefix: Option<IRCPrefix>,
101 pub command: String,
103 pub params: Vec<String>,
109}
110
111#[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 #[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 #[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 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 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 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 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 write!(f, " {param}")?;
284 } else {
285 write!(f, " :{param}")?;
287 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 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}