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(-)
@@ -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"
@@ -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(())
}
@@ -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
@@ -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 {
@@ -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 {
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(),
}
}
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));
ctx.spawn(
self.broadcast_message(Some(msg.user_uuid), msg)
.into_actor(self),
);
}
}
@@ -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,
}
}
@@ -1,10 +1,14 @@
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());
@@ -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();
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),
);
}
let server_addr = self.server.clone();
let ctx_addr = ctx.address();
let nick = self.nick.clone();
let user_uuid = self.session_id;
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(), 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),
);
}
let msg = crate::entities::common_events::Message {
from: self.nick.clone(), 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),
);
}
}
@@ -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>,
}
@@ -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()),
@@ -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 {
pub channels: HashMap<String, Addr<Channel>>,
}
impl Server {
pub fn new() -> Self {
Self {
channels: HashMap::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()), ctx,
);
ctx.add_stream(read);
User::new(server_ctx.address(), write)
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>;
fn handle(
&mut self,
msg: crate::entities::channel::events::Join,
@@ -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
@@ -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! {
#[derive(Debug)]
pub enum Command<'a> {
$([<$name:camel>]([<$name:camel Command>]<'a>)),*
}
$(const [<$name _BYTES>]: &[u8] = stringify!($name).as_bytes();)*
impl Command<'_> {
pub fn parse(input: Bytes) -> Result<Option<Self>, nom::Err<NomError<BytesWrapper>>> {
let mut input = BytesWrapper::from(input);
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)
}
}
}
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>]<'_> {
#[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;
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>),
}
#[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 {}
@@ -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;
}
#[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) => {
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) => {
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 {
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<'_>);
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)))
}
}
}
@@ -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! {
#[derive(Debug)]
pub enum Command<'a> {
$([<$name:camel>]([<$name:camel Command>]<'a>)),*
}
$(const [<$name _BYTES>]: &[u8] = stringify!($name).as_bytes();)*
impl Command<'_> {
pub fn parse(input: Bytes) -> Result<Option<Self>, nom::Err<NomError<BytesWrapper>>> {
let mut input = BytesWrapper::from(input);
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)
}
}
}
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>]<'_> {
#[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>]),*),*
})
}
}
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"
))
}
}
@@ -0,0 +1,54 @@
pub mod commands;
pub mod primitives;
pub mod replies;
use std::fmt::Write;
use crate::RegisteredNick;
#[derive(Debug, derive_more::From)]
pub enum ServerMessage<'a> {
Reply(replies::Reply<'a>),
Command(replies::Source<'a>, commands::Command<'a>), Ping,
Pong,
}
impl ServerMessage<'_> {
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()
}
}
@@ -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;
}
#[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) => {
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) => {
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 {
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<'_>);
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)))
}
}
}
@@ -0,0 +1,170 @@
#![allow(clippy::wildcard_imports)]
use super::{commands::Command, primitives::*};
use std::fmt::Write;
#[derive(Debug)]
pub enum Source<'a> {
User(Nick<'a>), 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)?
),* $(,)?
) => {
#[derive(Debug)]
#[allow(clippy::pub_enum_variant_names)]
pub enum Reply<'a> {
$(
$name$(($($arg$(<$($gen),+>)?),*))*,
)*
}
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<'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 => "{} :{}", 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,
}
@@ -1,212 +0,0 @@
#![allow(clippy::wildcard_imports)]
use crate::{primitives::*, Command};
use std::fmt::Write;
#[derive(Debug)]
pub enum Source<'a> {
User(Nick<'a>), Server,
}
impl<'a> From<Nick<'a>> for Source<'a> {
fn from(other: Nick<'a>) -> Self {
Self::User(other)
}
}
#[derive(Debug, derive_more::From)]
pub enum ServerMessage<'a> {
Reply(Reply<'a>),
Command(Source<'a>, Command<'a>), 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) => 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)?
),* $(,)?
) => {
#[derive(Debug)]
#[allow(clippy::pub_enum_variant_names)]
pub enum Reply<'a> {
$(
$name$(($($arg$(<$($gen),+>)?),*))*,
)*
}
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<'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 => "{} :{}", 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,
}