🏡 index : ~doyle/titanirc.git

author Jordan Doyle <jordan@doyle.la> 2021-01-27 3:05:28.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-01-30 14:44:13.0 +00:00:00
commit
5ca8ef9e99ff57e347f54c53b495c86b591b3694 [patch]
tree
ed54594116d3f32d2ebbc0e956b7c75c97c7622f
parent
6d638389fdd519ac65bef8bc93ff4718b6277aae
download
5ca8ef9e99ff57e347f54c53b495c86b591b3694.tar.gz

Implement server -> client responses & initial connection song-and-dance



Diff

 Cargo.lock                       | 148 ++++++++++++++++++++++-
 titanirc-codec/src/lib.rs        |   2 +-
 titanirc-codec/src/wire.rs       |  18 +++-
 titanirc-server/Cargo.toml       |   3 +-
 titanirc-server/src/server.rs    |  11 +-
 titanirc-server/src/session.rs   |  76 +++++++++++-
 titanirc-types/Cargo.toml        |   4 +-
 titanirc-types/src/lib.rs        |  80 ++++++++++--
 titanirc-types/src/primitives.rs | 264 ++++++++++++++++++++++++++++++++++------
 titanirc-types/src/replies.rs    | 191 +++++++++++++++++++++++++++++-
 10 files changed, 743 insertions(+), 54 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index f206ece..5ea1955 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -105,6 +105,17 @@ dependencies = [
]

[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
 "hermit-abi",
 "libc",
 "winapi",
]

