forked from lavina/lavina
1
0
Fork 0
lavina/crates/proto-irc/src/client.rs

492 lines
14 KiB
Rust
Raw Normal View History

2023-10-04 18:27:43 +00:00
use anyhow::{anyhow, Result};
use nom::combinator::{all_consuming, opt};
2024-04-01 18:48:12 +00:00
use nom::error::ErrorKind;
use nonempty::NonEmpty;
2024-04-01 18:48:12 +00:00
use super::*;
/// Client-to-server command.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClientMessage {
/// `CAP`. Capability-related commands.
Capability {
subcommand: CapabilitySubcommand,
},
/// `PING <token>`
Ping {
2023-04-13 22:38:26 +00:00
token: Str,
},
/// `PONG <token>`
Pong {
2023-04-13 22:38:26 +00:00
token: Str,
},
/// `NICK <nickname>`
Nick {
2023-04-13 22:38:26 +00:00
nickname: Str,
},
/// `PASS <password>`
2023-08-16 14:30:02 +00:00
Pass {
password: Str,
},
/// `USER <username> 0 * :<realname>`
User {
2023-04-13 22:38:26 +00:00
username: Str,
realname: Str,
},
/// `JOIN <chan>`
2023-02-12 23:31:16 +00:00
Join(Chan),
/// `MODE <target>`
2023-02-16 19:37:17 +00:00
Mode {
target: Recipient,
},
/// `WHO <target>`
Who {
target: Recipient, // aka mask
},
2024-04-01 18:48:12 +00:00
/// WHOIS [<target>] <nick>
Whois {
target: Option<Str>, // server_name or nick_name
nick: Str,
},
/// `TOPIC <chan> :<topic>`
Topic {
chan: Chan,
2023-04-13 22:38:26 +00:00
topic: Str,
},
Part {
chan: Chan,
2023-04-13 22:38:26 +00:00
message: Str,
},
/// `PRIVMSG <target> :<msg>`
PrivateMessage {
recipient: Recipient,
2023-04-13 22:38:26 +00:00
body: Str,
},
/// `QUIT :<reason>`
Quit {
2023-04-13 22:38:26 +00:00
reason: Str,
},
Authenticate(Str),
}
2023-10-04 18:27:43 +00:00
pub fn client_message(input: &str) -> Result<ClientMessage> {
let res = all_consuming(alt((
client_message_capability,
client_message_ping,
client_message_pong,
client_message_nick,
2023-08-16 14:30:02 +00:00
client_message_pass,
client_message_user,
2023-02-12 23:31:16 +00:00
client_message_join,
client_message_mode,
client_message_who,
2024-04-01 18:48:12 +00:00
client_message_whois,
client_message_topic,
client_message_part,
2023-02-13 17:08:37 +00:00
client_message_privmsg,
client_message_quit,
client_message_authenticate,
2023-10-04 18:27:43 +00:00
)))(input);
match res {
Ok((_, e)) => Ok(e),
Err(e) => Err(anyhow!("Parsing failed: {e}")),
}
}
2023-04-13 19:15:48 +00:00
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 }))
}
2023-04-13 19:15:48 +00:00
fn client_message_ping(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("PING ")(input)?;
let (input, token) = token(input)?;
2023-10-04 18:27:43 +00:00
Ok((input, ClientMessage::Ping { token: token.into() }))
}
2023-04-13 19:15:48 +00:00
fn client_message_pong(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("PONG ")(input)?;
let (input, token) = token(input)?;
2023-10-04 18:27:43 +00:00
Ok((input, ClientMessage::Pong { token: token.into() }))
}
2023-04-13 19:15:48 +00:00
fn client_message_nick(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("NICK ")(input)?;
let (input, nickname) = receiver(input)?;
Ok((
input,
ClientMessage::Nick {
2023-04-13 19:15:48 +00:00
nickname: nickname.into(),
},
))
}
2023-08-16 14:30:02 +00:00
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(),
},
))
}
2023-04-13 19:15:48 +00:00
fn client_message_user(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("USER ")(input)?;
let (input, username) = receiver(input)?;
let (input, _) = tag(" ")(input)?;
2023-02-14 18:28:49 +00:00
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 {
2023-04-13 19:15:48 +00:00
username: username.into(),
realname: realname.into(),
},
))
}
2023-04-13 19:15:48 +00:00
fn client_message_join(input: &str) -> IResult<&str, ClientMessage> {
2023-02-12 23:31:16 +00:00
let (input, _) = tag("JOIN ")(input)?;
let (input, chan) = chan(input)?;
Ok((input, ClientMessage::Join(chan)))
}
2023-04-13 19:15:48 +00:00
fn client_message_mode(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("MODE ")(input)?;
2023-02-16 19:37:17 +00:00
let (input, target) = recipient(input)?;
2023-02-16 21:49:17 +00:00
Ok((input, ClientMessage::Mode { target }))
}
2023-04-13 19:15:48 +00:00
fn client_message_who(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("WHO ")(input)?;
let (input, target) = recipient(input)?;
Ok((input, ClientMessage::Who { target }))
}
2024-04-01 18:48:12 +00:00
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: nick.into(),
},
)),
[target, nick, ..] => Ok((
"",
ClientMessage::Whois {
target: Some(target.into()),
nick: nick.into(),
},
)),
// fixme: idk how to deal with this in more elegant way
[] => Err(nom::Err::Failure(nom::error::Error {
input: "No args passed",
code: ErrorKind::Fail,
})),
}
}
2023-04-13 19:15:48 +00:00
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)?,
};
2023-04-13 19:15:48 +00:00
let topic = topic.into();
Ok((input, ClientMessage::Topic { chan, topic }))
}
2023-04-13 19:15:48 +00:00
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)?,
};
2023-04-13 19:15:48 +00:00
let message = message.into();
Ok((input, ClientMessage::Part { chan, message }))
}
2023-04-13 19:15:48 +00:00
fn client_message_privmsg(input: &str) -> IResult<&str, ClientMessage> {
2023-02-13 17:08:37 +00:00
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)?,
};
2023-02-13 17:08:37 +00:00
2023-04-13 19:15:48 +00:00
let body = body.into();
2023-02-13 17:08:37 +00:00
Ok((input, ClientMessage::PrivateMessage { recipient, body }))
}
2023-04-13 19:15:48 +00:00
fn client_message_quit(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("QUIT :")(input)?;
let (input, reason) = token(input)?;
2023-10-04 18:27:43 +00:00
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<CapReq>),
/// CAP END
End,
}
2023-04-13 19:15:48 +00:00
fn capability_subcommand(input: &str) -> IResult<&str, CapabilitySubcommand> {
alt((
capability_subcommand_ls,
capability_subcommand_end,
capability_subcommand_req,
))(input)
}
2023-04-13 19:15:48 +00:00
fn capability_subcommand_ls(input: &str) -> IResult<&str, CapabilitySubcommand> {
let (input, _) = tag("LS ")(input)?;
let (input, code) = take(3usize)(input)?;
Ok((
input,
CapabilitySubcommand::List {
2023-04-13 19:15:48 +00:00
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::<Vec<_>>();
let caps = NonEmpty::from_vec(caps).ok_or_else(|| todo!())?;
Ok((input, CapabilitySubcommand::Req(caps)))
}
2023-04-13 19:15:48 +00:00
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::*;
2024-04-01 18:48:12 +00:00
#[test]
fn test_client_message_cap_ls() {
2023-04-13 19:15:48 +00:00
let input = "CAP LS 302";
let expected = ClientMessage::Capability {
subcommand: CapabilitySubcommand::List { code: *b"302" },
};
let result = client_message(input);
2023-10-04 18:27:43 +00:00
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_cap_end() {
2023-04-13 19:15:48 +00:00
let input = "CAP END";
let expected = ClientMessage::Capability {
subcommand: CapabilitySubcommand::End,
};
let result = client_message(input);
2023-10-04 18:27:43 +00:00
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_ping() {
2023-04-13 19:15:48 +00:00
let input = "PING 1337";
2023-10-04 18:27:43 +00:00
let expected = ClientMessage::Ping { token: "1337".into() };
let result = client_message(input);
2023-10-04 18:27:43 +00:00
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_pong() {
2023-04-13 19:15:48 +00:00
let input = "PONG 1337";
2023-10-04 18:27:43 +00:00
let expected = ClientMessage::Pong { token: "1337".into() };
let result = client_message(input);
2023-10-04 18:27:43 +00:00
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_nick() {
2023-04-13 19:15:48 +00:00
let input = "NICK SomeNick";
let expected = ClientMessage::Nick {
2023-04-13 19:15:48 +00:00
nickname: "SomeNick".into(),
};
let result = client_message(input);
2023-10-04 18:27:43 +00:00
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
2024-04-01 18:48:12 +00:00
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 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 expected_arg = ClientMessage::Whois {
target: None,
nick: "val".into(),
};
let expected_user_user = ClientMessage::Whois {
target: Some("val".into()),
nick: "val".into(),
};
let expected_server_user = ClientMessage::Whois {
target: Some("com.test.server".into()),
nick: "user".into(),
};
let expected_user_server = ClientMessage::Whois {
target: Some("user".into()),
nick: "com.test.server".into(),
};
let expected_user_list = ClientMessage::Whois {
target: None,
nick: "user_1,user_2,user_3".into(),
};
let expected_server_user_list = ClientMessage::Whois {
target: Some("com.test.server".into()),
nick: "user_1,user_2,user_3".into(),
};
let expected_more_than_two_params = ClientMessage::Whois {
target: Some("test.server".into()),
nick: "user_1,user_2,user_3".into(),
};
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))
}
#[test]
fn test_client_message_user() {
2023-04-13 19:15:48 +00:00
let input = "USER SomeNick 8 * :Real Name";
let expected = ClientMessage::User {
2023-04-13 19:15:48 +00:00
username: "SomeNick".into(),
realname: "Real Name".into(),
};
let result = client_message(input);
2023-10-04 18:27:43 +00:00
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_part() {
2023-04-13 19:15:48 +00:00
let input = "PART #chan :Pokasiki !!!";
let expected = ClientMessage::Part {
2023-04-13 19:15:48 +00:00
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);
2023-10-04 18:27:43 +00:00
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
}