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