[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -147,6 +158,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "clap"
version = "3.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
dependencies = [
 "atty",
 "bitflags",
 "clap_derive",
 "indexmap",
 "lazy_static",
 "os_str_bytes",
 "strsim",
 "termcolor",
 "textwrap",
 "unicode-width",
 "vec_map",
]

[[package]]
name = "clap_derive"
version = "3.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1"
dependencies = [
 "heck",
 "proc-macro-error",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "crossbeam-channel"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -174,6 +217,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993a608597367c6377b258c25d7120740f00ed23a2252b729b1932dd7866f908"

[[package]]
name = "derive_more"
version = "0.99.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "displaydoc"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -273,6 +327,12 @@ dependencies = [
]

[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"

[[package]]
name = "heck"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -313,6 +373,16 @@ dependencies = [
]

[[package]]
name = "indexmap"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b"
dependencies = [
 "autocfg",
 "hashbrown",
]

[[package]]
name = "instant"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -476,6 +546,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"

[[package]]
name = "os_str_bytes"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"

[[package]]
name = "parking_lot"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -531,6 +607,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"

[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
 "proc-macro-error-attr",
 "proc-macro2",
 "quote",
 "syn",
 "version_check",
]

[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
 "proc-macro2",
 "quote",
 "version_check",
]

[[package]]
name = "proc-macro2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -667,6 +767,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"

[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"

[[package]]
name = "syn"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -684,6 +790,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e"

[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
 "winapi-util",
]

[[package]]
name = "textwrap"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
dependencies = [
 "unicode-width",
]

[[package]]
name = "thiserror"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -735,6 +859,7 @@ dependencies = [
 "actix",
 "actix-rt",
 "async-stream",
 "clap",
 "displaydoc",
 "thiserror",
 "titanirc-codec",
@@ -747,6 +872,8 @@ dependencies = [
name = "titanirc-types"
version = "0.1.0"
dependencies = [
 "bytes",
 "derive_more",
 "nom",
 "paste",
]
@@ -866,6 +993,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"

[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"

[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -884,6 +1017,12 @@ dependencies = [
]

[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"

[[package]]
name = "version_check"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -918,6 +1057,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"

[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
 "winapi",
]

[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/titanirc-codec/src/lib.rs b/titanirc-codec/src/lib.rs
index bb3d677..206da83 100644
--- a/titanirc-codec/src/lib.rs
+++ b/titanirc-codec/src/lib.rs
@@ -3,4 +3,4 @@

mod wire;

pub use crate::wire::Decoder;
pub use crate::wire::{Decoder, Encoder};
diff --git a/titanirc-codec/src/wire.rs b/titanirc-codec/src/wire.rs
index eb9548e..9b9cee5 100644
--- a/titanirc-codec/src/wire.rs
+++ b/titanirc-codec/src/wire.rs
@@ -27,6 +27,8 @@ impl FrameDecoder for Decoder {

        let bytes = src.copy_to_bytes(length + 1);

        eprintln!("{:?}", std::str::from_utf8(&bytes[..bytes.len() - 2]));

        match Command::parse(&bytes[..bytes.len() - 2]) {
            Ok(Some(msg)) => Ok(Some(msg)),
            Ok(None) => Err(std::io::Error::new(
@@ -41,6 +43,22 @@ impl FrameDecoder for Decoder {
    }
}

pub struct Encoder;

impl tokio_util::codec::Encoder<titanirc_types::ServerMessage> for Encoder {
    type Error = std::io::Error;

    fn encode(
        &mut self,
        item: titanirc_types::ServerMessage,
        dst: &mut BytesMut,
    ) -> Result<(), Self::Error> {
        item.write("my.cool.server", "jordan", dst);
        dst.extend_from_slice(b"\r\n");
        Ok(())
    }
}

fn find_crlf(src: &mut BytesMut) -> Option<usize> {
    let mut iter = src.iter().enumerate();

diff --git a/titanirc-server/Cargo.toml b/titanirc-server/Cargo.toml
index f0ca783..8cb9be8 100644
--- a/titanirc-server/Cargo.toml
+++ b/titanirc-server/Cargo.toml
@@ -16,4 +16,5 @@ tokio = { version = "1.1", features = ["net", "signal"] }
tokio-util = "0.6"
async-stream = "0.3"
thiserror = "1"
displaydoc = "0.1"
\ No newline at end of file
displaydoc = "0.1"
clap = "3.0.0-beta.2"
\ No newline at end of file
diff --git a/titanirc-server/src/server.rs b/titanirc-server/src/server.rs
index 9740800..e0af39d 100644
--- a/titanirc-server/src/server.rs
+++ b/titanirc-server/src/server.rs
@@ -2,7 +2,7 @@ use crate::session::Session;

use std::net::SocketAddr;

use actix::prelude::*;
use actix::{io::FramedWrite, prelude::*};
use tokio::net::TcpStream;
use tokio_util::codec::FramedRead;

@@ -19,13 +19,16 @@ pub struct Connection(pub TcpStream, pub SocketAddr);
impl Handler<Connection> for Server {
    type Result = ();

    fn handle(&mut self, Connection(stream, remote): Connection, _: &mut Self::Context) {
    fn handle(&mut self, Connection(stream, remote): Connection, _ctx: &mut Self::Context) {
        println!("Accepted connection from {}", remote);

        Session::create(move |ctx| {
            let (read, _write) = tokio::io::split(stream);
            let (read, write) = tokio::io::split(stream);
            Session::add_stream(FramedRead::new(read, titanirc_codec::Decoder), ctx);
            Session {}
            Session {
                writer: FramedWrite::new(write, titanirc_codec::Encoder, ctx),
                last_active: std::time::Instant::now(),
            }
        });
    }
}
diff --git a/titanirc-server/src/session.rs b/titanirc-server/src/session.rs
index 8482c46..f87cd9f 100644
--- a/titanirc-server/src/session.rs
+++ b/titanirc-server/src/session.rs
@@ -1,16 +1,88 @@
use actix::prelude::*;
use actix::{
    io::{FramedWrite, WriteHandler},
    prelude::*,
};
use std::time::{Duration, Instant};
use titanirc_types::Command;
use tokio::{io::WriteHalf, net::TcpStream};

pub struct Session {}
pub struct Session {
    pub writer:
        FramedWrite<titanirc_types::ServerMessage, WriteHalf<TcpStream>, titanirc_codec::Encoder>,
    pub last_active: Instant,
}

fn schedule_ping(ctx: &mut <Session as Actor>::Context) {
    ctx.run_later(Duration::from_secs(30), |act, ctx| {
        if Instant::now().duration_since(act.last_active) > Duration::from_secs(240) {
            // send `QUIT :Ping timeout: 120 seconds` & `ERROR :Closing Link: {ip} (Ping timeout: 120 seconds)`
            eprintln!("ping timeout");
            ctx.stop();
        }

        act.writer.write(titanirc_types::ServerMessage::Ping);
        schedule_ping(ctx);
    });
}

impl Actor for Session {
    type Context = Context<Self>;

    fn started(&mut self, ctx: &mut Self::Context) {
        schedule_ping(ctx);
    }
}

impl WriteHandler<std::io::Error> for Session {}

impl StreamHandler<Result<Command, std::io::Error>> for Session {
    /// This is main event loop for client requests
    fn handle(&mut self, cmd: Result<Command, std::io::Error>, _ctx: &mut Self::Context) {
        self.last_active = Instant::now();

        match cmd {
            Ok(Command::Nick(_v)) => {
                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());
            }
            Ok(Command::Mode(titanirc_types::ModeCommand { mode, .. })) => self
                .writer
                .write(titanirc_types::Reply::RplUmodeIs(mode).into()),
            Ok(Command::Motd(_)) => {
                self.writer.write(
                    titanirc_types::Reply::RplMotdStart(titanirc_types::ServerName(
                        "my.test.server".to_string(),
                    ))
                    .into(),
                );
                self.writer.write(
                    titanirc_types::Reply::RplMotd(titanirc_types::FreeText(
                        "Hello, welcome to this server!".to_string(),
                    ))
                    .into(),
                );
                self.writer.write(
                    titanirc_types::Reply::RplMotd(titanirc_types::FreeText(
                        "it's very cool!".to_string(),
                    ))
                    .into(),
                );
                self.writer
                    .write(titanirc_types::Reply::RplEndOfMotd.into());
            }
            Ok(Command::Version(_)) => self.writer.write(
                titanirc_types::Reply::RplVersion(
                    clap::crate_version!().to_string(),
                    "release".to_string(),
                    titanirc_types::ServerName("my.test.server".to_string()),
                    titanirc_types::FreeText("https://github.com/MITBorg/titanirc".to_string()),
                )
                .into(),
            ),
            Ok(Command::Pong(_)) => {}
            Ok(cmd) => println!("cmd: {:?}", cmd),
            Err(e) => eprintln!("error decoding: {}", e),
        }
diff --git a/titanirc-types/Cargo.toml b/titanirc-types/Cargo.toml
index 73a11a8..562d290 100644
--- a/titanirc-types/Cargo.toml
+++ b/titanirc-types/Cargo.toml
@@ -8,4 +8,6 @@ edition = "2018"

[dependencies]
paste = "1.0"
nom = "6.1"
\ No newline at end of file
nom = "6.1"
derive_more = "0.99"
bytes = "1.0"
\ No newline at end of file
diff --git a/titanirc-types/src/lib.rs b/titanirc-types/src/lib.rs
index ec6c621..81e6b3c 100644
--- a/titanirc-types/src/lib.rs
+++ b/titanirc-types/src/lib.rs
@@ -2,8 +2,21 @@
#![allow(clippy::missing_errors_doc)]

mod primitives;
mod replies;

pub use crate::primitives::*;
pub use crate::replies::{Reply, ServerMessage};

use nom::{
    bytes::complete::{tag, take_till},
    error::Error as NomError,
};

fn parse_optional_source(input: &[u8]) -> nom::IResult<&[u8], &[u8]> {
    let (rest, _) = tag(":")(input)?;
    let (rest, _) = take_till(|c| c == b' ')(rest)?;
    tag(" ")(rest)
}

macro_rules! define_commands {
    (
@@ -22,20 +35,36 @@ macro_rules! define_commands {
            $(const [<$name _BYTES>]: &[u8] = stringify!($name).as_bytes();)*

            impl Command {
                pub fn parse(input: &[u8]) -> Result<Option<Self>, nom::Err<nom::error::Error<&[u8]>>> {
                    let (rest, kind) = nom::bytes::complete::take_till(|c| c == b' ')(input)?;
                pub fn parse(input: &[u8]) -> Result<Option<Self>, nom::Err<NomError<&[u8]>>> {
                    // skip the optional source at the start of the message
                    let rest = if let Ok((rest, _)) = parse_optional_source(input) {
                        rest
                    } else {
                        input
                    };

                    let (rest, kind) = take_till(|c| c == b' ')(rest)?;

                    match kind {
                    // fix this shit
                    match std::str::from_utf8(kind).unwrap().to_uppercase().as_bytes() {
                        $([<$name _BYTES>] => Ok(Some(Self::[<$name:camel>]([<$name:camel Command>]::parse(rest)?)))),*,
                        _ => Ok(None)
                    }
                }
            }

            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>] {
                    $($([<$param:snake>]: $param),*),*
                    $($(pub [<$param:snake>]: $param),*),*
                }

                impl [<$name:camel Command>] {
@@ -43,7 +72,7 @@ macro_rules! define_commands {
                    pub fn parse(rest: &[u8]) -> Result<Self, nom::Err<nom::error::Error<&[u8]>>> {
                        $(
                            $(
                                let (rest, _) = nom::bytes::complete::tag(" ")(rest)?;
                                let (rest, _) = tag(" ")(rest)?;
                                let (rest, [<$param:snake>]) = $param::parse(rest)?;
                            )*
                        )*
@@ -53,6 +82,21 @@ macro_rules! define_commands {
                        })
                    }
                }

                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(())
                    }
                }
            )*
        }
    };
@@ -62,17 +106,21 @@ define_commands! {
    USER(Username, HostName, ServerName, RealName),
    NICK(Nick),

    MOTD,
    VERSION,
    HELP,
    USERS,
    TIME,
    PONG(ServerName),
    PING(ServerName),
    LIST,
    MODE(Nick, Mode),
    WHOIS(Nick),
    USERHOST(Nick),
    USERIP(Nick),
    JOIN(Channel),

    PRIVMSG(Receiver, Message),
    PRIVMSG(Receiver, FreeText),
}

#[cfg(test)]
@@ -90,8 +138,24 @@ mod tests {
            Command::parse(b"PRIVMSG foo :baz"),
            Ok(Some(Command::Privmsg(super::PrivmsgCommand {
                receiver: super::Receiver::User(super::Nick(nick)),
                message: super::Message(msg),
            }))) if nick == "foo" && msg == ":baz"
                free_text: super::primitives::FreeText(msg),
            }))) if nick == "foo" && msg == "baz"
        ))
    }

    #[test]
    fn parse_privmsg_opt_source() {
        eprintln!(
            "{:?}",
            Command::parse(b":some-fake-source!dude@nice PRIVMSG foo :baz")
        );

        assert!(matches!(
            Command::parse(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),
            }))) if nick == "foo" && msg == "baz"
        ))
    }
}
diff --git a/titanirc-types/src/primitives.rs b/titanirc-types/src/primitives.rs
index ed3dcb9..7803339 100644
--- a/titanirc-types/src/primitives.rs
+++ b/titanirc-types/src/primitives.rs
@@ -1,4 +1,14 @@
use nom::IResult;
use derive_more::{Deref, From};
use nom::{
    bytes::complete::{tag, take_till},
    combinator::{iterator, map_res},
    sequence::terminated,
    IResult,
};

pub trait ValidatingParser {
    fn validate(bytes: &[u8]) -> bool;
}

pub trait PrimitiveParser {
    fn parse(bytes: &[u8]) -> IResult<&[u8], Self>
@@ -6,58 +16,250 @@ pub trait PrimitiveParser {
        Self: Sized;
}

macro_rules! standard_string_parser {
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: &[u8]) -> IResult<&[u8], Self> {
                let (rest, val) = nom::combinator::map_res(
                    nom::bytes::complete::take_till(|c| c == b' '),
                    std::str::from_utf8,
                )(bytes)?;
                let (rest, _) = tag(b":")(bytes)?;
                Ok((&[], Self(std::str::from_utf8(rest).unwrap().to_string())))
            }
        }

        impl std::fmt::Display for $name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                f.write_str(&self.0)
            }
        }
    };
}

