🏡 index : ~doyle/titanirc.git

author Jordan Doyle <jordan@doyle.la> 2021-02-01 22:09:55.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-02-01 22:09:55.0 +00:00:00
commit
e6cae0179d49c667ec9e7357338ff2cd18cf760c [patch]
tree
89bb0ebc4e631d8267fb66ae3edf46e5548f3239
parent
b9116667db096e098f6df4fd2d061242eef6ad4e
download
e6cae0179d49c667ec9e7357338ff2cd18cf760c.tar.gz

User nick state sharing & don't duplicate PRIVMSGs back to the user



Diff

 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(-)

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<String>,
    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<titanirc_types::ServerMessage<'_>> for Encoder {
impl tokio_util::codec::Encoder<titanirc_types::protocol::ServerMessage<'_>> 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<super::Handle, JoinError>;

@@ -9,7 +10,8 @@ pub type JoinResult = Result<super::Handle, JoinError>;
#[rtype(result = "JoinResult")]
pub struct Join {
    pub channel_name: String,
    pub nick: String,
    pub user_uuid: UserUuid,
    pub nick: RegisteredNick,
    pub user: Addr<User>,
}

@@ -24,7 +26,7 @@ pub enum JoinError {
#[rtype(result = "")]
pub struct JoinBroadcast {
    pub channel_name: String,
    pub nick: String,
    pub nick: RegisteredNick,
}

impl From<Join> 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<Addr<User>>,
    pub members: HashMap<UserUuid, Addr<User>>,
}

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<M>(&self, msg: M) -> impl Future<Output = ()>
    fn broadcast_message<M>(
        &self,
        skip_sender: Option<UserUuid>,
        msg: M,
    ) -> impl Future<Output = ()>
    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<events::Join> 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<super::common_events::Message> 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<T>: Actor {
@@ -38,14 +42,13 @@ impl CommandHandler<NickCommand<'static>> 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<JoinCommand<'static>> 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<ModeCommand<'static>> 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<MotdCommand<'static>> 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<VersionCommand<'static>> 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<PrivmsgCommand<'static>> 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<Server>,
    pub writer: FramedWrite<
        WriteHalf<TcpStream>,
@@ -23,7 +35,8 @@ pub struct User {
        <titanirc_codec::Encoder as tokio_util::codec::Encoder<ServerMessage<'static>>>::Error,
    >,
    pub last_active: Instant,
    pub nick: Option<String>,
    pub nick: RegisteredNick,
    pub channels: HashMap<Arc<String>, 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<Server>,
        writer: FramedWrite<WriteHalf<TcpStream>, 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 <User as Actor>::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<Arc<JoinBroadcast>> for User {

    fn handle(&mut self, msg: Arc<JoinBroadcast>, _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<Arc<crate::entities::common_events::Message>> 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<String, Addr<Channel>>,
    // A list of known connected users.
    // pub users: Vec<(UserIdent, Addr<User>)>,    // 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<Connection> 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<Result<Command, _>>`.
            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<Connection> for Server {
impl Handler<crate::entities::channel::events::Join> for Server {
    type Result = ResponseActFuture<Self, crate::entities::channel::events::JoinResult>;

    // 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<BytesWrapper, BytesWrapper> {
    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<Option<Self>, nom::Err<NomError<BytesWrapper>>> {
                    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<Self, nom::Err<nom::error::Error<BytesWrapper>>> {
                        $(
                            $(
                                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<Command<'a>> for [<$name:camel Command>]<'a> {
                    fn into(self) -> Command<'a> {
                        Command::[<$name:camel>](self)
                    }
                }
            )*
        }
    };
#[derive(Debug, Clone)]
pub struct UserIdent {
    nick: RegisteredNick,
    username: Arc<String>,
    host: Arc<String>,
}

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<arc_swap::ArcSwapOption<Bytes>>);

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<Arc<Bytes>> {
        self.0.load().clone()
    }

    #[test]
    fn parse_empty() {
        assert!(matches!(Command::parse(Bytes::from_static(b"")), Ok(None)));
    pub fn set(&self, nick: Arc<Bytes>) {
        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<H: std::hash::Hasher>(&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<BytesWrapper, Self>
    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<BytesWrapper> 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<BytesWrapper, Self> {
                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<BytesWrapper, Self> {
                let (rest, val) = take_till(|c| c == b' ')(bytes.clone())?;

                if !<Self as ValidatingParser>::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<Nick<'a>>);
space_delimited_display!(Nicks<'_>);

impl PrimitiveParser for Nicks<'_> {
    fn parse(bytes: BytesWrapper) -> IResult<BytesWrapper, Self> {
        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<RightsPrefixedNick<'a>>);
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<RightsPrefixedChannel<'a>>);
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<BytesWrapper, Self> {
        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<BytesWrapper, BytesWrapper> {
    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<Option<Self>, nom::Err<NomError<BytesWrapper>>> {
                    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<Self, nom::Err<nom::error::Error<BytesWrapper>>> {
                        $(
                            $(
                                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<Command<'a>> 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<BytesWrapper, Self>
    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<BytesWrapper> 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<BytesWrapper, Self> {
                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<BytesWrapper, Self> {
                let (rest, val) = take_till(|c| c == b' ')(bytes.clone())?;

                if !<Self as ValidatingParser>::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<Nick<'a>>);
space_delimited_display!(Nicks<'_>);

impl PrimitiveParser for Nicks<'_> {
    fn parse(bytes: BytesWrapper) -> IResult<BytesWrapper, Self> {
        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<RightsPrefixedNick<'a>>);
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<RightsPrefixedChannel<'a>>);
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<BytesWrapper, Self> {
        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<Nick<'a>> 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<Nick<'a>> 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,
}