rust-irc/src/proto/message.rs

329 lines
10 KiB
Rust
Raw Normal View History

//! Messages to and from the server.
use std::borrow::ToOwned;
2016-03-18 02:39:58 +01:00
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::str::FromStr;
2017-06-20 20:54:06 +02:00
use error;
use error::{Error, ErrorKind};
use client::data::Command;
/// IRC Message data.
2015-02-03 19:11:33 +01:00
#[derive(Clone, PartialEq, Debug)]
2014-11-02 23:25:45 +01:00
pub struct Message {
/// Message tags as defined by [IRCv3.2](http://ircv3.net/specs/core/message-tags-3.2.html).
pub tags: Option<Vec<Tag>>,
/// The message prefix (or source) as defined by [RFC 2812](http://tools.ietf.org/html/rfc2812).
2014-11-02 23:25:45 +01:00
pub prefix: Option<String>,
/// The IRC command.
pub command: Command,
2014-11-02 23:25:45 +01:00
}
impl Message {
/// Creates a new Message.
2017-06-19 19:59:26 +02:00
pub fn new(
prefix: Option<&str>,
command: &str,
args: Vec<&str>,
suffix: Option<&str>,
2017-06-20 20:54:06 +02:00
) -> error::Result<Message> {
Message::with_tags(None, prefix, command, args, suffix)
}
/// Creates a new Message optionally including IRCv3.2 message tags.
2017-06-19 19:59:26 +02:00
pub fn with_tags(
tags: Option<Vec<Tag>>,
prefix: Option<&str>,
command: &str,
args: Vec<&str>,
suffix: Option<&str>,
2017-06-20 20:54:06 +02:00
) -> error::Result<Message> {
Ok(Message {
tags: tags,
prefix: prefix.map(|s| s.to_owned()),
command: try!(Command::new(command, args, suffix)),
})
}
2015-06-22 18:03:57 +02:00
/// Gets the nickname of the message source, if it exists.
pub fn source_nickname(&self) -> Option<&str> {
2016-02-07 09:01:05 +01:00
// <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
// <servername> ::= <host>
2017-06-19 19:59:26 +02:00
self.prefix.as_ref().and_then(|s| match (
s.find('!'),
s.find('@'),
s.find('.'),
) {
(Some(i), _, _) => Some(&s[..i]), // <nick> '!' <user> [ '@' <host> ]
(None, Some(i), _) => Some(&s[..i]), // <nick> '@' <host>
(None, None, None) => Some(s), // <nick>
_ => None, // <servername>
})
}
/// Converts a Message into a String according to the IRC protocol.
pub fn to_string(&self) -> String {
// TODO: tags
2014-11-02 23:25:45 +01:00
let mut ret = String::new();
if let Some(ref prefix) = self.prefix {
ret.push(':');
2017-06-19 19:46:01 +02:00
ret.push_str(prefix);
2014-11-02 23:25:45 +01:00
ret.push(' ');
}
let cmd: String = From::from(&self.command);
ret.push_str(&cmd);
ret.push_str("\r\n");
2014-11-02 23:25:45 +01:00
ret
}
}
impl From<Command> for Message {
fn from(cmd: Command) -> Message {
2017-06-19 19:59:26 +02:00
Message {
tags: None,
prefix: None,
command: cmd,
}
}
}
impl FromStr for Message {
2017-06-20 20:54:06 +02:00
type Err = Error;
fn from_str(s: &str) -> Result<Message, Self::Err> {
let mut state = s;
2017-06-19 19:59:26 +02:00
if s.is_empty() {
2017-06-20 20:54:06 +02:00
return Err(ErrorKind::ParseEmpty.into());
2017-06-19 19:59:26 +02:00
}
let tags = if state.starts_with('@') {
let tags = state.find(' ').map(|i| &state[1..i]);
2017-06-19 19:59:26 +02:00
state = state.find(' ').map_or("", |i| &state[i + 1..]);
tags.map(|ts| {
ts.split(';')
.filter(|s| !s.is_empty())
.map(|s: &str| {
let mut iter = s.splitn(2, '=');
let (fst, snd) = (iter.next(), iter.next());
Tag(fst.unwrap_or("").to_owned(), snd.map(|s| s.to_owned()))
})
.collect::<Vec<_>>()
})
} else {
None
};
let prefix = if state.starts_with(':') {
2015-01-09 23:38:46 +01:00
let prefix = state.find(' ').map(|i| &state[1..i]);
2017-06-19 19:59:26 +02:00
state = state.find(' ').map_or("", |i| &state[i + 1..]);
prefix
} else {
None
};
let suffix = if state.contains(" :") {
2017-06-19 19:59:26 +02:00
let suffix = state.find(" :").map(|i| &state[i + 2..state.len() - 2]);
state = state.find(" :").map_or("", |i| &state[..i + 1]);
suffix
} else {
None
};
2015-01-09 23:38:46 +01:00
let command = match state.find(' ').map(|i| &state[..i]) {
Some(cmd) => {
2017-06-19 19:59:26 +02:00
state = state.find(' ').map_or("", |i| &state[i + 1..]);
cmd
}
2017-06-20 20:54:06 +02:00
_ => return Err(ErrorKind::InvalidCommand.into()),
};
2017-06-19 19:59:26 +02:00
if suffix.is_none() {
state = &state[..state.len() - 2]
}
let args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect();
2017-06-19 19:59:26 +02:00
Message::with_tags(tags, prefix, command, args, suffix)
2017-06-20 20:54:06 +02:00
.map_err(|_| ErrorKind::InvalidCommand.into())
}
}
2015-04-26 06:01:33 +02:00
impl<'a> From<&'a str> for Message {
fn from(s: &'a str) -> Message {
s.parse().unwrap()
}
}
2016-03-18 02:39:58 +01:00
impl Display for Message {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "{}", self.to_string())
}
}
/// A message tag as defined by [IRCv3.2](http://ircv3.net/specs/core/message-tags-3.2.html).
#[derive(Clone, PartialEq, Debug)]
2016-06-19 06:37:58 +02:00
pub struct Tag(pub String, pub Option<String>);
#[cfg(test)]
mod test {
use super::{Message, Tag};
use client::data::Command::{PRIVMSG, Raw};
#[test]
fn new() {
let message = Message {
tags: None,
prefix: None,
command: PRIVMSG(format!("test"), format!("Testing!")),
};
2017-06-19 19:59:26 +02:00
assert_eq!(
Message::new(None, "PRIVMSG", vec!["test"], Some("Testing!")).unwrap(),
message
)
}
#[test]
fn source_nickname() {
2017-06-19 19:59:26 +02:00
assert_eq!(
Message::new(None, "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
None
);
2017-06-19 19:59:26 +02:00
assert_eq!(
Message::new(Some("irc.test.net"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
None
);
2017-06-19 19:59:26 +02:00
assert_eq!(
Message::new(Some("test!test@test"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
2017-06-19 19:59:26 +02:00
assert_eq!(
Message::new(Some("test@test"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
2017-06-19 19:59:26 +02:00
assert_eq!(
Message::new(Some("test!test@irc.test.com"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
2017-06-19 19:59:26 +02:00
assert_eq!(
Message::new(Some("test!test@127.0.0.1"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
2017-06-19 19:59:26 +02:00
assert_eq!(
Message::new(Some("test@test.com"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
2017-06-19 19:59:26 +02:00
assert_eq!(
Message::new(Some("test"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
}
#[test]
fn to_string() {
let message = Message {
tags: None,
prefix: None,
command: PRIVMSG(format!("test"), format!("Testing!")),
};
assert_eq!(&message.to_string()[..], "PRIVMSG test :Testing!\r\n");
let message = Message {
tags: None,
prefix: Some(format!("test!test@test")),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
2017-06-19 19:59:26 +02:00
assert_eq!(
&message.to_string()[..],
":test!test@test PRIVMSG test :Still testing!\r\n"
);
}
#[test]
fn from_string() {
let message = Message {
tags: None,
prefix: None,
command: PRIVMSG(format!("test"), format!("Testing!")),
};
2017-06-20 20:54:06 +02:00
assert_eq!("PRIVMSG test :Testing!\r\n".parse::<Message>().unwrap(), message);
let message = Message {
tags: None,
prefix: Some(format!("test!test@test")),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
2017-06-19 19:59:26 +02:00
assert_eq!(
2017-06-20 20:54:06 +02:00
":test!test@test PRIVMSG test :Still testing!\r\n".parse::<Message>().unwrap(),
message
2017-06-19 19:59:26 +02:00
);
let message = Message {
2017-06-19 19:59:26 +02:00
tags: Some(vec![
Tag(format!("aaa"), Some(format!("bbb"))),
Tag(format!("ccc"), None),
Tag(format!("example.com/ddd"), Some(format!("eee"))),
]),
prefix: Some(format!("test!test@test")),
command: PRIVMSG(format!("test"), format!("Testing with tags!")),
};
2017-06-19 19:59:26 +02:00
assert_eq!(
"@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
tags!\r\n"
2017-06-20 20:54:06 +02:00
.parse::<Message>().unwrap(),
message
2017-06-19 19:59:26 +02:00
)
}
#[test]
fn to_message() {
let message = Message {
tags: None,
prefix: None,
command: PRIVMSG(format!("test"), format!("Testing!")),
};
2015-04-26 06:11:51 +02:00
let msg: Message = "PRIVMSG test :Testing!\r\n".into();
assert_eq!(msg, message);
let message = Message {
tags: None,
prefix: Some(format!("test!test@test")),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
2015-04-26 06:11:51 +02:00
let msg: Message = ":test!test@test PRIVMSG test :Still testing!\r\n".into();
assert_eq!(msg, message);
}
#[test]
fn to_message_with_colon_in_arg() {
// Apparently, UnrealIRCd (and perhaps some others) send some messages that include
// colons within individual parameters. So, let's make sure it parses correctly.
let message = Message {
tags: None,
prefix: Some(format!("test!test@test")),
command: Raw(
2017-06-19 19:59:26 +02:00
format!("COMMAND"),
vec![format!("ARG:test")],
Some(format!("Testing!")),
),
};
2015-04-26 06:11:51 +02:00
let msg: Message = ":test!test@test COMMAND ARG:test :Testing!\r\n".into();
assert_eq!(msg, message);
}
#[test]
2015-03-22 04:08:41 +01:00
#[should_panic]
fn to_message_invalid_format() {
2015-04-26 06:11:51 +02:00
let _: Message = ":invalid :message".into();
}
}