From e6cae0179d49c667ec9e7357338ff2cd18cf760c Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 1 Feb 2021 22:09:55 +0000 Subject: [PATCH] User nick state sharing & don't duplicate PRIVMSGs back to the user --- Cargo.lock | 19 +++++++++++++++++++ titanirc-codec/src/wire.rs | 24 +++++++----------------- titanirc-server/Cargo.toml | 5 ++++- titanirc-server/src/entities/channel/events.rs | 8 +++++--- titanirc-server/src/entities/channel/mod.rs | 32 +++++++++++++++++++++++--------- titanirc-server/src/entities/mod.rs | 5 +++-- titanirc-server/src/entities/user/commands.rs | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------------------------- titanirc-server/src/entities/user/mod.rs | 31 ++++++++++++++++++++++++------- titanirc-server/src/server.rs | 19 +++++++++++++++---- titanirc-types/Cargo.toml | 3 ++- titanirc-types/src/lib.rs | 202 +++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------------------------------------------------------- titanirc-types/src/primitives.rs | 319 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- titanirc-types/src/protocol/commands.rs | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ titanirc-types/src/protocol/mod.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ titanirc-types/src/protocol/primitives.rs | 328 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ titanirc-types/src/protocol/replies.rs | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ titanirc-types/src/replies.rs | 212 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 17 files changed, 932 insertions(+), 804 deletions(-) delete mode 100644 titanirc-types/src/primitives.rs create mode 100644 titanirc-types/src/protocol/commands.rs create mode 100644 titanirc-types/src/protocol/mod.rs create mode 100644 titanirc-types/src/protocol/primitives.rs create mode 100644 titanirc-types/src/protocol/replies.rs delete mode 100644 titanirc-types/src/replies.rs diff --git a/Cargo.lock b/Cargo.lock index c95ee74..dd0dc7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,12 @@ dependencies = [ ] [[package]] +name = "arc-swap" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d7d63395147b81a9e570bcc6243aaf71c017bd666d4909cfef0085bdda8d73" + +[[package]] name = "arrayvec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -894,9 +900,11 @@ version = "0.1.0" dependencies = [ "actix", "actix-rt", + "arc-swap", "async-stream", "bytes", "clap", + "derive_more", "displaydoc", "futures-util", "thiserror", @@ -904,12 +912,14 @@ dependencies = [ "titanirc-types", "tokio", "tokio-util", + "uuid", ] [[package]] name = "titanirc-types" version = "0.1.0" dependencies = [ + "arc-swap", "bytes", "derive_more", "nom", @@ -1043,6 +1053,15 @@ dependencies = [ ] [[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + +[[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/titanirc-codec/src/wire.rs b/titanirc-codec/src/wire.rs index 6da1ea9..f96faf6 100644 --- a/titanirc-codec/src/wire.rs +++ b/titanirc-codec/src/wire.rs @@ -1,5 +1,5 @@ use bytes::BytesMut; -use titanirc_types::Command; +use titanirc_types::{protocol::commands::Command, RegisteredNick}; use tokio_util::codec::Decoder as FrameDecoder; pub const MAX_LENGTH: usize = 1024; @@ -50,35 +50,25 @@ impl FrameDecoder for Decoder { pub struct Encoder { server_name: &'static str, - pub nick: Option, + nick: RegisteredNick, } impl Encoder { #[must_use] - pub fn new(server_name: &'static str) -> Self { - Self { - server_name, - nick: None, - } + pub fn new(server_name: &'static str, nick: RegisteredNick) -> Self { + Self { server_name, nick } } } -impl tokio_util::codec::Encoder> for Encoder { +impl tokio_util::codec::Encoder> for Encoder { type Error = std::io::Error; fn encode( &mut self, - item: titanirc_types::ServerMessage, + item: titanirc_types::protocol::ServerMessage, dst: &mut BytesMut, ) -> Result<(), Self::Error> { - item.write( - &self.server_name, - match &self.nick { - Some(v) => v, - None => "*", - }, - dst, - ); + item.write(&self.server_name, &self.nick, dst); dst.extend_from_slice(b"\r\n"); Ok(()) } diff --git a/titanirc-server/Cargo.toml b/titanirc-server/Cargo.toml index bf7b7f8..1169f17 100644 --- a/titanirc-server/Cargo.toml +++ b/titanirc-server/Cargo.toml @@ -19,4 +19,7 @@ thiserror = "1" displaydoc = "0.1" clap = "3.0.0-beta.2" futures-util = "0.3" -bytes = "1.0" \ No newline at end of file +bytes = "1.0" +uuid = { version = "0.8", features = ["v4"] } +derive_more = "0.99" +arc-swap = "1.2" \ No newline at end of file diff --git a/titanirc-server/src/entities/channel/events.rs b/titanirc-server/src/entities/channel/events.rs index f40b862..4662145 100644 --- a/titanirc-server/src/entities/channel/events.rs +++ b/titanirc-server/src/entities/channel/events.rs @@ -1,5 +1,6 @@ -use crate::entities::user::User; +use crate::entities::user::{User, UserUuid}; use actix::prelude::*; +use titanirc_types::RegisteredNick; pub type JoinResult = Result; @@ -9,7 +10,8 @@ pub type JoinResult = Result; #[rtype(result = "JoinResult")] pub struct Join { pub channel_name: String, - pub nick: String, + pub user_uuid: UserUuid, + pub nick: RegisteredNick, pub user: Addr, } @@ -24,7 +26,7 @@ pub enum JoinError { #[rtype(result = "")] pub struct JoinBroadcast { pub channel_name: String, - pub nick: String, + pub nick: RegisteredNick, } impl From for JoinBroadcast { diff --git a/titanirc-server/src/entities/channel/mod.rs b/titanirc-server/src/entities/channel/mod.rs index b7a503f..17dffec 100644 --- a/titanirc-server/src/entities/channel/mod.rs +++ b/titanirc-server/src/entities/channel/mod.rs @@ -1,9 +1,9 @@ pub mod events; use actix::prelude::*; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; -use crate::entities::user::User; +use crate::entities::user::{User, UserUuid}; use self::events::JoinBroadcast; @@ -16,18 +16,22 @@ pub struct Handle { /// An IRC channel. pub struct Channel { - pub members: Vec>, + pub members: HashMap>, } impl Channel { pub fn new() -> Self { Self { - members: Vec::new(), + members: HashMap::new(), } } // TODO: add a flag not to broadcast messages to the source so PRIVMSGs dont get duplicated - fn broadcast_message(&self, msg: M) -> impl Future + fn broadcast_message( + &self, + skip_sender: Option, + msg: M, + ) -> impl Future where M: Message + Send + Sync, M::Result: Send, @@ -38,7 +42,13 @@ impl Channel { let msg = Arc::new(msg); - for member in &self.members { + for (uuid, member) in &self.members { + if let Some(skip_sender) = &skip_sender { + if skip_sender == uuid { + continue; + } + } + futures.push(member.send(msg.clone())); } @@ -56,10 +66,10 @@ impl actix::Handler for Channel { type Result = events::JoinResult; fn handle(&mut self, msg: events::Join, ctx: &mut Self::Context) -> Self::Result { - self.members.push(msg.user.clone()); + self.members.insert(msg.user_uuid, msg.user.clone()); ctx.spawn( - self.broadcast_message(JoinBroadcast::from(msg)) + self.broadcast_message(None, JoinBroadcast::from(msg)) .into_actor(self), ); @@ -77,6 +87,10 @@ impl actix::Handler for Channel { msg: super::common_events::Message, ctx: &mut Self::Context, ) -> Self::Result { - ctx.spawn(self.broadcast_message(msg).into_actor(self)); + // TODO: don't allow messages from unconnected clients + ctx.spawn( + self.broadcast_message(Some(msg.user_uuid), msg) + .into_actor(self), + ); } } diff --git a/titanirc-server/src/entities/mod.rs b/titanirc-server/src/entities/mod.rs index 1b5829a..90cbc3f 100644 --- a/titanirc-server/src/entities/mod.rs +++ b/titanirc-server/src/entities/mod.rs @@ -7,8 +7,9 @@ pub mod common_events { #[derive(Debug, Message)] #[rtype(result = "")] pub struct Message { - pub from: String, - pub to: titanirc_types::Receiver<'static>, + pub from: titanirc_types::RegisteredNick, + pub user_uuid: crate::entities::user::UserUuid, + pub to: titanirc_types::protocol::primitives::Receiver<'static>, pub message: String, } } diff --git a/titanirc-server/src/entities/user/commands.rs b/titanirc-server/src/entities/user/commands.rs index 0e00f9d..6d6eadb 100644 --- a/titanirc-server/src/entities/user/commands.rs +++ b/titanirc-server/src/entities/user/commands.rs @@ -1,10 +1,14 @@ //! Handlers for commands originating from a user. -use std::time::Instant; +use std::{sync::Arc, time::Instant}; use actix::{Actor, AsyncContext, StreamHandler, WrapFuture}; -use titanirc_types::{ - Command, JoinCommand, ModeCommand, MotdCommand, NickCommand, PrivmsgCommand, VersionCommand, +use titanirc_types::protocol::{ + commands::{ + Command, JoinCommand, ModeCommand, MotdCommand, NickCommand, PrivmsgCommand, VersionCommand, + }, + primitives, + replies::Reply, }; pub trait CommandHandler: Actor { @@ -38,14 +42,13 @@ impl CommandHandler> for super::User { NickCommand { nick, .. }: NickCommand<'static>, _ctx: &mut Self::Context, ) { - self.nick = Some(std::str::from_utf8(&nick.0[..]).unwrap().to_string()); - (*self.writer.encoder_mut()).nick = self.nick.clone(); - - self.writer.write(titanirc_types::Reply::RplWelcome.into()); - self.writer.write(titanirc_types::Reply::RplYourHost.into()); - self.writer.write(titanirc_types::Reply::RplCreated.into()); - self.writer.write(titanirc_types::Reply::RplMyInfo.into()); - self.writer.write(titanirc_types::Reply::RplISupport.into()); + self.nick.set(Arc::new(nick.to_bytes())); + + self.writer.write(Reply::RplWelcome.into()); + self.writer.write(Reply::RplYourHost.into()); + self.writer.write(Reply::RplCreated.into()); + self.writer.write(Reply::RplMyInfo.into()); + self.writer.write(Reply::RplISupport.into()); // LUSERS // RPL_UMODEIS // MOTD @@ -58,29 +61,31 @@ impl CommandHandler> for super::User { JoinCommand { channel, .. }: JoinCommand<'static>, ctx: &mut Self::Context, ) { - if let Some(ref nick) = self.nick { - let server_addr = self.server.clone(); - let ctx_addr = ctx.address(); - let nick = nick.clone(); - - // TODO: needs to send MODE & NAMES (353, 366) - ctx.spawn( - async move { - server_addr - .send(crate::entities::channel::events::Join { - channel_name: std::str::from_utf8(&channel.0[..]).unwrap().to_string(), - user: ctx_addr, - nick, - }) - .await - .unwrap() - .unwrap(); - - println!("joined chan!"); - } - .into_actor(self), - ); - } + // TODO: ensure the user has a nick set before they join a channel!!! + + let server_addr = self.server.clone(); + let ctx_addr = ctx.address(); + let nick = self.nick.clone(); + let user_uuid = self.session_id; + + // TODO: needs to send MODE & NAMES (353, 366) + ctx.spawn( + async move { + server_addr + .send(crate::entities::channel::events::Join { + channel_name: std::str::from_utf8(&channel.0[..]).unwrap().to_string(), + user_uuid, + user: ctx_addr, + nick, + }) + .await + .unwrap() + .unwrap(); + + println!("joined chan!"); + } + .into_actor(self), + ); } } @@ -90,8 +95,7 @@ impl CommandHandler> for super::User { ModeCommand { mode, .. }: ModeCommand<'static>, _ctx: &mut Self::Context, ) { - self.writer - .write(titanirc_types::Reply::RplUmodeIs(mode).into()) + self.writer.write(Reply::RplUmodeIs(mode).into()) } } @@ -101,20 +105,13 @@ impl CommandHandler> for super::User { static MOTD1: bytes::Bytes = bytes::Bytes::from_static(b"Hello, welcome to this server!"); static MOTD2: bytes::Bytes = bytes::Bytes::from_static(b"it's very cool!"); - self.writer.write( - titanirc_types::Reply::RplMotdStart(titanirc_types::ServerName( - SERVER_NAME.clone().into(), - )) - .into(), - ); - self.writer.write( - titanirc_types::Reply::RplMotd(titanirc_types::FreeText(MOTD1.clone().into())).into(), - ); - self.writer.write( - titanirc_types::Reply::RplMotd(titanirc_types::FreeText(MOTD2.clone().into())).into(), - ); self.writer - .write(titanirc_types::Reply::RplEndOfMotd.into()); + .write(Reply::RplMotdStart(primitives::ServerName(SERVER_NAME.clone().into())).into()); + self.writer + .write(Reply::RplMotd(primitives::FreeText(MOTD1.clone().into())).into()); + self.writer + .write(Reply::RplMotd(primitives::FreeText(MOTD2.clone().into())).into()); + self.writer.write(Reply::RplEndOfMotd.into()); } } @@ -125,11 +122,11 @@ impl CommandHandler> for super::User { bytes::Bytes::from_static(b"https://github.com/MITBorg/titanirc"); self.writer.write( - titanirc_types::Reply::RplVersion( + titanirc_types::protocol::replies::Reply::RplVersion( clap::crate_version!().to_string(), "release".to_string(), - titanirc_types::ServerName(SERVER_NAME.clone().into()), - titanirc_types::FreeText(INFO.clone().into()), + primitives::ServerName(SERVER_NAME.clone().into()), + primitives::FreeText(INFO.clone().into()), ) .into(), ) @@ -146,21 +143,22 @@ impl CommandHandler> for super::User { }: PrivmsgCommand<'static>, ctx: &mut Self::Context, ) { - if let Some(nick) = &self.nick { - let msg = crate::entities::common_events::Message { - from: nick.clone(), // TODO: this need to be a full user string i think - to: receiver, - message: free_text.to_string(), - }; - - let server_addr = self.server.clone(); - - ctx.spawn( - async move { - server_addr.send(msg).await.unwrap(); - } - .into_actor(self), - ); - } + // TODO: ensure the user has a nick before sending messages!! + + let msg = crate::entities::common_events::Message { + from: self.nick.clone(), // TODO: this need to be a full user string i think + user_uuid: self.session_id, + to: receiver, + message: free_text.to_string(), + }; + + let server_addr = self.server.clone(); + + ctx.spawn( + async move { + server_addr.send(msg).await.unwrap(); + } + .into_actor(self), + ); } } diff --git a/titanirc-server/src/entities/user/mod.rs b/titanirc-server/src/entities/user/mod.rs index 9de081f..8a55af8 100644 --- a/titanirc-server/src/entities/user/mod.rs +++ b/titanirc-server/src/entities/user/mod.rs @@ -3,19 +3,31 @@ pub mod events; use crate::{entities::channel::events::JoinBroadcast, server::Server}; -use std::sync::Arc; +use std::{collections::HashMap, hash::Hash, sync::Arc}; use actix::{ io::{FramedWrite, WriteHandler}, prelude::*, }; +use bytes::Bytes; +use derive_more::Deref; use std::time::{Duration, Instant}; use titanirc_types::{ - Channel, FreeText, JoinCommand, Nick, PrivmsgCommand, Receiver, ServerMessage, Source, + protocol::commands::{JoinCommand, PrivmsgCommand}, + protocol::primitives::{Channel, FreeText, Nick, Receiver}, + protocol::replies::Source, + protocol::ServerMessage, + RegisteredNick, }; use tokio::{io::WriteHalf, net::TcpStream}; +use uuid::Uuid; + +#[derive(Debug, Deref, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[allow(clippy::module_name_repetitions)] +pub struct UserUuid(Uuid); pub struct User { + pub session_id: UserUuid, pub server: Addr, pub writer: FramedWrite< WriteHalf, @@ -23,7 +35,8 @@ pub struct User { >>::Error, >, pub last_active: Instant, - pub nick: Option, + pub nick: RegisteredNick, + pub channels: HashMap, crate::entities::channel::Handle>, } // TODO: broadcast a leave to all the user's channels on actor shutdown @@ -32,12 +45,15 @@ impl User { pub fn new( server: Addr, writer: FramedWrite, titanirc_codec::Encoder>, + nick: RegisteredNick, ) -> Self { Self { + session_id: UserUuid(Uuid::new_v4()), server, writer, last_active: Instant::now(), - nick: None, + nick, + channels: HashMap::new(), } } } @@ -51,7 +67,8 @@ fn schedule_ping(ctx: &mut ::Context) { ctx.stop(); } - act.writer.write(titanirc_types::ServerMessage::Ping); + act.writer + .write(titanirc_types::protocol::ServerMessage::Ping); schedule_ping(ctx); }); } @@ -74,7 +91,7 @@ impl actix::Handler> for User { fn handle(&mut self, msg: Arc, _ctx: &mut Self::Context) -> Self::Result { self.writer.write(ServerMessage::Command( - Source::User(Nick(msg.nick.as_bytes().into())), + Source::User(Nick((*msg.nick.load().unwrap()).clone().into())), JoinCommand { _phantom: std::marker::PhantomData, channel: Channel(msg.channel_name.as_bytes().into()), @@ -93,7 +110,7 @@ impl actix::Handler> for User { _ctx: &mut Self::Context, ) -> Self::Result { self.writer.write(ServerMessage::Command( - Source::User(Nick(msg.from.as_bytes().into())), + Source::User(Nick((*msg.from.load().unwrap()).clone().into())), PrivmsgCommand { _phantom: std::marker::PhantomData, free_text: FreeText(msg.message.as_bytes().into()), diff --git a/titanirc-server/src/server.rs b/titanirc-server/src/server.rs index 3002f37..f9de0e0 100644 --- a/titanirc-server/src/server.rs +++ b/titanirc-server/src/server.rs @@ -3,7 +3,7 @@ use crate::entities::{channel::Channel, user::User}; use std::{collections::HashMap, net::SocketAddr}; use actix::{io::FramedWrite, prelude::*}; -use titanirc_types::Receiver; +use titanirc_types::{protocol::primitives::Receiver, RegisteredNick, UserIdent}; use tokio::net::TcpStream; use tokio_util::codec::FramedRead; @@ -16,12 +16,15 @@ use tokio_util::codec::FramedRead; pub struct Server { /// A list of known channels and the addresses to them. pub channels: HashMap>, + // A list of known connected users. + // pub users: Vec<(UserIdent, Addr)>, // todo: add this when we know how auth is gonna work } impl Server { pub fn new() -> Self { Self { channels: HashMap::new(), + // users: Vec::new(), } } } @@ -44,15 +47,22 @@ impl Handler for Server { println!("Accepted connection from {}", remote); User::create(move |ctx| { + let nick = RegisteredNick::new(); + let (read, write) = tokio::io::split(stream); let read = FramedRead::new(read, titanirc_codec::Decoder); - let write = - FramedWrite::new(write, titanirc_codec::Encoder::new("my.cool.server"), ctx); + let write = FramedWrite::new( + write, + titanirc_codec::Encoder::new("my.cool.server", nick.clone()), // TODO: this should take a UserIdent + ctx, + ); // Make our new `User` handle all events from this socket in `StreamHandler>`. ctx.add_stream(read); - User::new(server_ctx.address(), write) + // TODO: don't give the user a full server handle until they're authed + // ... only add the self.user to `user` then and only then. + User::new(server_ctx.address(), write, nick) }); } } @@ -61,6 +71,7 @@ impl Handler for Server { impl Handler for Server { type Result = ResponseActFuture; + // TODO: validate channel name fn handle( &mut self, msg: crate::entities::channel::events::Join, diff --git a/titanirc-types/Cargo.toml b/titanirc-types/Cargo.toml index b71e64f..ce18f98 100644 --- a/titanirc-types/Cargo.toml +++ b/titanirc-types/Cargo.toml @@ -11,4 +11,5 @@ paste = "1.0" nom = "6.1" derive_more = "0.99" bytes = "1.0" -nom-bytes = { git = "https://github.com/w4/nom-bytes" } \ No newline at end of file +nom-bytes = { git = "https://github.com/w4/nom-bytes" } +arc-swap = "1.2" \ No newline at end of file diff --git a/titanirc-types/src/lib.rs b/titanirc-types/src/lib.rs index ad5b569..c03fe03 100644 --- a/titanirc-types/src/lib.rs +++ b/titanirc-types/src/lib.rs @@ -1,178 +1,58 @@ #![deny(clippy::pedantic)] #![allow(clippy::missing_errors_doc)] -mod primitives; -mod replies; - -pub use crate::primitives::*; -pub use crate::replies::{Reply, ServerMessage, Source}; +use std::{hash::Hash, sync::Arc}; use bytes::Bytes; -use nom::{ - bytes::complete::{tag, take_till}, - error::Error as NomError, -}; -use nom_bytes::BytesWrapper; - -fn parse_optional_source(input: BytesWrapper) -> nom::IResult { - let (rest, _) = tag(":".as_bytes())(input)?; - let (rest, _) = take_till(|c| c == b' ')(rest)?; - tag(" ".as_bytes())(rest) -} - -macro_rules! define_commands { - ( - $( - $name:ident$(( - $($param:ident$(<$($gen:tt),+>)?),* - ))? - ),* $(,)? - ) => { - paste::paste! { - /// All the commands that can be ran by a client, also provides a `Display` - /// implementation that serialises the command for sending over the wire, - /// ie. for forwarding. - #[derive(Debug)] - pub enum Command<'a> { - $([<$name:camel>]([<$name:camel Command>]<'a>)),* - } - - $(const [<$name _BYTES>]: &[u8] = stringify!($name).as_bytes();)* - - impl Command<'_> { - /// Parses a command from the wire, returning an `Err` if the command was unparsable or - /// `Ok(None)` if the command was unrecognsied. The given `Bytes` should have the CRLF - /// stripped. - pub fn parse(input: Bytes) -> Result, nom::Err>> { - let mut input = BytesWrapper::from(input); - - // skip the optional source at the start of the message - if let Ok((input_source_stripped, _)) = parse_optional_source(input.clone()) { - input = input_source_stripped; - } - - let (params, command) = take_till(|c| c == b' ')(input)?; - - match command.to_ascii_uppercase().as_ref() { - $([<$name _BYTES>] => Ok(Some(Self::[<$name:camel>]([<$name:camel Command>]::parse(params)?)))),*, - _ => Ok(None) - } - } - } - - /// Serialises the command for sending over the wire. - impl std::fmt::Display for Command<'_> { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - $(Self::[<$name:camel>](cmd) => cmd.fmt(fmt)),* - } - } - } - - $( - #[derive(Debug)] - pub struct [<$name:camel Command>]<'a> { - pub _phantom: std::marker::PhantomData<&'a ()>, - $($(pub [<$param:snake>]: $param$(<$($gen),+>)?),*),* - } - - impl [<$name:camel Command>]<'_> { - /// Parses the command's arguments, with each parameter separated by a space. - #[allow(unused_variables)] - pub fn parse(rest: BytesWrapper) -> Result>> { - $( - $( - let (rest, _) = tag(" ".as_bytes())(rest)?; - let (rest, [<$param:snake>]) = $param::parse(rest)?; - )* - )* - Ok(Self { - _phantom: std::marker::PhantomData, - $($([<$param:snake>]),*),* - }) - } - } +pub mod protocol; - /// Serialises the command's arguments for sending over the wire, joining - /// all the arguments separating them with a space. - impl std::fmt::Display for [<$name:camel Command>]<'_> { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - fmt.write_str(stringify!($name))?; - - $( - $( - fmt.write_str(" ")?; - self.[<$param:snake>].fmt(fmt)?; - )* - )* - - Ok(()) - } - } - - impl<'a> Into> for [<$name:camel Command>]<'a> { - fn into(self) -> Command<'a> { - Command::[<$name:camel>](self) - } - } - )* - } - }; +#[derive(Debug, Clone)] +pub struct UserIdent { + nick: RegisteredNick, + username: Arc, + host: Arc, } -define_commands! { - USER(Username<'a>, HostName<'a>, ServerName<'a>, RealName<'a>), - NICK(Nick<'a>), - - MOTD, - VERSION, - HELP, - USERS, - TIME, - PONG(ServerName<'a>), - PING(ServerName<'a>), - LIST, - MODE(Nick<'a>, Mode<'a>), - WHOIS(Nick<'a>), - USERHOST(Nick<'a>), - USERIP(Nick<'a>), - JOIN(Channel<'a>), - - PRIVMSG(Receiver<'a>, FreeText<'a>), -} +/// A reference to a user's nickname. The actual nickname can be loaded using +/// `UserNick::load()`, however this username can be changed fairly quickly, +/// so the loaded value shouldn't be stored. +/// +/// The user's nickname can be changed using `UserNick::set()` however this +/// doesn't do any validation that the user is actually allowed to use the +/// nick, nor does it send out any events alerting the users of the server +/// that the user's nick has changed. +#[derive(Debug, Clone)] +#[allow(clippy::clippy::module_name_repetitions)] +pub struct RegisteredNick(Arc>); + +impl RegisteredNick { + #[must_use] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self(Arc::new(arc_swap::ArcSwapOption::empty())) + } -#[cfg(test)] -mod tests { - use super::Command; - use bytes::Bytes; + #[must_use] + pub fn load(&self) -> Option> { + self.0.load().clone() + } - #[test] - fn parse_empty() { - assert!(matches!(Command::parse(Bytes::from_static(b"")), Ok(None))); + pub fn set(&self, nick: Arc) { + self.0.store(Some(nick)) } +} - #[test] - fn parse_privmsg() { - assert!(matches!( - Command::parse(Bytes::from_static(b"PRIVMSG foo :baz")), - Ok(Some(Command::Privmsg(super::PrivmsgCommand { - receiver: super::Receiver::User(super::Nick(nick)), - free_text: super::primitives::FreeText(msg), - _phantom: std::marker::PhantomData, - }))) if &*nick == b"foo" && &*msg == b"baz" - )) +impl Hash for RegisteredNick { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.0).hash(state) } +} - #[test] - fn parse_privmsg_opt_source() { - assert!(matches!( - Command::parse(Bytes::from_static(b":some-fake-source!dude@nice PRIVMSG foo :baz")), - Ok(Some(Command::Privmsg(super::PrivmsgCommand { - receiver: super::Receiver::User(super::Nick(nick)), - free_text: super::primitives::FreeText(msg), - _phantom: std::marker::PhantomData, - }))) if &*nick == b"foo" && &*msg == b"baz" - )) +impl PartialEq for RegisteredNick { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) } } + +impl Eq for RegisteredNick {} diff --git a/titanirc-types/src/primitives.rs b/titanirc-types/src/primitives.rs deleted file mode 100644 index 0be35dc..0000000 --- a/titanirc-types/src/primitives.rs +++ /dev/null @@ -1,319 +0,0 @@ -use bytes::Bytes; -use derive_more::{Deref, From}; -use nom::{ - bytes::complete::{tag, take_till}, - combinator::iterator, - sequence::terminated, - IResult, -}; -use nom_bytes::BytesWrapper; - -pub trait ValidatingParser { - fn validate(bytes: &[u8]) -> bool; -} - -pub trait PrimitiveParser { - fn parse(bytes: BytesWrapper) -> IResult - where - Self: Sized; -} - -/// A `Cow`-like implementation where `Owned` is a `bytes::Bytes` and `Borrowed` -/// is `&[u8]`. -#[derive(Debug, From)] -pub enum BytesCow<'a> { - Owned(Bytes), - Borrowed(&'a [u8]), -} - -impl From for BytesCow<'_> { - fn from(other: BytesWrapper) -> Self { - Self::Owned(other.into()) - } -} - -impl Clone for BytesCow<'_> { - fn clone(&self) -> Self { - Self::Owned(match self { - Self::Owned(b) => b.clone(), - Self::Borrowed(b) => Bytes::copy_from_slice(b), - }) - } -} - -impl std::ops::Deref for BytesCow<'_> { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - match self { - Self::Owned(b) => &*b, - Self::Borrowed(b) => *b, - } - } -} - -macro_rules! noop_validator { - ($name:ty) => { - impl ValidatingParser for $name { - fn validate(_: &[u8]) -> bool { - true - } - } - }; -} - -macro_rules! free_text_primitive { - ($name:ty) => { - impl PrimitiveParser for $name { - fn parse(bytes: BytesWrapper) -> IResult { - let (rest, _) = tag(":".as_bytes())(bytes)?; - Ok((Bytes::new().into(), Self(rest.into()))) - } - } - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match std::str::from_utf8(&self.0[..]) { - Ok(v) => f.write_str(v), - Err(_e) => { - // todo: report this better - eprintln!("Invalid utf-8 in {}", stringify!($name)); - Err(std::fmt::Error) - } - } - } - } - }; -} - -macro_rules! space_terminated_primitive { - ($name:ty) => { - impl PrimitiveParser for $name { - fn parse(bytes: BytesWrapper) -> IResult { - let (rest, val) = take_till(|c| c == b' ')(bytes.clone())?; - - if !::validate(&val[..]) { - return Err(nom::Err::Failure(nom::error::Error::new( - bytes, - nom::error::ErrorKind::Verify, - ))); - } - - Ok((rest, Self(val.into()))) - } - } - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match std::str::from_utf8(&self.0[..]) { - Ok(v) => f.write_str(v), - Err(_e) => { - // todo: report this better - eprintln!("Invalid utf-8 in {}", stringify!($name)); - Err(std::fmt::Error) - } - } - } - } - }; -} - -macro_rules! space_delimited_display { - ($name:ty) => { - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // once the first iteration is complete, we'll start adding spaces before - // each nickname. - let mut space = false; - - for value in &self.0 { - if space { - f.write_str(" ")?; - } else { - space = true; - } - - value.fmt(f)?; - } - - Ok(()) - } - } - }; -} - -pub struct Letter; - -impl ValidatingParser for Letter { - fn validate(bytes: &[u8]) -> bool { - bytes - .iter() - .all(|c| (b'a'..=b'z').contains(c) || (b'A'..=b'Z').contains(c)) - } -} - -pub struct Number; - -impl ValidatingParser for Number { - fn validate(bytes: &[u8]) -> bool { - bytes.iter().all(|c| (b'0'..=b'9').contains(c)) - } -} - -pub struct Special; - -impl ValidatingParser for Special { - fn validate(bytes: &[u8]) -> bool { - const ALLOWED: &[u8] = &[b'-', b'[', b']', b'\\', b'`', b'^', b'{', b'}']; - - bytes.iter().all(|c| ALLOWED.contains(c)) - } -} - -#[derive(Debug, Deref, Clone, From)] -pub struct Username<'a>(pub BytesCow<'a>); -space_terminated_primitive!(Username<'_>); -noop_validator!(Username<'_>); - -#[derive(Debug, Deref, Clone, From)] -pub struct Mode<'a>(pub BytesCow<'a>); -space_terminated_primitive!(Mode<'_>); -noop_validator!(Mode<'_>); - -#[derive(Debug, Deref, Clone, From)] -pub struct HostName<'a>(pub BytesCow<'a>); -space_terminated_primitive!(HostName<'_>); -noop_validator!(HostName<'_>); - -#[derive(Debug, Deref, Clone, From)] -pub struct ServerName<'a>(pub BytesCow<'a>); -space_terminated_primitive!(ServerName<'_>); -noop_validator!(ServerName<'_>); - -#[derive(Debug, Deref, Clone, From)] -pub struct RealName<'a>(pub BytesCow<'a>); -space_terminated_primitive!(RealName<'_>); -noop_validator!(RealName<'_>); - -#[derive(Debug, Deref, Clone, From)] -pub struct Nick<'a>(pub BytesCow<'a>); -space_terminated_primitive!(Nick<'_>); - -// TODO: i feel like this would be better suited as a nom chomper to stop -// iterating over the string twice unnecessarily -impl ValidatingParser for Nick<'_> { - fn validate(bytes: &[u8]) -> bool { - if bytes.is_empty() { - return false; - } - - if !Letter::validate(&[bytes[0]]) { - return false; - } - - bytes[1..] - .iter() - .all(|c| Letter::validate(&[*c]) || Number::validate(&[*c]) || Special::validate(&[*c])) - } -} - -#[derive(Debug, Deref, Clone, From)] -pub struct Channel<'a>(pub BytesCow<'a>); -space_terminated_primitive!(Channel<'_>); -noop_validator!(Channel<'_>); - -#[derive(Debug, Deref, Clone, From)] -pub struct FreeText<'a>(pub BytesCow<'a>); -free_text_primitive!(FreeText<'_>); -noop_validator!(FreeText<'_>); - -#[derive(Debug, Deref, Clone, From)] -pub struct Nicks<'a>(pub Vec>); -space_delimited_display!(Nicks<'_>); - -impl PrimitiveParser for Nicks<'_> { - fn parse(bytes: BytesWrapper) -> IResult { - let mut it = iterator( - bytes, - terminated(take_till(|c| c == b' '), tag(" ".as_bytes())), - ); - - let parsed = it.map(|v| Nick(v.into())).collect(); - - it.finish() - .map(move |(remaining, _)| (remaining, Self(parsed))) - } -} - -#[derive(Debug, Clone)] -pub struct RightsPrefixedNick<'a>(pub Rights, pub Nick<'a>); - -impl std::fmt::Display for RightsPrefixedNick<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f)?; - self.1.fmt(f) - } -} - -#[derive(Debug, Deref, Clone, From)] -pub struct RightsPrefixedNicks<'a>(pub Vec>); -space_delimited_display!(RightsPrefixedNicks<'_>); - -#[derive(Debug, Clone)] -pub struct RightsPrefixedChannel<'a>(pub Rights, pub Nick<'a>); - -impl std::fmt::Display for RightsPrefixedChannel<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f)?; - self.1.fmt(f) - } -} - -#[derive(Debug, Deref, Clone, From)] -pub struct RightsPrefixedChannels<'a>(pub Vec>); -space_delimited_display!(RightsPrefixedChannels<'_>); - -#[derive(Debug, Copy, Clone)] -pub enum Rights { - Op, - Voice, -} - -impl std::fmt::Display for Rights { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Self::Op => "@", - Self::Voice => "+", - }) - } -} - -#[derive(Debug, From, Clone)] -pub enum Receiver<'a> { - User(Nick<'a>), - Channel(Channel<'a>), -} - -impl std::ops::Deref for Receiver<'_> { - type Target = str; - - fn deref(&self) -> &Self::Target { - std::str::from_utf8(match self { - Self::User(nick) => &*nick, - Self::Channel(channel) => &*channel, - }) - .unwrap() - } -} - -impl PrimitiveParser for Receiver<'_> { - fn parse(bytes: BytesWrapper) -> IResult { - if bytes.get(0) == Some(&b'#') { - let (rest, channel) = Channel::parse(bytes)?; - Ok((rest, Self::Channel(channel))) - } else { - let (rest, nick) = Nick::parse(bytes)?; - Ok((rest, Self::User(nick))) - } - } -} diff --git a/titanirc-types/src/protocol/commands.rs b/titanirc-types/src/protocol/commands.rs new file mode 100644 index 0000000..542b9b5 --- /dev/null +++ b/titanirc-types/src/protocol/commands.rs @@ -0,0 +1,171 @@ +use super::primitives::*; + +use bytes::Bytes; +use nom::{ + bytes::complete::{tag, take_till}, + error::Error as NomError, +}; +use nom_bytes::BytesWrapper; + +fn parse_optional_source(input: BytesWrapper) -> nom::IResult { + let (rest, _) = tag(":".as_bytes())(input)?; + let (rest, _) = take_till(|c| c == b' ')(rest)?; + tag(" ".as_bytes())(rest) +} + +macro_rules! define_commands { + ( + $( + $name:ident$(( + $($param:ident$(<$($gen:tt),+>)?),* + ))? + ),* $(,)? + ) => { + paste::paste! { + /// All the commands that can be ran by a client, also provides a `Display` + /// implementation that serialises the command for sending over the wire, + /// ie. for forwarding. + #[derive(Debug)] + pub enum Command<'a> { + $([<$name:camel>]([<$name:camel Command>]<'a>)),* + } + + $(const [<$name _BYTES>]: &[u8] = stringify!($name).as_bytes();)* + + impl Command<'_> { + /// Parses a command from the wire, returning an `Err` if the command was unparsable or + /// `Ok(None)` if the command was unrecognsied. The given `Bytes` should have the CRLF + /// stripped. + pub fn parse(input: Bytes) -> Result, nom::Err>> { + let mut input = BytesWrapper::from(input); + + // skip the optional source at the start of the message + if let Ok((input_source_stripped, _)) = parse_optional_source(input.clone()) { + input = input_source_stripped; + } + + let (params, command) = take_till(|c| c == b' ')(input)?; + + match command.to_ascii_uppercase().as_ref() { + $([<$name _BYTES>] => Ok(Some(Self::[<$name:camel>]([<$name:camel Command>]::parse(params)?)))),*, + _ => Ok(None) + } + } + } + + /// Serialises the command for sending over the wire. + impl std::fmt::Display for Command<'_> { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + $(Self::[<$name:camel>](cmd) => cmd.fmt(fmt)),* + } + } + } + + $( + #[derive(Debug)] + pub struct [<$name:camel Command>]<'a> { + pub _phantom: std::marker::PhantomData<&'a ()>, + $($(pub [<$param:snake>]: $param$(<$($gen),+>)?),*),* + } + + impl [<$name:camel Command>]<'_> { + /// Parses the command's arguments, with each parameter separated by a space. + #[allow(unused_variables)] + pub fn parse(rest: BytesWrapper) -> Result>> { + $( + $( + let (rest, _) = tag(" ".as_bytes())(rest)?; + let (rest, [<$param:snake>]) = $param::parse(rest)?; + )* + )* + + Ok(Self { + _phantom: std::marker::PhantomData, + $($([<$param:snake>]),*),* + }) + } + } + + /// Serialises the command's arguments for sending over the wire, joining + /// all the arguments separating them with a space. + impl std::fmt::Display for [<$name:camel Command>]<'_> { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt.write_str(stringify!($name))?; + + $( + $( + fmt.write_str(" ")?; + self.[<$param:snake>].fmt(fmt)?; + )* + )* + + Ok(()) + } + } + + impl<'a> Into> for [<$name:camel Command>]<'a> { + fn into(self) -> Command<'a> { + Command::[<$name:camel>](self) + } + } + )* + } + }; +} + +define_commands! { + USER(Username<'a>, HostName<'a>, ServerName<'a>, RealName<'a>), + NICK(Nick<'a>), + + MOTD, + VERSION, + HELP, + USERS, + TIME, + PONG(ServerName<'a>), + PING(ServerName<'a>), + LIST, + MODE(Nick<'a>, Mode<'a>), + WHOIS(Nick<'a>), + USERHOST(Nick<'a>), + USERIP(Nick<'a>), + JOIN(Channel<'a>), + + PRIVMSG(Receiver<'a>, FreeText<'a>), +} + +#[cfg(test)] +mod tests { + use super::Command; + use bytes::Bytes; + + #[test] + fn parse_empty() { + assert!(matches!(Command::parse(Bytes::from_static(b"")), Ok(None))); + } + + #[test] + fn parse_privmsg() { + assert!(matches!( + Command::parse(Bytes::from_static(b"PRIVMSG foo :baz")), + Ok(Some(Command::Privmsg(super::PrivmsgCommand { + receiver: super::Receiver::User(super::Nick(nick)), + free_text: super::primitives::FreeText(msg), + _phantom: std::marker::PhantomData, + }))) if &*nick == b"foo" && &*msg == b"baz" + )) + } + + #[test] + fn parse_privmsg_opt_source() { + assert!(matches!( + Command::parse(Bytes::from_static(b":some-fake-source!dude@nice PRIVMSG foo :baz")), + Ok(Some(Command::Privmsg(super::PrivmsgCommand { + receiver: super::Receiver::User(super::Nick(nick)), + free_text: super::primitives::FreeText(msg), + _phantom: std::marker::PhantomData, + }))) if &*nick == b"foo" && &*msg == b"baz" + )) + } +} diff --git a/titanirc-types/src/protocol/mod.rs b/titanirc-types/src/protocol/mod.rs new file mode 100644 index 0000000..b5dead4 --- /dev/null +++ b/titanirc-types/src/protocol/mod.rs @@ -0,0 +1,54 @@ +pub mod commands; +pub mod primitives; +pub mod replies; + +use std::fmt::Write; + +use crate::RegisteredNick; + +/// A message to be sent to the client over the wire. +#[derive(Debug, derive_more::From)] +pub enum ServerMessage<'a> { + /// A `RPL_*`/`ERR_*` type from the IRC spec. + Reply(replies::Reply<'a>), + /// Normally a 'forwarded' message, ie. a `VERSION` for another client or + /// a `PRIVMSG`. + Command(replies::Source<'a>, commands::Command<'a>), // change Nick to whatever type nick!user@netmask is.. + /// A server ping to the client. + Ping, + /// A server pong to the client. + Pong, +} + +impl ServerMessage<'_> { + /// Writes out this `ServerMessage` to `dst`, in the expected format for the wire. + /// + /// This function omits the CRLF from the end of the line. + pub fn write( + self, + server_name: &str, + client_username: &RegisteredNick, + dst: &mut bytes::BytesMut, + ) { + match self { + Self::Reply(reply) => { + write!(dst, ":{} {} ", server_name, reply.code()).unwrap(); + match client_username.load() { + Some(v) => dst.extend_from_slice(&v[..]), + None => dst.write_char('*').unwrap(), + } + write!(dst, " {}", reply) + } + Self::Ping => write!(dst, "PING :{}", server_name), + Self::Pong => write!(dst, "PONG :{}", server_name), + Self::Command(source, command) => { + let source = match &source { + replies::Source::User(nick) => std::str::from_utf8(nick).unwrap(), + replies::Source::Server => server_name, + }; + write!(dst, ":{} {}", source, command) + } + } + .unwrap() + } +} diff --git a/titanirc-types/src/protocol/primitives.rs b/titanirc-types/src/protocol/primitives.rs new file mode 100644 index 0000000..66cf347 --- /dev/null +++ b/titanirc-types/src/protocol/primitives.rs @@ -0,0 +1,328 @@ +use bytes::Bytes; +use derive_more::{Deref, From}; +use nom::{ + bytes::complete::{tag, take_till}, + combinator::iterator, + sequence::terminated, + IResult, +}; +use nom_bytes::BytesWrapper; + +pub trait ValidatingParser { + fn validate(bytes: &[u8]) -> bool; +} + +pub trait PrimitiveParser { + fn parse(bytes: BytesWrapper) -> IResult + where + Self: Sized; +} + +/// A `Cow`-like implementation where `Owned` is a `bytes::Bytes` and `Borrowed` +/// is `&[u8]`. +#[derive(Debug, From)] +pub enum BytesCow<'a> { + Owned(Bytes), + Borrowed(&'a [u8]), +} + +impl BytesCow<'_> { + pub fn to_bytes(&self) -> Bytes { + match self { + Self::Owned(b) => b.clone(), + Self::Borrowed(b) => Bytes::copy_from_slice(b), + } + } +} + +impl From for BytesCow<'_> { + fn from(other: BytesWrapper) -> Self { + Self::Owned(other.into()) + } +} + +impl Clone for BytesCow<'_> { + fn clone(&self) -> Self { + Self::Owned(match self { + Self::Owned(b) => b.clone(), + Self::Borrowed(b) => Bytes::copy_from_slice(b), + }) + } +} + +impl std::ops::Deref for BytesCow<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + match self { + Self::Owned(b) => &*b, + Self::Borrowed(b) => *b, + } + } +} + +macro_rules! noop_validator { + ($name:ty) => { + impl ValidatingParser for $name { + fn validate(_: &[u8]) -> bool { + true + } + } + }; +} + +macro_rules! free_text_primitive { + ($name:ty) => { + impl PrimitiveParser for $name { + fn parse(bytes: BytesWrapper) -> IResult { + let (rest, _) = tag(":".as_bytes())(bytes)?; + Ok((Bytes::new().into(), Self(rest.into()))) + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match std::str::from_utf8(&self.0[..]) { + Ok(v) => f.write_str(v), + Err(_e) => { + // todo: report this better + eprintln!("Invalid utf-8 in {}", stringify!($name)); + Err(std::fmt::Error) + } + } + } + } + }; +} + +macro_rules! space_terminated_primitive { + ($name:ty) => { + impl PrimitiveParser for $name { + fn parse(bytes: BytesWrapper) -> IResult { + let (rest, val) = take_till(|c| c == b' ')(bytes.clone())?; + + if !::validate(&val[..]) { + return Err(nom::Err::Failure(nom::error::Error::new( + bytes, + nom::error::ErrorKind::Verify, + ))); + } + + Ok((rest, Self(val.into()))) + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match std::str::from_utf8(&self.0[..]) { + Ok(v) => f.write_str(v), + Err(_e) => { + // todo: report this better + eprintln!("Invalid utf-8 in {}", stringify!($name)); + Err(std::fmt::Error) + } + } + } + } + }; +} + +macro_rules! space_delimited_display { + ($name:ty) => { + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // once the first iteration is complete, we'll start adding spaces before + // each nickname. + let mut space = false; + + for value in &self.0 { + if space { + f.write_str(" ")?; + } else { + space = true; + } + + value.fmt(f)?; + } + + Ok(()) + } + } + }; +} + +pub struct Letter; + +impl ValidatingParser for Letter { + fn validate(bytes: &[u8]) -> bool { + bytes + .iter() + .all(|c| (b'a'..=b'z').contains(c) || (b'A'..=b'Z').contains(c)) + } +} + +pub struct Number; + +impl ValidatingParser for Number { + fn validate(bytes: &[u8]) -> bool { + bytes.iter().all(|c| (b'0'..=b'9').contains(c)) + } +} + +pub struct Special; + +impl ValidatingParser for Special { + fn validate(bytes: &[u8]) -> bool { + const ALLOWED: &[u8] = &[b'-', b'[', b']', b'\\', b'`', b'^', b'{', b'}']; + + bytes.iter().all(|c| ALLOWED.contains(c)) + } +} + +#[derive(Debug, Deref, Clone, From)] +pub struct Username<'a>(pub BytesCow<'a>); +space_terminated_primitive!(Username<'_>); +noop_validator!(Username<'_>); + +#[derive(Debug, Deref, Clone, From)] +pub struct Mode<'a>(pub BytesCow<'a>); +space_terminated_primitive!(Mode<'_>); +noop_validator!(Mode<'_>); + +#[derive(Debug, Deref, Clone, From)] +pub struct HostName<'a>(pub BytesCow<'a>); +space_terminated_primitive!(HostName<'_>); +noop_validator!(HostName<'_>); + +#[derive(Debug, Deref, Clone, From)] +pub struct ServerName<'a>(pub BytesCow<'a>); +space_terminated_primitive!(ServerName<'_>); +noop_validator!(ServerName<'_>); + +#[derive(Debug, Deref, Clone, From)] +pub struct RealName<'a>(pub BytesCow<'a>); +space_terminated_primitive!(RealName<'_>); +noop_validator!(RealName<'_>); + +#[derive(Debug, Deref, Clone, From)] +pub struct Nick<'a>(pub BytesCow<'a>); +space_terminated_primitive!(Nick<'_>); + +// TODO: i feel like this would be better suited as a nom chomper to stop +// iterating over the string twice unnecessarily +impl ValidatingParser for Nick<'_> { + fn validate(bytes: &[u8]) -> bool { + if bytes.is_empty() { + return false; + } + + if !Letter::validate(&[bytes[0]]) { + return false; + } + + bytes[1..] + .iter() + .all(|c| Letter::validate(&[*c]) || Number::validate(&[*c]) || Special::validate(&[*c])) + } +} + +#[derive(Debug, Deref, Clone, From)] +pub struct Channel<'a>(pub BytesCow<'a>); +space_terminated_primitive!(Channel<'_>); +noop_validator!(Channel<'_>); + +#[derive(Debug, Deref, Clone, From)] +pub struct FreeText<'a>(pub BytesCow<'a>); +free_text_primitive!(FreeText<'_>); +noop_validator!(FreeText<'_>); + +#[derive(Debug, Deref, Clone, From)] +pub struct Nicks<'a>(pub Vec>); +space_delimited_display!(Nicks<'_>); + +impl PrimitiveParser for Nicks<'_> { + fn parse(bytes: BytesWrapper) -> IResult { + let mut it = iterator( + bytes, + terminated(take_till(|c| c == b' '), tag(" ".as_bytes())), + ); + + let parsed = it.map(|v| Nick(v.into())).collect(); + + it.finish() + .map(move |(remaining, _)| (remaining, Self(parsed))) + } +} + +#[derive(Debug, Clone)] +pub struct RightsPrefixedNick<'a>(pub Rights, pub Nick<'a>); + +impl std::fmt::Display for RightsPrefixedNick<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f)?; + self.1.fmt(f) + } +} + +#[derive(Debug, Deref, Clone, From)] +pub struct RightsPrefixedNicks<'a>(pub Vec>); +space_delimited_display!(RightsPrefixedNicks<'_>); + +#[derive(Debug, Clone)] +pub struct RightsPrefixedChannel<'a>(pub Rights, pub Nick<'a>); + +impl std::fmt::Display for RightsPrefixedChannel<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f)?; + self.1.fmt(f) + } +} + +#[derive(Debug, Deref, Clone, From)] +pub struct RightsPrefixedChannels<'a>(pub Vec>); +space_delimited_display!(RightsPrefixedChannels<'_>); + +#[derive(Debug, Copy, Clone)] +pub enum Rights { + Op, + Voice, +} + +impl std::fmt::Display for Rights { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Op => "@", + Self::Voice => "+", + }) + } +} + +#[derive(Debug, From, Clone)] +pub enum Receiver<'a> { + User(Nick<'a>), + Channel(Channel<'a>), +} + +impl std::ops::Deref for Receiver<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + std::str::from_utf8(match self { + Self::User(nick) => &*nick, + Self::Channel(channel) => &*channel, + }) + .unwrap() + } +} + +impl PrimitiveParser for Receiver<'_> { + fn parse(bytes: BytesWrapper) -> IResult { + if bytes.get(0) == Some(&b'#') { + let (rest, channel) = Channel::parse(bytes)?; + Ok((rest, Self::Channel(channel))) + } else { + let (rest, nick) = Nick::parse(bytes)?; + Ok((rest, Self::User(nick))) + } + } +} diff --git a/titanirc-types/src/protocol/replies.rs b/titanirc-types/src/protocol/replies.rs new file mode 100644 index 0000000..eb5796c --- /dev/null +++ b/titanirc-types/src/protocol/replies.rs @@ -0,0 +1,170 @@ +#![allow(clippy::wildcard_imports)] + +use super::{commands::Command, primitives::*}; +use std::fmt::Write; + +/// The origin of a message that's about to be returned to the client. +#[derive(Debug)] +pub enum Source<'a> { + User(Nick<'a>), // change Nick to whatever type nick!user@netmask is.. + Server, +} + +impl<'a> From> for Source<'a> { + fn from(other: Nick<'a>) -> Self { + Self::User(other) + } +} + +macro_rules! define_replies { + ( + $( + $name:ident$(($($arg:ident$(<$($gen:tt),+>)?),*))? = $num:expr $(=> $msg:expr)? + ),* $(,)? + ) => { + /// A `RPL_*` or `ERR_*` type as defined in the IRC spec. + #[derive(Debug)] + #[allow(clippy::pub_enum_variant_names)] + pub enum Reply<'a> { + $( + $name$(($($arg$(<$($gen),+>)?),*))*, + )* + } + + /// Outputs the `RPL_*`/`ERR_*` type for the wire as defined in the IRC spec. + impl std::fmt::Display for Reply<'_> { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + paste::paste! { + match self { + $(Self::$name$(($([<$arg:snake>]),*))* => write!(fmt, concat!("", $($msg),*) $(, $([<$arg:snake>]),*)*)),* + } + } + } + } + + impl Reply<'_> { + /// The numeric code for this reply kind. + #[must_use] + pub fn code(&self) -> &'static str { + paste::paste! { + match self { + $(Self::$name$(($([<_ $arg:snake>]),*))* => stringify!($num)),* + } + } + } + } + }; +} + +type Target = String; +type CommandName = String; +type Mask = String; +type Banid = String; +type ConfigFile = String; +type Hopcount = String; +type ServerInfo = String; +type HG = String; +type Debuglevel = String; +type ModeParams = String; +type Version = String; +type AmtVisible = String; +type Integer = String; +type File = String; +type FileOp = String; +type Char = String; + +// TODO: fix these +type UserHost = String; + +define_replies! { + RplWelcome = 001 => ":Welcome to the network jordan!jordan@proper.sick.kid", + RplYourHost = 002 => ":Your host is a sick kid", + RplCreated = 003 => ":This server was created at some point", + RplMyInfo = 004 => ":my.test.server 0.0.1 DOQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI", + RplISupport = 005 => "D :are supported by this server", + + RplUmodeIs(Mode<'a>) = 221 => "{}", + + ErrNoSuchNick(Nick<'a>) = 401 => "{} :No such nick/channel", + ErrNoSuchServer(ServerName<'a>) = 402 => "{} :No such server", + ErrNoSuchChannel(Channel<'a>) = 403 => "{} :No such channel", + ErrCannotSendToChan(Channel<'a>) = 404 => "{} :Cannot send to channel", + ErrTooManyChannels(Channel<'a>) = 405 => "{} :You have joined too many channels", + ErrWasNoSuchNick(Nick<'a>) = 406 => "{} :There was no such nickname", + ErrTooManyTargets(Target) = 407 => "{} :Duplicate recipients. No message delivered", + ErrNoOrigin = 409 => ":No origin specified", + ErrNoRecipient(CommandName) = 411 => ":No recipient given ({})", + ErrNoTextToSend = 412 => ":No text to send", + ErrNoTopLevel(Mask) = 413 => "{} :No toplevel domain specified", + ErrWildTopLevel(Mask) = 414 => "{} :Wildcard in toplevel domain", + ErrUnknownCommand(CommandName) = 421 => "{} :Unknown command", + ErrNoMotd = 422 => ":MOTD File is missing", + ErrNoAdminInfo(ServerName<'a>) = 423 => "{} :No administrative info available", + ErrFileError(FileOp, File) = 424 => ":File error doing {} on {}", + ErrNoNickGiven = 431 => ":No nickname given", + ErrErroneusNick(Nick<'a>) = 432 => "{} :Erroneus nickname", + ErrNickInUse(Nick<'a>) = 433 => "{} :Nick is already in use", + ErrNickCollision(Nick<'a>) = 436 => "{} :Nick collision KILL", + ErrUserNotInChannel(Nick<'a>, Channel<'a>) = 441 => "{} {} :They aren't on that channel", + ErrNotOnChannel(Channel<'a>) = 442 => "{} :You're not on that channel", + ErrUserOnChannel(Username<'a>, Channel<'a>) = 443 => "{} {} :is already on channel", + ErrNoLogin(Username<'a>) = 444 => "{} :User not logged in", + ErrSummonDisabled = 445 => ":SUMMON has been disabled", + ErrUsersDisabled = 446 => ":USERS has been disabled", + ErrNotRegistered = 451 => ":You have not registered", + ErrNeedMoreParams(CommandName) = 461 => "{} :Not enough parameters", + ErrAlreadyRegistered = 462 => ":You may not reregister", + ErrNoPermForHost = 463 => ":Your host isn't among the privileged", + ErrPasswdMismatch = 464 => ":Password incorrect", + ErrYoureBannedCreep = 465 => ":You are banned from this server", + ErrKeySet(Channel<'a>) = 467 => "{} :Channel key already set", + ErrChannelIsFull(Channel<'a>) = 471 => "{} :Cannot join channel (+l)", + ErrUnknownMode(Char) = 472 => "{} :is unknown mode char to me", + ErrInviteOnlyChan(Channel<'a>) = 473 => "{} :Cannot join channel (+i)", + ErrBannedFromChan(Channel<'a>) = 474 => "{} :Cannot join channel (+b)", + ErrBadChannelKey(Channel<'a>) = 475 => "{} :Cannot join channel (+k)", + ErrNoPrivileges = 481 => ":Permission Denied- You're not an IRC operator", + ErrChanOPrivsNeeded(Channel<'a>) = 482 => "{} :You're not channel operator", + ErrCantKillServer = 483 => ":You cant kill a server!", + ErrNoOperHost = 491 => ":No O-lines for your host", + ErrUmodeUnknownFlag = 501 => ":Unknown MODE flag", + ErrUsersDontMatch = 502 => ":Cant change mode for other users", + RplNone = 300, + RplUserHost(UserHost) = 302 => "{}", + RplIson(Nicks<'a>) = 303 => "{}", + RplAway(Nick<'a>, FreeText<'a>) = 301 => "{} :{}", + RplUnaway = 305 => ":You are no longer marked as being away", + RplNowAway = 306 => ":You have been marked as being away", + RplWhoisUser(Nick<'a>, Username<'a>, HostName<'a>, RealName<'a>) = 311 => "{} {} {} * :{}", + RplWhoisServer(Nick<'a>, ServerName<'a>, ServerInfo) = 312 => "{} {} :{}", + RplWhoisOperator(Nick<'a>) = 313 => "{} :is an IRC operator", + RplWhoisIdle(Nick<'a>, Integer) = 317 => "{} {} :seconds idle", + RplEndOfWhois(Nick<'a>) = 318 => "{} :End of /WHOIS list", + RplWhoisChannels(Nick<'a>, RightsPrefixedChannels<'a>) = 319 => "{} :{}", // todo + RplWhoWasUser(Nick<'a>, Username<'a>, HostName<'a>, RealName<'a>) = 314 => "{} {} {} * :{}", + RplEndOfWhoWas(Nick<'a>) = 369 => "{} :End of WHOWAS", + RplListStart = 321 => "Channel :Users RealName", + RplList(Channel<'a>, AmtVisible, FreeText<'a>) = 322 => "{} {} :{}", + RplListEnd = 323 => ":End of /LIST", + RplChannelModeIs(Channel<'a>, Mode<'a>, ModeParams) = 324 => "{} {} {}", + RplNoTopic(Channel<'a>) = 331 => "{} :No topic is set", + RplTopic(Channel<'a>, FreeText<'a>) = 332 => "{} :{}", + RplInviting(Channel<'a>, Nick<'a>) = 341 => "{} {}", + RplVersion(Version, Debuglevel, ServerName<'a>, FreeText<'a>) = 351 => "{}.{} {} :{}", + RplWhoReply(Channel<'a>, Username<'a>, HostName<'a>, ServerName<'a>, Nick<'a>, HG, Hopcount, RealName<'a>) = 352 => "{} {} {} {} {} {}[*][@|+] :{} {}", + RplEndOfWho(Target) = 315 => "{} :End of /WHO list", + RplNamReply(Channel<'a>, RightsPrefixedNicks<'a>) = 353 => "{} :{}", + RplEndOfNames(Channel<'a>) = 366 => "{} :End of /NAMES list", + RplLinks(Mask, ServerName<'a>, Hopcount, ServerInfo) = 364 => "{} {} :{} {}", + RplEndOfLinks(Mask) = 365 => "{} :End of /LINKS list", + RplBanList(Channel<'a>, Banid) = 367 => "{} {}", + RPLEndOfBanList(Channel<'a>) = 368 => "{} :End of channel ban list", + RplInfo(String) = 371 => ":{}", + RplEndOfInfo = 374 => ":End of /INFO list", + RplMotdStart(ServerName<'a>) = 375 => ":- {} Message of the day -", + RplMotd(FreeText<'a>) = 372 => ":- {}", + RplEndOfMotd = 376 => ":End of /MOTD command", + RplYoureOper = 381 => ":You are now an IRC operator", + RplRehashing(ConfigFile) = 382 => "{} :Rehashing", + RplTime = 391, +} diff --git a/titanirc-types/src/replies.rs b/titanirc-types/src/replies.rs deleted file mode 100644 index b11eb08..0000000 --- a/titanirc-types/src/replies.rs +++ /dev/null @@ -1,212 +0,0 @@ -#![allow(clippy::wildcard_imports)] - -use crate::{primitives::*, Command}; -use std::fmt::Write; - -/// The origin of a message that's about to be returned to the client. -#[derive(Debug)] -pub enum Source<'a> { - User(Nick<'a>), // change Nick to whatever type nick!user@netmask is.. - Server, -} - -impl<'a> From> for Source<'a> { - fn from(other: Nick<'a>) -> Self { - Self::User(other) - } -} - -/// A message to be sent to the client over the wire. -#[derive(Debug, derive_more::From)] -pub enum ServerMessage<'a> { - /// A `RPL_*`/`ERR_*` type from the IRC spec. - Reply(Reply<'a>), - /// Normally a 'forwarded' message, ie. a `VERSION` for another client or - /// a `PRIVMSG`. - Command(Source<'a>, Command<'a>), // change Nick to whatever type nick!user@netmask is.. - /// A server ping to the client. - Ping, - /// A server pong to the client. - Pong, -} - -impl ServerMessage<'_> { - /// Writes out this `ServerMessage` to `dst`, in the expected format for the wire. - /// - /// This function omits the CRLF from the end of the line. - pub fn write(self, server_name: &str, client_username: &str, dst: &mut bytes::BytesMut) { - match self { - Self::Reply(reply) => write!( - dst, - ":{} {} {} {}", - server_name, - reply.code(), - client_username, - reply, - ), - Self::Ping => write!(dst, "PING :{}", server_name), - Self::Pong => write!(dst, "PONG :{}", server_name), - Self::Command(source, command) => { - let source = match &source { - Source::User(nick) => std::str::from_utf8(nick).unwrap(), - Source::Server => server_name, - }; - write!(dst, ":{} {}", source, command) - } - } - .unwrap() - } -} - -macro_rules! define_replies { - ( - $( - $name:ident$(($($arg:ident$(<$($gen:tt),+>)?),*))? = $num:expr $(=> $msg:expr)? - ),* $(,)? - ) => { - /// A `RPL_*` or `ERR_*` type as defined in the IRC spec. - #[derive(Debug)] - #[allow(clippy::pub_enum_variant_names)] - pub enum Reply<'a> { - $( - $name$(($($arg$(<$($gen),+>)?),*))*, - )* - } - - /// Outputs the `RPL_*`/`ERR_*` type for the wire as defined in the IRC spec. - impl std::fmt::Display for Reply<'_> { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - paste::paste! { - match self { - $(Self::$name$(($([<$arg:snake>]),*))* => write!(fmt, concat!("", $($msg),*) $(, $([<$arg:snake>]),*)*)),* - } - } - } - } - - impl Reply<'_> { - /// The numeric code for this reply kind. - #[must_use] - pub fn code(&self) -> &'static str { - paste::paste! { - match self { - $(Self::$name$(($([<_ $arg:snake>]),*))* => stringify!($num)),* - } - } - } - } - }; -} - -type Target = String; -type CommandName = String; -type Mask = String; -type Banid = String; -type ConfigFile = String; -type Hopcount = String; -type ServerInfo = String; -type HG = String; -type Debuglevel = String; -type ModeParams = String; -type Version = String; -type AmtVisible = String; -type Integer = String; -type File = String; -type FileOp = String; -type Char = String; - -// TODO: fix these -type UserHost = String; - -define_replies! { - RplWelcome = 001 => ":Welcome to the network jordan!jordan@proper.sick.kid", - RplYourHost = 002 => ":Your host is a sick kid", - RplCreated = 003 => ":This server was created at some point", - RplMyInfo = 004 => ":my.test.server 0.0.1 DOQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI", - RplISupport = 005 => "D :are supported by this server", - - RplUmodeIs(Mode<'a>) = 221 => "{}", - - ErrNoSuchNick(Nick<'a>) = 401 => "{} :No such nick/channel", - ErrNoSuchServer(ServerName<'a>) = 402 => "{} :No such server", - ErrNoSuchChannel(Channel<'a>) = 403 => "{} :No such channel", - ErrCannotSendToChan(Channel<'a>) = 404 => "{} :Cannot send to channel", - ErrTooManyChannels(Channel<'a>) = 405 => "{} :You have joined too many channels", - ErrWasNoSuchNick(Nick<'a>) = 406 => "{} :There was no such nickname", - ErrTooManyTargets(Target) = 407 => "{} :Duplicate recipients. No message delivered", - ErrNoOrigin = 409 => ":No origin specified", - ErrNoRecipient(CommandName) = 411 => ":No recipient given ({})", - ErrNoTextToSend = 412 => ":No text to send", - ErrNoTopLevel(Mask) = 413 => "{} :No toplevel domain specified", - ErrWildTopLevel(Mask) = 414 => "{} :Wildcard in toplevel domain", - ErrUnknownCommand(CommandName) = 421 => "{} :Unknown command", - ErrNoMotd = 422 => ":MOTD File is missing", - ErrNoAdminInfo(ServerName<'a>) = 423 => "{} :No administrative info available", - ErrFileError(FileOp, File) = 424 => ":File error doing {} on {}", - ErrNoNickGiven = 431 => ":No nickname given", - ErrErroneusNick(Nick<'a>) = 432 => "{} :Erroneus nickname", - ErrNickInUse(Nick<'a>) = 433 => "{} :Nick is already in use", - ErrNickCollision(Nick<'a>) = 436 => "{} :Nick collision KILL", - ErrUserNotInChannel(Nick<'a>, Channel<'a>) = 441 => "{} {} :They aren't on that channel", - ErrNotOnChannel(Channel<'a>) = 442 => "{} :You're not on that channel", - ErrUserOnChannel(Username<'a>, Channel<'a>) = 443 => "{} {} :is already on channel", - ErrNoLogin(Username<'a>) = 444 => "{} :User not logged in", - ErrSummonDisabled = 445 => ":SUMMON has been disabled", - ErrUsersDisabled = 446 => ":USERS has been disabled", - ErrNotRegistered = 451 => ":You have not registered", - ErrNeedMoreParams(CommandName) = 461 => "{} :Not enough parameters", - ErrAlreadyRegistered = 462 => ":You may not reregister", - ErrNoPermForHost = 463 => ":Your host isn't among the privileged", - ErrPasswdMismatch = 464 => ":Password incorrect", - ErrYoureBannedCreep = 465 => ":You are banned from this server", - ErrKeySet(Channel<'a>) = 467 => "{} :Channel key already set", - ErrChannelIsFull(Channel<'a>) = 471 => "{} :Cannot join channel (+l)", - ErrUnknownMode(Char) = 472 => "{} :is unknown mode char to me", - ErrInviteOnlyChan(Channel<'a>) = 473 => "{} :Cannot join channel (+i)", - ErrBannedFromChan(Channel<'a>) = 474 => "{} :Cannot join channel (+b)", - ErrBadChannelKey(Channel<'a>) = 475 => "{} :Cannot join channel (+k)", - ErrNoPrivileges = 481 => ":Permission Denied- You're not an IRC operator", - ErrChanOPrivsNeeded(Channel<'a>) = 482 => "{} :You're not channel operator", - ErrCantKillServer = 483 => ":You cant kill a server!", - ErrNoOperHost = 491 => ":No O-lines for your host", - ErrUmodeUnknownFlag = 501 => ":Unknown MODE flag", - ErrUsersDontMatch = 502 => ":Cant change mode for other users", - RplNone = 300, - RplUserHost(UserHost) = 302 => "{}", - RplIson(Nicks<'a>) = 303 => "{}", - RplAway(Nick<'a>, FreeText<'a>) = 301 => "{} :{}", - RplUnaway = 305 => ":You are no longer marked as being away", - RplNowAway = 306 => ":You have been marked as being away", - RplWhoisUser(Nick<'a>, Username<'a>, HostName<'a>, RealName<'a>) = 311 => "{} {} {} * :{}", - RplWhoisServer(Nick<'a>, ServerName<'a>, ServerInfo) = 312 => "{} {} :{}", - RplWhoisOperator(Nick<'a>) = 313 => "{} :is an IRC operator", - RplWhoisIdle(Nick<'a>, Integer) = 317 => "{} {} :seconds idle", - RplEndOfWhois(Nick<'a>) = 318 => "{} :End of /WHOIS list", - RplWhoisChannels(Nick<'a>, RightsPrefixedChannels<'a>) = 319 => "{} :{}", // todo - RplWhoWasUser(Nick<'a>, Username<'a>, HostName<'a>, RealName<'a>) = 314 => "{} {} {} * :{}", - RplEndOfWhoWas(Nick<'a>) = 369 => "{} :End of WHOWAS", - RplListStart = 321 => "Channel :Users RealName", - RplList(Channel<'a>, AmtVisible, FreeText<'a>) = 322 => "{} {} :{}", - RplListEnd = 323 => ":End of /LIST", - RplChannelModeIs(Channel<'a>, Mode<'a>, ModeParams) = 324 => "{} {} {}", - RplNoTopic(Channel<'a>) = 331 => "{} :No topic is set", - RplTopic(Channel<'a>, FreeText<'a>) = 332 => "{} :{}", - RplInviting(Channel<'a>, Nick<'a>) = 341 => "{} {}", - RplVersion(Version, Debuglevel, ServerName<'a>, FreeText<'a>) = 351 => "{}.{} {} :{}", - RplWhoReply(Channel<'a>, Username<'a>, HostName<'a>, ServerName<'a>, Nick<'a>, HG, Hopcount, RealName<'a>) = 352 => "{} {} {} {} {} {}[*][@|+] :{} {}", - RplEndOfWho(Target) = 315 => "{} :End of /WHO list", - RplNamReply(Channel<'a>, RightsPrefixedNicks<'a>) = 353 => "{} :{}", - RplEndOfNames(Channel<'a>) = 366 => "{} :End of /NAMES list", - RplLinks(Mask, ServerName<'a>, Hopcount, ServerInfo) = 364 => "{} {} :{} {}", - RplEndOfLinks(Mask) = 365 => "{} :End of /LINKS list", - RplBanList(Channel<'a>, Banid) = 367 => "{} {}", - RPLEndOfBanList(Channel<'a>) = 368 => "{} :End of channel ban list", - RplInfo(String) = 371 => ":{}", - RplEndOfInfo = 374 => ":End of /INFO list", - RplMotdStart(ServerName<'a>) = 375 => ":- {} Message of the day -", - RplMotd(FreeText<'a>) = 372 => ":- {}", - RplEndOfMotd = 376 => ":End of /MOTD command", - RplYoureOper = 381 => ":You are now an IRC operator", - RplRehashing(ConfigFile) = 382 => "{} :Rehashing", - RplTime = 391, -} -- libgit2 1.7.2