macro_rules! space_terminated_primitive {
    ($name:ty) => {
        impl PrimitiveParser for $name {
            fn parse(bytes: &[u8]) -> IResult<&[u8], Self> {
                let (rest, val) = map_res(take_till(|c| c == b' '), std::str::from_utf8)(bytes)?;

                if !<Self as ValidatingParser>::validate(val.as_bytes()) {
                    return Err(nom::Err::Failure(nom::error::Error::new(
                        bytes,
                        nom::error::ErrorKind::Verify,
                    )));
                }

                // TODO: don't clone
                Ok((rest, Self(val.to_string())))
            }
        }

        impl std::fmt::Display for $name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                f.write_str(&self.0)
            }
        }
    };
}

#[derive(Debug)]
pub struct Username(pub(crate) String);
standard_string_parser!(Username);
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;

#[derive(Debug)]
pub struct HostName(pub(crate) String);
standard_string_parser!(HostName);
                for value in &self.0 {
                    if space {
                        f.write_str(" ")?;
                    } else {
                        space = true;
                    }

#[derive(Debug)]
pub struct ServerName(pub(crate) String);
standard_string_parser!(ServerName);
                    value.fmt(f)?;
                }

#[derive(Debug)]
pub struct RealName(pub(crate) String);
standard_string_parser!(RealName);
                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, From)]
