irc_proto/
message.rs

1//! A module providing a data structure for messages to and from IRC servers.
2use std::borrow::ToOwned;
3use std::fmt::{Display, Formatter, Result as FmtResult, Write};
4use std::str::FromStr;
5
6use crate::chan::ChannelExt;
7use crate::command::Command;
8use crate::error;
9use crate::error::{MessageParseError, ProtocolError};
10use crate::prefix::Prefix;
11
12/// A data structure representing an IRC message according to the protocol specification. It
13/// consists of a collection of IRCv3 tags, a prefix (describing the source of the message), and
14/// the protocol command. If the command is unknown, it is treated as a special raw command that
15/// consists of a collection of arguments and the special suffix argument. Otherwise, the command
16/// is parsed into a more useful form as described in [`Command`].
17#[derive(Clone, PartialEq, Debug)]
18pub struct Message {
19    /// Message tags as defined by [IRCv3.2](http://ircv3.net/specs/core/message-tags-3.2.html).
20    /// These tags are used to add extended information to the given message, and are commonly used
21    /// in IRCv3 extensions to the IRC protocol.
22    pub tags: Option<Vec<Tag>>,
23    /// The message prefix (or source) as defined by [RFC 2812](http://tools.ietf.org/html/rfc2812).
24    pub prefix: Option<Prefix>,
25    /// The IRC command, parsed according to the known specifications. The command itself and its
26    /// arguments (including the special suffix argument) are captured in this component.
27    pub command: Command,
28}
29
30impl Message {
31    /// Creates a new message from the given components.
32    ///
33    /// # Example
34    /// ```
35    /// # extern crate irc_proto;
36    /// # use irc_proto::Message;
37    /// # fn main() {
38    /// let message = Message::new(
39    ///     Some("nickname!username@hostname"), "JOIN", vec!["#channel"]
40    /// ).unwrap();
41    /// # }
42    /// ```
43    pub fn new(
44        prefix: Option<&str>,
45        command: &str,
46        args: Vec<&str>,
47    ) -> Result<Message, MessageParseError> {
48        Message::with_tags(None, prefix, command, args)
49    }
50
51    /// Creates a new IRCv3.2 message from the given components, including message tags. These tags
52    /// are used to add extended information to the given message, and are commonly used in IRCv3
53    /// extensions to the IRC protocol.
54    pub fn with_tags(
55        tags: Option<Vec<Tag>>,
56        prefix: Option<&str>,
57        command: &str,
58        args: Vec<&str>,
59    ) -> Result<Message, error::MessageParseError> {
60        Ok(Message {
61            tags,
62            prefix: prefix.map(|p| p.into()),
63            command: Command::new(command, args)?,
64        })
65    }
66
67    /// Gets the nickname of the message source, if it exists.
68    ///
69    /// # Example
70    /// ```
71    /// # extern crate irc_proto;
72    /// # use irc_proto::Message;
73    /// # fn main() {
74    /// let message = Message::new(
75    ///     Some("nickname!username@hostname"), "JOIN", vec!["#channel"]
76    /// ).unwrap();
77    /// assert_eq!(message.source_nickname(), Some("nickname"));
78    /// # }
79    /// ```
80    pub fn source_nickname(&self) -> Option<&str> {
81        // <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
82        // <servername> ::= <host>
83        self.prefix.as_ref().and_then(|p| match p {
84            Prefix::Nickname(name, _, _) => Some(&name[..]),
85            _ => None,
86        })
87    }
88
89    /// Gets the likely intended place to respond to this message.
90    /// If the type of the message is a `PRIVMSG` or `NOTICE` and the message is sent to a channel,
91    /// the result will be that channel. In all other cases, this will call `source_nickname`.
92    ///
93    /// # Example
94    /// ```
95    /// # extern crate irc_proto;
96    /// # use irc_proto::Message;
97    /// # fn main() {
98    /// let msg1 = Message::new(
99    ///     Some("ada"), "PRIVMSG", vec!["#channel", "Hi, everyone!"]
100    /// ).unwrap();
101    /// assert_eq!(msg1.response_target(), Some("#channel"));
102    /// let msg2 = Message::new(
103    ///     Some("ada"), "PRIVMSG", vec!["betsy", "betsy: hi"]
104    /// ).unwrap();
105    /// assert_eq!(msg2.response_target(), Some("ada"));
106    /// # }
107    /// ```
108    pub fn response_target(&self) -> Option<&str> {
109        match self.command {
110            Command::PRIVMSG(ref target, _) if target.is_channel_name() => Some(target),
111            Command::NOTICE(ref target, _) if target.is_channel_name() => Some(target),
112            _ => self.source_nickname(),
113        }
114    }
115}
116
117impl From<Command> for Message {
118    fn from(cmd: Command) -> Message {
119        Message {
120            tags: None,
121            prefix: None,
122            command: cmd,
123        }
124    }
125}
126
127impl FromStr for Message {
128    type Err = ProtocolError;
129
130    fn from_str(s: &str) -> Result<Message, Self::Err> {
131        if s.is_empty() {
132            return Err(ProtocolError::InvalidMessage {
133                string: s.to_owned(),
134                cause: MessageParseError::EmptyMessage,
135            });
136        }
137
138        let mut state = s;
139
140        let tags = if state.starts_with('@') {
141            let tags = state.find(' ').map(|i| &state[1..i]);
142            state = state.find(' ').map_or("", |i| &state[i + 1..]);
143            tags.map(|ts| {
144                ts.split(';')
145                    .filter(|s| !s.is_empty())
146                    .map(|s: &str| {
147                        let mut iter = s.splitn(2, '=');
148                        let (fst, snd) = (iter.next(), iter.next());
149                        let snd = snd.map(unescape_tag_value);
150                        Tag(fst.unwrap_or("").to_owned(), snd)
151                    })
152                    .collect::<Vec<_>>()
153            })
154        } else {
155            None
156        };
157
158        let prefix = if state.starts_with(':') {
159            let prefix = state.find(' ').map(|i| &state[1..i]);
160            state = state.find(' ').map_or("", |i| &state[i + 1..]);
161            prefix
162        } else {
163            None
164        };
165
166        let line_ending_len = if state.ends_with("\r\n") {
167            "\r\n"
168        } else if state.ends_with('\r') {
169            "\r"
170        } else if state.ends_with('\n') {
171            "\n"
172        } else {
173            ""
174        }
175        .len();
176
177        let suffix = if state.contains(" :") {
178            let suffix = state
179                .find(" :")
180                .map(|i| &state[i + 2..state.len() - line_ending_len]);
181            state = state.find(" :").map_or("", |i| &state[..i + 1]);
182            suffix
183        } else {
184            state = &state[..state.len() - line_ending_len];
185            None
186        };
187
188        let command = match state.find(' ').map(|i| &state[..i]) {
189            Some(cmd) => {
190                state = state.find(' ').map_or("", |i| &state[i + 1..]);
191                cmd
192            }
193            // If there's no arguments but the "command" starts with colon, it's not a command.
194            None if state.starts_with(':') => {
195                return Err(ProtocolError::InvalidMessage {
196                    string: s.to_owned(),
197                    cause: MessageParseError::InvalidCommand,
198                })
199            }
200            // If there's no arguments following the command, the rest of the state is the command.
201            None => {
202                let cmd = state;
203                state = "";
204                cmd
205            }
206        };
207
208        let mut args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect();
209        if let Some(suffix) = suffix {
210            args.push(suffix);
211        }
212
213        Message::with_tags(tags, prefix, command, args).map_err(|e| ProtocolError::InvalidMessage {
214            string: s.to_owned(),
215            cause: e,
216        })
217    }
218}
219
220impl<'a> From<&'a str> for Message {
221    fn from(s: &'a str) -> Message {
222        s.parse().unwrap()
223    }
224}
225
226impl Display for Message {
227    /// Converts a Message into a String according to the IRC protocol.
228    ///
229    /// # Example
230    /// ```
231    /// # extern crate irc_proto;
232    /// # use irc_proto::Message;
233    /// # fn main() {
234    /// let msg = Message::new(
235    ///     Some("ada"), "PRIVMSG", vec!["#channel", "Hi, everyone!"]
236    /// ).unwrap();
237    /// assert_eq!(msg.to_string(), ":ada PRIVMSG #channel :Hi, everyone!\r\n");
238    /// # }
239    /// ```
240    fn fmt(&self, f: &mut Formatter) -> FmtResult {
241        if let Some(ref tags) = self.tags {
242            f.write_char('@')?;
243            for (i, tag) in tags.iter().enumerate() {
244                if i > 0 {
245                    f.write_char(';')?;
246                }
247                f.write_str(&tag.0)?;
248                if let Some(ref value) = tag.1 {
249                    f.write_char('=')?;
250                    escape_tag_value(f, value)?;
251                }
252            }
253            f.write_char(' ')?;
254        }
255        if let Some(ref prefix) = self.prefix {
256            write!(f, ":{} ", prefix)?
257        }
258        write!(f, "{}\r\n", String::from(&self.command))
259    }
260}
261
262/// A message tag as defined by [IRCv3.2](http://ircv3.net/specs/core/message-tags-3.2.html).
263/// It consists of a tag key, and an optional value for the tag. Each message can contain a number
264/// of tags (in the string format, they are separated by semicolons). Tags are used to add extended
265/// information to a message under IRCv3.
266#[derive(Clone, PartialEq, Debug)]
267pub struct Tag(pub String, pub Option<String>);
268
269fn escape_tag_value(f: &mut dyn Write, value: &str) -> FmtResult {
270    for c in value.chars() {
271        match c {
272            ';' => f.write_str("\\:")?,
273            ' ' => f.write_str("\\s")?,
274            '\\' => f.write_str("\\\\")?,
275            '\r' => f.write_str("\\r")?,
276            '\n' => f.write_str("\\n")?,
277            c => f.write_char(c)?,
278        }
279    }
280    Ok(())
281}
282
283fn unescape_tag_value(value: &str) -> String {
284    let mut unescaped = String::with_capacity(value.len());
285    let mut iter = value.chars();
286    while let Some(c) = iter.next() {
287        let r = if c == '\\' {
288            match iter.next() {
289                Some(':') => ';',
290                Some('s') => ' ',
291                Some('\\') => '\\',
292                Some('r') => '\r',
293                Some('n') => '\n',
294                Some(c) => c,
295                None => break,
296            }
297        } else {
298            c
299        };
300        unescaped.push(r);
301    }
302    unescaped
303}
304
305#[cfg(test)]
306mod test {
307    use super::{Message, Tag};
308    use crate::command::Command::{Raw, PRIVMSG, QUIT};
309
310    #[test]
311    fn new() {
312        let message = Message {
313            tags: None,
314            prefix: None,
315            command: PRIVMSG(format!("test"), format!("Testing!")),
316        };
317        assert_eq!(
318            Message::new(None, "PRIVMSG", vec!["test", "Testing!"]).unwrap(),
319            message
320        )
321    }
322
323    #[test]
324    fn source_nickname() {
325        assert_eq!(
326            Message::new(None, "PING", vec!["data"])
327                .unwrap()
328                .source_nickname(),
329            None
330        );
331
332        assert_eq!(
333            Message::new(Some("irc.test.net"), "PING", vec!["data"])
334                .unwrap()
335                .source_nickname(),
336            None
337        );
338
339        assert_eq!(
340            Message::new(Some("test!test@test"), "PING", vec!["data"])
341                .unwrap()
342                .source_nickname(),
343            Some("test")
344        );
345
346        assert_eq!(
347            Message::new(Some("test@test"), "PING", vec!["data"])
348                .unwrap()
349                .source_nickname(),
350            Some("test")
351        );
352
353        assert_eq!(
354            Message::new(Some("test!test@irc.test.com"), "PING", vec!["data"])
355                .unwrap()
356                .source_nickname(),
357            Some("test")
358        );
359
360        assert_eq!(
361            Message::new(Some("test!test@127.0.0.1"), "PING", vec!["data"])
362                .unwrap()
363                .source_nickname(),
364            Some("test")
365        );
366
367        assert_eq!(
368            Message::new(Some("test@test.com"), "PING", vec!["data"])
369                .unwrap()
370                .source_nickname(),
371            Some("test")
372        );
373
374        assert_eq!(
375            Message::new(Some("test"), "PING", vec!["data"])
376                .unwrap()
377                .source_nickname(),
378            Some("test")
379        );
380    }
381
382    #[test]
383    fn to_string() {
384        let message = Message {
385            tags: None,
386            prefix: None,
387            command: PRIVMSG(format!("test"), format!("Testing!")),
388        };
389        assert_eq!(&message.to_string()[..], "PRIVMSG test Testing!\r\n");
390        let message = Message {
391            tags: None,
392            prefix: Some("test!test@test".into()),
393            command: PRIVMSG(format!("test"), format!("Still testing!")),
394        };
395        assert_eq!(
396            &message.to_string()[..],
397            ":test!test@test PRIVMSG test :Still testing!\r\n"
398        );
399    }
400
401    #[test]
402    fn from_string() {
403        let message = Message {
404            tags: None,
405            prefix: None,
406            command: PRIVMSG(format!("test"), format!("Testing!")),
407        };
408        assert_eq!(
409            "PRIVMSG test :Testing!\r\n".parse::<Message>().unwrap(),
410            message
411        );
412        let message = Message {
413            tags: None,
414            prefix: Some("test!test@test".into()),
415            command: PRIVMSG(format!("test"), format!("Still testing!")),
416        };
417        assert_eq!(
418            ":test!test@test PRIVMSG test :Still testing!\r\n"
419                .parse::<Message>()
420                .unwrap(),
421            message
422        );
423        let message = Message {
424            tags: Some(vec![
425                Tag(format!("aaa"), Some(format!("bbb"))),
426                Tag(format!("ccc"), None),
427                Tag(format!("example.com/ddd"), Some(format!("eee"))),
428            ]),
429            prefix: Some("test!test@test".into()),
430            command: PRIVMSG(format!("test"), format!("Testing with tags!")),
431        };
432        assert_eq!(
433            "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
434             tags!\r\n"
435                .parse::<Message>()
436                .unwrap(),
437            message
438        )
439    }
440
441    #[test]
442    fn from_string_atypical_endings() {
443        let message = Message {
444            tags: None,
445            prefix: None,
446            command: PRIVMSG(format!("test"), format!("Testing!")),
447        };
448        assert_eq!(
449            "PRIVMSG test :Testing!\r".parse::<Message>().unwrap(),
450            message
451        );
452        assert_eq!(
453            "PRIVMSG test :Testing!\n".parse::<Message>().unwrap(),
454            message
455        );
456        assert_eq!(
457            "PRIVMSG test :Testing!".parse::<Message>().unwrap(),
458            message
459        );
460    }
461
462    #[test]
463    fn from_and_to_string() {
464        let message =
465            "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
466             tags!\r\n";
467        assert_eq!(message.parse::<Message>().unwrap().to_string(), message);
468    }
469
470    #[test]
471    fn to_message() {
472        let message = Message {
473            tags: None,
474            prefix: None,
475            command: PRIVMSG(format!("test"), format!("Testing!")),
476        };
477        let msg: Message = "PRIVMSG test :Testing!\r\n".into();
478        assert_eq!(msg, message);
479        let message = Message {
480            tags: None,
481            prefix: Some("test!test@test".into()),
482            command: PRIVMSG(format!("test"), format!("Still testing!")),
483        };
484        let msg: Message = ":test!test@test PRIVMSG test :Still testing!\r\n".into();
485        assert_eq!(msg, message);
486    }
487
488    #[test]
489    fn to_message_with_colon_in_arg() {
490        // Apparently, UnrealIRCd (and perhaps some others) send some messages that include
491        // colons within individual parameters. So, let's make sure it parses correctly.
492        let message = Message {
493            tags: None,
494            prefix: Some("test!test@test".into()),
495            command: Raw(
496                format!("COMMAND"),
497                vec![format!("ARG:test"), format!("Testing!")],
498            ),
499        };
500        let msg: Message = ":test!test@test COMMAND ARG:test :Testing!\r\n".into();
501        assert_eq!(msg, message);
502    }
503
504    #[test]
505    fn to_message_no_prefix_no_args() {
506        let message = Message {
507            tags: None,
508            prefix: None,
509            command: QUIT(None),
510        };
511        let msg: Message = "QUIT\r\n".into();
512        assert_eq!(msg, message);
513    }
514
515    #[test]
516    #[should_panic]
517    fn to_message_invalid_format() {
518        let _: Message = ":invalid :message".into();
519    }
520
521    #[test]
522    fn to_message_tags_escapes() {
523        let msg = "@tag=\\:\\s\\\\\\r\\n\\a\\ :test PRIVMSG #test :test\r\n"
524            .parse::<Message>()
525            .unwrap();
526        let message = Message {
527            tags: Some(vec![Tag("tag".to_string(), Some("; \\\r\na".to_string()))]),
528            prefix: Some("test".into()),
529            command: PRIVMSG("#test".to_string(), "test".to_string()),
530        };
531        assert_eq!(msg, message);
532    }
533
534    #[test]
535    fn to_string_tags_escapes() {
536        let msg = Message {
537            tags: Some(vec![Tag("tag".to_string(), Some("; \\\r\na".to_string()))]),
538            prefix: Some("test".into()),
539            command: PRIVMSG("#test".to_string(), "test".to_string()),
540        }
541        .to_string();
542        let message = "@tag=\\:\\s\\\\\\r\\na :test PRIVMSG #test test\r\n";
543        assert_eq!(msg, message);
544    }
545
546    #[test]
547    fn to_message_with_colon_in_suffix() {
548        let msg = "PRIVMSG #test ::test".parse::<Message>().unwrap();
549        let message = Message {
550            tags: None,
551            prefix: None,
552            command: PRIVMSG("#test".to_string(), ":test".to_string()),
553        };
554        assert_eq!(msg, message);
555    }
556
557    #[test]
558    fn to_string_with_colon_in_suffix() {
559        let msg = Message {
560            tags: None,
561            prefix: None,
562            command: PRIVMSG("#test".to_string(), ":test".to_string()),
563        }
564        .to_string();
565        let message = "PRIVMSG #test ::test\r\n";
566        assert_eq!(msg, message);
567    }
568}