use anyhow::{anyhow, Result}; use nom::combinator::{all_consuming, opt}; use nom::error::ErrorKind; use nonempty::NonEmpty; use super::*; /// Client-to-server command. #[derive(Clone, Debug, PartialEq, Eq)] pub enum ClientMessage { /// `CAP`. Capability-related commands. Capability { subcommand: CapabilitySubcommand, }, /// `PING ` Ping { token: Str, }, /// `PONG ` Pong { token: Str, }, /// `NICK ` Nick { nickname: Str, }, /// `PASS ` Pass { password: Str, }, /// `USER 0 * :` User { username: Str, realname: Str, }, /// `JOIN ` Join(Chan), /// `MODE ` Mode { target: Recipient, }, /// `WHO ` Who { target: Recipient, // aka mask }, /// WHOIS [] Whois { target: Option, // server_name or nick_name nick: Option, }, /// `TOPIC :` Topic { chan: Chan, topic: Str, }, Part { chan: Chan, message: Str, }, /// `PRIVMSG :` PrivateMessage { recipient: Recipient, body: Str, }, /// `QUIT :` Quit { reason: Str, }, Authenticate(Str), } pub fn client_message(input: &str) -> Result { let res = all_consuming(alt(( client_message_capability, client_message_ping, client_message_pong, client_message_nick, client_message_pass, client_message_user, client_message_join, client_message_mode, client_message_who, client_message_whois, client_message_topic, client_message_part, client_message_privmsg, client_message_quit, client_message_authenticate, )))(input); match res { Ok((_, e)) => Ok(e), Err(e) => Err(anyhow!("Parsing failed: {e}")), } } fn client_message_capability(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("CAP ")(input)?; let (input, subcommand) = capability_subcommand(input)?; Ok((input, ClientMessage::Capability { subcommand })) } fn client_message_ping(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("PING ")(input)?; let (input, token) = token(input)?; Ok((input, ClientMessage::Ping { token: token.into() })) } fn client_message_pong(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("PONG ")(input)?; let (input, token) = token(input)?; Ok((input, ClientMessage::Pong { token: token.into() })) } fn client_message_nick(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("NICK ")(input)?; let (input, nickname) = receiver(input)?; Ok(( input, ClientMessage::Nick { nickname: nickname.into(), }, )) } fn client_message_pass(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("PASS ")(input)?; let (input, r) = opt(tag(":"))(input)?; let (input, password) = match r { Some(_) => token(input)?, None => receiver(input)?, }; Ok(( input, ClientMessage::Pass { password: password.into(), }, )) } fn client_message_user(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("USER ")(input)?; let (input, username) = receiver(input)?; let (input, _) = tag(" ")(input)?; let (input, _) = receiver(input)?; let (input, _) = tag(" ")(input)?; let (input, _) = receiver(input)?; let (input, _) = tag(" ")(input)?; let (input, r) = opt(tag(":"))(input)?; let (input, realname) = match r { Some(_) => token(input)?, None => receiver(input)?, }; Ok(( input, ClientMessage::User { username: username.into(), realname: realname.into(), }, )) } fn client_message_join(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("JOIN ")(input)?; let (input, chan) = chan(input)?; Ok((input, ClientMessage::Join(chan))) } fn client_message_mode(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("MODE ")(input)?; let (input, target) = recipient(input)?; Ok((input, ClientMessage::Mode { target })) } fn client_message_who(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("WHO ")(input)?; let (input, target) = recipient(input)?; Ok((input, ClientMessage::Who { target })) } fn client_message_whois(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("WHOIS ")(input)?; let args: Vec<_> = input.split_whitespace().collect(); match args.as_slice()[..] { [nick] => Ok(( "", ClientMessage::Whois { target: None, nick: Some(nick.into()), }, )), [target, nick, ..] => Ok(( "", ClientMessage::Whois { target: Some(target.into()), nick: Some(nick.into()), }, )), // fixme: idk how to deal with this in more elegant way [] => Ok(( "", ClientMessage::Whois { target: None, nick: None, }, )), } } fn client_message_topic(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("TOPIC ")(input)?; let (input, chan) = chan(input)?; let (input, _) = tag(" ")(input)?; let (input, r) = opt(tag(":"))(input)?; let (input, topic) = match r { Some(_) => token(input)?, None => receiver(input)?, }; let topic = topic.into(); Ok((input, ClientMessage::Topic { chan, topic })) } fn client_message_part(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("PART ")(input)?; let (input, chan) = chan(input)?; let (input, _) = tag(" ")(input)?; let (input, r) = opt(tag(":"))(input)?; let (input, message) = match r { Some(_) => token(input)?, None => receiver(input)?, }; let message = message.into(); Ok((input, ClientMessage::Part { chan, message })) } fn client_message_privmsg(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("PRIVMSG ")(input)?; let (input, recipient) = recipient(input)?; let (input, _) = tag(" ")(input)?; let (input, r) = opt(tag(":"))(input)?; let (input, body) = match r { Some(_) => token(input)?, None => receiver(input)?, }; let body = body.into(); Ok((input, ClientMessage::PrivateMessage { recipient, body })) } fn client_message_quit(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("QUIT :")(input)?; let (input, reason) = token(input)?; Ok((input, ClientMessage::Quit { reason: reason.into() })) } fn client_message_authenticate(input: &str) -> IResult<&str, ClientMessage> { let (input, _) = tag("AUTHENTICATE ")(input)?; let (input, body) = token(input)?; Ok((input, ClientMessage::Authenticate(body.into()))) } #[derive(Clone, Debug, PartialEq, Eq)] pub enum CapabilitySubcommand { /// CAP LS {code} List { code: [u8; 3] }, /// CAP REQ :... Req(NonEmpty), /// CAP END End, } fn capability_subcommand(input: &str) -> IResult<&str, CapabilitySubcommand> { alt(( capability_subcommand_ls, capability_subcommand_end, capability_subcommand_req, ))(input) } fn capability_subcommand_ls(input: &str) -> IResult<&str, CapabilitySubcommand> { let (input, _) = tag("LS ")(input)?; let (input, code) = take(3usize)(input)?; Ok(( input, CapabilitySubcommand::List { code: code.as_bytes().try_into().unwrap(), }, )) } fn capability_subcommand_req(input: &str) -> IResult<&str, CapabilitySubcommand> { let (input, _) = tag("REQ ")(input)?; let (input, r) = opt(tag(":"))(input)?; let (input, body) = match r { Some(_) => token(input)?, None => receiver(input)?, }; let caps = body .split(' ') .map(|cap| { let to_disable = cap.starts_with('-'); let name = if to_disable { &cap[1..] } else { &cap[..] }; CapReq { to_disable, name: name.into(), } }) .collect::>(); let caps = NonEmpty::from_vec(caps).ok_or_else(|| todo!())?; Ok((input, CapabilitySubcommand::Req(caps))) } fn capability_subcommand_end(input: &str) -> IResult<&str, CapabilitySubcommand> { let (input, _) = tag("END")(input)?; Ok((input, CapabilitySubcommand::End)) } #[derive(Clone, Debug, PartialEq, Eq)] pub struct CapReq { pub to_disable: bool, pub name: Str, } #[cfg(test)] mod test { use assert_matches::*; use nonempty::nonempty; use super::*; #[test] fn test_client_message_cap_ls() { let input = "CAP LS 302"; let expected = ClientMessage::Capability { subcommand: CapabilitySubcommand::List { code: *b"302" }, }; let result = client_message(input); assert_matches!(result, Ok(result) => assert_eq!(expected, result)); } #[test] fn test_client_message_cap_end() { let input = "CAP END"; let expected = ClientMessage::Capability { subcommand: CapabilitySubcommand::End, }; let result = client_message(input); assert_matches!(result, Ok(result) => assert_eq!(expected, result)); } #[test] fn test_client_message_ping() { let input = "PING 1337"; let expected = ClientMessage::Ping { token: "1337".into() }; let result = client_message(input); assert_matches!(result, Ok(result) => assert_eq!(expected, result)); } #[test] fn test_client_message_pong() { let input = "PONG 1337"; let expected = ClientMessage::Pong { token: "1337".into() }; let result = client_message(input); assert_matches!(result, Ok(result) => assert_eq!(expected, result)); } #[test] fn test_client_message_nick() { let input = "NICK SomeNick"; let expected = ClientMessage::Nick { nickname: "SomeNick".into(), }; let result = client_message(input); assert_matches!(result, Ok(result) => assert_eq!(expected, result)); } #[test] fn test_client_message_whois() { let test_user = "WHOIS val"; let test_user_user = "WHOIS val val"; let test_server_user = "WHOIS com.test.server user"; let test_user_server = "WHOIS user com.test.server"; let test_users_list = "WHOIS user_1,user_2,user_3"; let test_server_users_list = "WHOIS com.test.server user_1,user_2,user_3"; let test_more_than_two_params = "WHOIS test.server user_1,user_2,user_3 whatever spam"; let test_none_none_params = "WHOIS "; let res_one_arg = client_message(test_user); let res_user_user = client_message(test_user_user); let res_server_user = client_message(test_server_user); let res_user_server = client_message(test_user_server); let res_users_list = client_message(test_users_list); let res_server_users_list = client_message(test_server_users_list); let res_more_than_two_params = client_message(test_more_than_two_params); let res_none_none_params = client_message(test_none_none_params); let expected_arg = ClientMessage::Whois { target: None, nick: Some("val".into()), }; let expected_user_user = ClientMessage::Whois { target: Some("val".into()), nick: Some("val".into()), }; let expected_server_user = ClientMessage::Whois { target: Some("com.test.server".into()), nick: Some("user".into()), }; let expected_user_server = ClientMessage::Whois { target: Some("user".into()), nick: Some("com.test.server".into()), }; let expected_user_list = ClientMessage::Whois { target: None, nick: Some("user_1,user_2,user_3".into()), }; let expected_server_user_list = ClientMessage::Whois { target: Some("com.test.server".into()), nick: Some("user_1,user_2,user_3".into()), }; let expected_more_than_two_params = ClientMessage::Whois { target: Some("test.server".into()), nick: Some("user_1,user_2,user_3".into()), }; let expected_none_none_params = ClientMessage::Whois { target: None, nick: None, }; assert_matches!(res_one_arg, Ok(result) => assert_eq!(expected_arg, result)); assert_matches!(res_user_user, Ok(result) => assert_eq!(expected_user_user, result)); assert_matches!(res_server_user, Ok(result) => assert_eq!(expected_server_user, result)); assert_matches!(res_user_server, Ok(result) => assert_eq!(expected_user_server, result)); assert_matches!(res_users_list, Ok(result) => assert_eq!(expected_user_list, result)); assert_matches!(res_server_users_list, Ok(result) => assert_eq!(expected_server_user_list, result)); assert_matches!(res_more_than_two_params, Ok(result) => assert_eq!(expected_more_than_two_params, result)); assert_matches!(res_none_none_params, Ok(result) => assert_eq!(expected_none_none_params, result)) } #[test] fn test_client_message_user() { let input = "USER SomeNick 8 * :Real Name"; let expected = ClientMessage::User { username: "SomeNick".into(), realname: "Real Name".into(), }; let result = client_message(input); assert_matches!(result, Ok(result) => assert_eq!(expected, result)); } #[test] fn test_client_message_part() { let input = "PART #chan :Pokasiki !!!"; let expected = ClientMessage::Part { chan: Chan::Global("chan".into()), message: "Pokasiki !!!".into(), }; let result = client_message(input); assert_matches!(result, Ok(result) => assert_eq!(expected, result)); } #[test] fn test_client_cap_req() { let input = "CAP REQ :multi-prefix -sasl"; let expected = ClientMessage::Capability { subcommand: CapabilitySubcommand::Req(nonempty![ CapReq { to_disable: false, name: "multi-prefix".into() }, CapReq { to_disable: true, name: "sasl".into() } ]), }; let result = client_message(input); assert_matches!(result, Ok(result) => assert_eq!(expected, result)); } }