pub struct Username(pub String);
space_terminated_primitive!(Username);
noop_validator!(Username);

#[derive(Debug, Deref, From)]
pub struct Mode(pub String);
space_terminated_primitive!(Mode);
noop_validator!(Mode);

#[derive(Debug, Deref, From)]
pub struct HostName(pub String);
space_terminated_primitive!(HostName);
noop_validator!(HostName);

#[derive(Debug, Deref, From)]
pub struct ServerName(pub String);
space_terminated_primitive!(ServerName);
noop_validator!(ServerName);

#[derive(Debug, Deref, From)]
pub struct RealName(pub String);
space_terminated_primitive!(RealName);
noop_validator!(RealName);

#[derive(Debug, Deref, From)]
pub struct Nick(pub String);
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, From)]
pub struct Channel(pub String);
space_terminated_primitive!(Channel);
noop_validator!(Channel);

#[derive(Debug, Deref, From)]
pub struct FreeText(pub String);
free_text_primitive!(FreeText);
noop_validator!(FreeText);

#[derive(Debug, Deref, From)]
pub struct Nicks(pub Vec<Nick>);
space_delimited_display!(Nicks);

impl PrimitiveParser for Nicks {
    fn parse(bytes: &[u8]) -> IResult<&[u8], Self> {
        let mut it = iterator(bytes, terminated(take_till(|c| c == b' '), tag(b" ")));

        let parsed = it
            .map(|v| Nick(std::str::from_utf8(v).unwrap().to_string()))
            .collect();

        it.finish()
            .map(move |(remaining, _)| (remaining, Self(parsed)))
    }
}

