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<'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
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 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 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 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 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 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 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 write!(f, " {}", param)?;
282 } else {
283 write!(f, " :{}", param)?;
285 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 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}