use nonempty::NonEmpty; use tokio::io::AsyncWrite; use tokio::io::AsyncWriteExt; use super::*; use crate::user::PrefixedNick; /// Server-to-client message. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ServerMessage { /// Optional tags section, prefixed with `@` pub tags: Vec, /// Optional server name, prefixed with `:`. pub sender: Option, pub body: ServerMessageBody, } impl ServerMessage { pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> { match &self.sender { Some(ref sender) => { writer.write_all(b":").await?; writer.write_all(sender.as_bytes()).await?; writer.write_all(b" ").await?; } None => {} } self.body.write_async(writer).await?; writer.write_all(b"\r\n").await?; Ok(()) } } pub fn server_message(input: &str) -> IResult<&str, ServerMessage> { let (input, command) = server_message_body(input)?; let (input, _) = tag("\r\n")(input)?; let message = ServerMessage { tags: vec![], sender: None, body: command, }; Ok((input, message)) } #[derive(Clone, Debug, PartialEq, Eq)] pub enum ServerMessageBody { Notice { first_target: Str, rest_targets: Vec, text: Str, }, Ping { token: Str, }, Pong { from: Str, token: Str, }, PrivateMessage { target: Recipient, body: Str, }, Join(Chan), Part(Chan), Error { reason: Str, }, Cap { target: Str, subcmd: CapSubBody, }, Authenticate(Str), N001Welcome { client: Str, text: Str, }, N002YourHost { client: Str, text: Str, }, N003Created { client: Str, text: Str, }, N004MyInfo { client: Str, hostname: Str, softname: Str, // TODO user modes, channel modes, channel modes with a parameter }, N005ISupport { client: Str, params: Str, // TODO make this a datatype }, /// Reply to a client's [Mode](crate::protos::irc::client::ClientMessage::Mode) request. N221UserModeIs { client: Str, modes: Str, }, /// Final reply to a client's [Who](crate::protos::irc::client::ClientMessage::Who) request. N315EndOfWho { client: Str, mask: Recipient, /// Usually `b"End of WHO list"` msg: Str, }, N332Topic { client: Str, chat: Chan, topic: Str, }, /// A reply to a client's [Who](crate::protos::irc::client::ClientMessage::Who) request. N352WhoReply { client: Str, // chan = * username: Str, /// User's hostname host: Str, /// Hostname of the server the user is connected to server: Str, nickname: Str, /// Flags flags: AwayStatus, hops: u8, realname: Str, }, N353NamesReply { client: Str, chan: Chan, members: NonEmpty, }, N366NamesReplyEnd { client: Str, chan: Chan, }, N474BannedFromChan { client: Str, chan: Chan, message: Str, }, N502UsersDontMatch { client: Str, message: Str, }, N900LoggedIn { nick: Str, address: Str, account: Str, message: Str, }, N903SaslSuccess { nick: Str, message: Str, } } impl ServerMessageBody { pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> { match self { ServerMessageBody::Notice { first_target, rest_targets, text, } => { writer.write_all(b"NOTICE ").await?; writer.write_all(first_target.as_bytes()).await?; writer.write_all(b" :").await?; writer.write_all(text.as_bytes()).await?; } ServerMessageBody::Ping { token } => { writer.write_all(b"PING ").await?; writer.write_all(token.as_bytes()).await?; } ServerMessageBody::Pong { from, token } => { writer.write_all(b"PONG ").await?; writer.write_all(from.as_bytes()).await?; writer.write_all(b" :").await?; writer.write_all(token.as_bytes()).await?; } ServerMessageBody::PrivateMessage { target, body } => { writer.write_all(b"PRIVMSG ").await?; target.write_async(writer).await?; writer.write_all(b" :").await?; writer.write_all(body.as_bytes()).await?; } ServerMessageBody::Join(chan) => { writer.write_all(b"JOIN ").await?; chan.write_async(writer).await?; } ServerMessageBody::Part(chan) => { writer.write_all(b"PART ").await?; chan.write_async(writer).await?; } ServerMessageBody::Error { reason } => { writer.write_all(b"ERROR :").await?; writer.write_all(reason.as_bytes()).await?; } ServerMessageBody::Cap { target, subcmd } => { writer.write_all(b"CAP ").await?; writer.write_all(target.as_bytes()).await?; match subcmd { CapSubBody::Ls(caps) => { writer.write_all(b" LS :").await?; writer.write_all(caps.as_bytes()).await?; } CapSubBody::Ack(caps) => { writer.write_all(b" ACK :").await?; writer.write_all(caps.as_bytes()).await?; } } } ServerMessageBody::Authenticate(body) => { writer.write_all(b"AUTHENTICATE ").await?; writer.write_all(body.as_bytes()).await?; } ServerMessageBody::N001Welcome { client, text } => { writer.write_all(b"001 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" :").await?; writer.write_all(text.as_bytes()).await?; } ServerMessageBody::N002YourHost { client, text } => { writer.write_all(b"002 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" :").await?; writer.write_all(text.as_bytes()).await?; } ServerMessageBody::N003Created { client, text } => { writer.write_all(b"003 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" :").await?; writer.write_all(text.as_bytes()).await?; } ServerMessageBody::N004MyInfo { client, hostname, softname, } => { writer.write_all(b"004 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" ").await?; writer.write_all(hostname.as_bytes()).await?; writer.write_all(b" ").await?; writer.write_all(softname.as_bytes()).await?; writer.write_all(b" r CFILPQbcefgijklmnopqrstvz").await?; // TODO remove hardcoded modes } ServerMessageBody::N005ISupport { client, params } => { writer.write_all(b"005 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" ").await?; writer.write_all(params.as_bytes()).await?; writer.write_all(b" :are supported by this server").await?; } ServerMessageBody::N221UserModeIs { client, modes } => { writer.write_all(b"221 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" ").await?; writer.write_all(modes.as_bytes()).await?; } ServerMessageBody::N315EndOfWho { client, mask, msg } => { writer.write_all(b"315 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" ").await?; mask.write_async(writer).await?; writer.write_all(b" :").await?; writer.write_all(msg.as_bytes()).await?; } ServerMessageBody::N332Topic { client, chat, topic, } => { writer.write_all(b"332 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" ").await?; chat.write_async(writer).await?; writer.write_all(b" :").await?; writer.write_all(topic.as_bytes()).await?; } ServerMessageBody::N352WhoReply { client, username, host, server, flags, nickname, hops, realname, } => { writer.write_all(b"352 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" * ").await?; writer.write_all(username.as_bytes()).await?; writer.write_all(b" ").await?; writer.write_all(host.as_bytes()).await?; writer.write_all(b" ").await?; writer.write_all(server.as_bytes()).await?; writer.write_all(b" ").await?; match flags { AwayStatus::Here => writer.write_all(b"H").await?, AwayStatus::Gone => writer.write_all(b"G").await?, } writer.write_all(b" ").await?; writer.write_all(nickname.as_bytes()).await?; writer.write_all(b" :").await?; writer.write_all(hops.to_string().as_bytes()).await?; writer.write_all(b" ").await?; writer.write_all(realname.as_bytes()).await?; } ServerMessageBody::N353NamesReply { client, chan, members, } => { writer.write_all(b"353 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" = ").await?; chan.write_async(writer).await?; writer.write_all(b" :").await?; for member in members { writer .write_all(member.prefix.to_string().as_bytes()) .await?; writer.write_all(member.nick.as_bytes()).await?; writer.write_all(b" ").await?; } } ServerMessageBody::N366NamesReplyEnd { client, chan } => { writer.write_all(b"366 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" ").await?; chan.write_async(writer).await?; writer.write_all(b" :End of /NAMES list").await?; } ServerMessageBody::N474BannedFromChan { client, chan, message, } => { writer.write_all(b"474 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" ").await?; chan.write_async(writer).await?; writer.write_all(b" :").await?; writer.write_all(message.as_bytes()).await?; } ServerMessageBody::N502UsersDontMatch { client, message } => { writer.write_all(b"502 ").await?; writer.write_all(client.as_bytes()).await?; writer.write_all(b" ").await?; writer.write_all(b" :").await?; writer.write_all(message.as_bytes()).await?; } ServerMessageBody::N900LoggedIn { nick, address, account, message } => { writer.write_all(b"900 ").await?; writer.write_all(nick.as_bytes()).await?; writer.write_all(b" ").await?; writer.write_all(address.as_bytes()).await?; writer.write_all(b" ").await?; writer.write_all(account.as_bytes()).await?; writer.write_all(b" :").await?; writer.write_all(message.as_bytes()).await?; } ServerMessageBody::N903SaslSuccess { nick, message } => { writer.write_all(b"903 ").await?; writer.write_all(nick.as_bytes()).await?; writer.write_all(b" :").await?; writer.write_all(message.as_bytes()).await?; } } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq)] pub enum CapSubBody { Ls(Str), Ack(Str), } #[derive(Clone, Debug, PartialEq, Eq)] pub enum AwayStatus { Here, Gone, } fn server_message_body(input: &str) -> IResult<&str, ServerMessageBody> { alt(( server_message_body_notice, server_message_body_ping, server_message_body_pong, server_message_body_cap, ))(input) } fn server_message_body_notice(input: &str) -> IResult<&str, ServerMessageBody> { let (input, _) = tag("NOTICE ")(input)?; let (input, first_target) = receiver(input)?; let (input, _) = tag(" :")(input)?; let (input, text) = token(input)?; let first_target = first_target.into(); let text = text.into(); Ok(( input, ServerMessageBody::Notice { first_target, rest_targets: vec![], text, }, )) } fn server_message_body_ping(input: &str) -> IResult<&str, ServerMessageBody> { let (input, _) = tag("PING ")(input)?; let (input, token) = token(input)?; Ok((input, ServerMessageBody::Ping { token: token.into() })) } fn server_message_body_pong(input: &str) -> IResult<&str, ServerMessageBody> { let (input, _) = tag("PONG ")(input)?; let (input, from) = receiver(input)?; let (input, _) = tag(" :")(input)?; let (input, token) = token(input)?; Ok(( input, ServerMessageBody::Pong { from: from.into(), token: token.into(), }, )) } fn server_message_body_cap(input: &str) -> IResult<&str, ServerMessageBody> { let (input, _) = tag("CAP ")(input)?; let (input, from) = receiver(input)?; let (input, _) = tag(" LS :")(input)?; let (input, token) = token(input)?; Ok(( input, ServerMessageBody::Cap { target: from.into(), subcmd: CapSubBody::Ls(token.into()), }, )) } #[cfg(test)] mod test { use assert_matches::*; use super::*; use crate::testkit::*; #[test] fn test_server_message_notice() { let input = "NOTICE * :*** Looking up your hostname...\r\n"; let expected = ServerMessage { tags: vec![], sender: None, body: ServerMessageBody::Notice { first_target: "*".into(), rest_targets: vec![], text: "*** Looking up your hostname...".into(), }, }; let result = server_message(input); assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result)); let mut bytes = vec![]; sync_future(expected.write_async(&mut bytes)).unwrap().unwrap(); assert_eq!(bytes, input.as_bytes()); } #[test] fn test_server_message_pong() { let input = "PONG server.example :LAG004911\r\n"; let expected = ServerMessage { tags: vec![], sender: None, body: ServerMessageBody::Pong { from: "server.example".into(), token: "LAG004911".into(), }, }; let result = server_message(input); assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result)); let mut bytes = vec![]; sync_future(expected.write_async(&mut bytes)).unwrap().unwrap(); assert_eq!(bytes, input.as_bytes()); } #[test] fn test_server_message_cap_ls() { let input = "CAP * LS :sasl\r\n"; let expected = ServerMessage { tags: vec![], sender: None, body: ServerMessageBody::Cap { target: "*".into(), subcmd: CapSubBody::Ls("sasl".into()), }, }; let result = server_message(input); assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result)); let mut bytes = vec![]; sync_future(expected.write_async(&mut bytes)).unwrap().unwrap(); assert_eq!(bytes, input.as_bytes()); } }