#[derive(Debug)]
pub struct Nick(pub(crate) String);
standard_string_parser!(Nick);
pub struct RightsPrefixedNick(pub Rights, pub Nick);

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, From)]
pub struct RightsPrefixedNicks(pub Vec<RightsPrefixedNick>);
space_delimited_display!(RightsPrefixedNicks);

#[derive(Debug)]
pub struct Channel(pub(crate) String);
standard_string_parser!(Channel);
pub struct RightsPrefixedChannel(pub Rights, pub Nick);

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, From)]
pub struct RightsPrefixedChannels(pub Vec<RightsPrefixedChannel>);
space_delimited_display!(RightsPrefixedChannels);

#[derive(Debug)]
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)]
pub enum Receiver {
    User(Nick),
    Channel(Channel),
}

impl std::ops::Deref for Receiver {
    type Target = String;

    fn deref(&self) -> &Self::Target {
        match self {
            Self::User(nick) => &*nick,
            Self::Channel(channel) => &*channel,
        }
    }
}

impl PrimitiveParser for Receiver {
    fn parse(bytes: &[u8]) -> IResult<&[u8], Self> {
        if let Ok((rest, _)) =
            nom::bytes::complete::tag::<_, _, nom::error::Error<&[u8]>>("#")(bytes)
        if let Ok((_, _)) = nom::bytes::complete::tag::<_, _, nom::error::Error<&[u8]>>("#")(bytes)
        {
            let (rest, channel) = Channel::parse(rest)?;
            let (rest, channel) = Channel::parse(bytes)?;
            Ok((rest, Self::Channel(channel)))
        } else {
            let (rest, nick) = Nick::parse(bytes)?;
@@ -65,15 +267,3 @@ impl PrimitiveParser for Receiver {
        }
    }
}

#[derive(Debug)]
pub struct Message(pub(crate) String);

impl PrimitiveParser for Message {
    fn parse(bytes: &[u8]) -> IResult<&[u8], Self> {
        // TODO: don't clone, don't panic
        let val = std::str::from_utf8(bytes).expect("utf-8").to_string();

        Ok((b"", Self(val)))
    }
}
diff --git a/titanirc-types/src/replies.rs b/titanirc-types/src/replies.rs
new file mode 100644
index 0000000..e89296f
--- /dev/null
+++ b/titanirc-types/src/replies.rs
@@ -0,0 +1,191 @@
use crate::{primitives::*, Command};
use std::fmt::Write;

#[derive(Debug)]
pub enum Source {
    User(Nick), // change Nick to whatever type nick!user@netmask is..
    Server,
}

#[derive(Debug, derive_more::From)]
pub enum ServerMessage {
    Reply(Reply),
    Command(Source, Command), // change Nick to whatever type nick!user@netmask is..
    Ping,
    Pong,
}

impl ServerMessage {
    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) => nick.as_str(),
                    Source::Server => server_name,
                };
                write!(dst, ":{} {}", source, command)
            }
        }
        .unwrap()
    }
}

