1use 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#[derive(Clone, PartialEq, Debug)]
15pub struct Message {
16 pub tags: Option<Vec<Tag>>,
20 pub prefix: Option<String>,
22 pub command: Command,
25}
26
27impl Message {
28 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 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 pub fn source_nickname(&self) -> Option<&str> {
78 self.prefix.as_ref().and_then(|s| match (
81 s.find('!'),
82 s.find('@'),
83 s.find('.'),
84 ) {
85 (Some(i), _, _) | (None, Some(i), _) => Some(&s[..i]), (None, None, None) => Some(s), _ => None, })
90 }
91
92 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 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 None if state.starts_with(':') => {
234 return Err(IrcError::InvalidMessage {
235 string: s.to_owned(),
236 cause: MessageParseError::InvalidCommand,
237 })
238 }
239 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#[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 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}