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(-)
@@ -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"
@@ -3,4 +3,4 @@
mod wire;
pub use crate::wire::Decoder;
pub use crate::wire::{Decoder, Encoder};
@@ -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();
@@ -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
@@ -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(),
}
});
}
}
@@ -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) {
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 {
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),
}
@@ -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
@@ -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]>>> {
let rest = if let Ok((rest, _)) = parse_optional_source(input) {
rest
} else {
input
};
let (rest, kind) = take_till(|c| c == b' ')(rest)?;
match kind {
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"
))
}
}
@@ -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,
)));
}
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 {
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);
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> {
let val = std::str::from_utf8(bytes).expect("utf-8").to_string();
Ok((b"", Self(val)))
}
}
@@ -0,0 +1,191 @@
use crate::{primitives::*, Command};
use std::fmt::Write;
#[derive(Debug)]
pub enum Source {
User(Nick), Server,
}
#[derive(Debug, derive_more::From)]
pub enum ServerMessage {
Reply(Reply),
Command(Source, Command), 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;
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 => "{} :{}", 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,
}