macro_rules! define_replies {
    (
        $(
            $name:ident$(($($arg:ty),*))? = $num:expr $(=> $msg:expr)?
        ),* $(,)?
    ) => {
        #[derive(Debug)]
        #[allow(clippy::pub_enum_variant_names)]
        pub enum Reply {
            $(
                $name$(($($arg),*))*,
            )*
        }

        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 {
            #[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) = 221 => "{}",

    ErrNoSuchNick(Nick) = 401 => "{} :No such nick/channel",
    ErrNoSuchServer(ServerName) = 402 => "{} :No such server",
    ErrNoSuchChannel(Channel) = 403 => "{} :No such channel",
    ErrCannotSendToChan(Channel) = 404 => "{} :Cannot send to channel",
    ErrTooManyChannels(Channel) = 405 => "{} :You have joined too many channels",
    ErrWasNoSuchNick(Nick) = 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) = 423 => "{} :No administrative info available",
    ErrFileError(FileOp, File) = 424 => ":File error doing {} on {}",
    ErrNoNickGiven = 431 => ":No nickname given",
    ErrErroneusNick(Nick) = 432 => "{} :Erroneus nickname",
    ErrNickInUse(Nick) = 433 => "{} :Nick is already in use",
    ErrNickCollision(Nick) = 436 => "{} :Nick collision KILL",
    ErrUserNotInChannel(Nick, Channel) = 441 => "{} {} :They aren't on that channel",
    ErrNotOnChannel(Channel) = 442 => "{} :You're not on that channel",
    ErrUserOnChannel(Username, Channel) = 443 => "{} {} :is already on channel",
    ErrNoLogin(Username) = 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) = 467 => "{} :Channel key already set",
    ErrChannelIsFull(Channel) = 471 => "{} :Cannot join channel (+l)",
    ErrUnknownMode(Char) = 472 => "{} :is unknown mode char to me",
    ErrInviteOnlyChan(Channel) = 473 => "{} :Cannot join channel (+i)",
    ErrBannedFromChan(Channel) = 474 => "{} :Cannot join channel (+b)",
    ErrBadChannelKey(Channel) = 475 => "{} :Cannot join channel (+k)",
    ErrNoPrivileges = 481 => ":Permission Denied- You're not an IRC operator",
    ErrChanOPrivsNeeded(Channel) = 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) = 303 => "{}",
    RplAway(Nick, FreeText) = 301 => "{} :{}",
    RplUnaway = 305 => ":You are no longer marked as being away",
    RplNowAway = 306 => ":You have been marked as being away",
    RplWhoisUser(Nick, Username, HostName, RealName) = 311 => "{} {} {} * :{}",
    RplWhoisServer(Nick, ServerName, ServerInfo) = 312 => "{} {} :{}",
    RplWhoisOperator(Nick) = 313 => "{} :is an IRC operator",
    RplWhoisIdle(Nick, Integer) = 317 => "{} {} :seconds idle",
    RplEndOfWhois(Nick) = 318 => "{} :End of /WHOIS list",
    RplWhoisChannels(Nick, RightsPrefixedChannels) = 319 => "{} :{}", // todo
    RplWhoWasUser(Nick, Username, HostName, RealName) = 314 => "{} {} {} * :{}",
    RplEndOfWhoWas(Nick) = 369 => "{} :End of WHOWAS",
    RplListStart = 321 => "Channel :Users  RealName",
    RplList(Channel, AmtVisible, FreeText) = 322 => "{} {} :{}",
    RplListEnd = 323 => ":End of /LIST",
    RplChannelModeIs(Channel, Mode, ModeParams) = 324 => "{} {} {}",
    RplNoTopic(Channel) = 331 => "{} :No topic is set",
    RplTopic(Channel, FreeText) = 332 => "{} :{}",
    RplInviting(Channel, Nick) = 341 => "{} {}",
    RplVersion(Version, Debuglevel, ServerName, FreeText) = 351 => "{}.{} {} :{}",
    RplWhoReply(Channel, Username, HostName, ServerName, Nick, HG, Hopcount, RealName) = 352 => "{} {} {} {} {} {}[*][@|+] :{} {}",
    RplEndOfWho(Target) = 315 => "{} :End of /WHO list",
    RplNamReply(Channel, RightsPrefixedNicks) = 353 => "{} :{}",
    RplEndOfNames(Channel) = 366 => "{} :End of /NAMES list",
    RplLinks(Mask, ServerName, Hopcount, ServerInfo) = 364 => "{} {} :{} {}",
    RplEndOfLinks(Mask) = 365 => "{} :End of /LINKS list",
    RplBanList(Channel, Banid) = 367 => "{} {}",
    RPLEndOfBanList(Channel) = 368 => "{} :End of channel ban list",
    RplInfo(String) = 371 => ":{}",
    RplEndOfInfo = 374 => ":End of /INFO list",
    RplMotdStart(ServerName) = 375 => ":- {} Message of the day -",
    RplMotd(FreeText) = 372 => ":- {}",
    RplEndOfMotd = 376 => ":End of /MOTD command",
    RplYoureOper = 381 => ":You are now an IRC operator",
    RplRehashing(ConfigFile) = 382 => "{} :Rehashing",
    RplTime = 391,
}