Skip to content

feat(irc-proto/ircv3): add standard replies support #265

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions irc-proto/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::chan::ChannelExt;
use crate::error::MessageParseError;
use crate::mode::{ChannelMode, Mode, UserMode};
use crate::response::Response;
use crate::standard_reply::{StandardTypes, StandardCodes};

/// List of all client commands as defined in [RFC 2812](http://tools.ietf.org/html/rfc2812). This
/// also includes commands from the
Expand Down Expand Up @@ -200,6 +201,9 @@ pub enum Command {
// Default option.
/// An IRC response code with arguments and optional suffix.
Response(Response, Vec<String>),
/// https://ircv3.net/specs/extensions/standard-replies
/// [FAIL | WARN | NOTE] <command> <code> [<context>] <description>
StandardResponse(StandardTypes, String, StandardCodes, Vec<String>, String),
/// A raw IRC command unknown to the crate.
Raw(String, Vec<String>),
}
Expand Down Expand Up @@ -408,6 +412,10 @@ impl<'a> From<&'a Command> for String {
&format!("{:03}", *resp as u16),
&a.iter().map(|s| &s[..]).collect::<Vec<_>>(),
),
Command::StandardResponse(ref r#type, ref command, ref code, ref args, ref description) => {
match r#type {
}
},
Command::Raw(ref c, ref a) => {
stringify(c, &a.iter().map(|s| &s[..]).collect::<Vec<_>>())
}
Expand Down Expand Up @@ -958,6 +966,14 @@ impl Command {
} else {
raw(cmd, args)
}
} else if StandardTypes::is_standard_type(cmd) {
let code = StandardCodes::from_message(args[0], args[1]);
let mut std_args: Vec<String> = args.iter().skip(2).map(|&s| s.to_owned()).collect();
let desc = std_args.pop().ok_or_else(|| MessageParseError::MissingDescriptionInStandardReply)?;

Command::StandardResponse(
StandardTypes::from_str(cmd).map_err(MessageParseError::InvalidStandardReplyType)?,
args[0].to_owned(), code, std_args, desc)
} else if let Ok(resp) = cmd.parse() {
Command::Response(resp, args.into_iter().map(|s| s.to_owned()).collect())
} else {
Expand Down Expand Up @@ -1128,6 +1144,7 @@ mod test {
use super::Command;
use super::Response;
use crate::Message;
use crate::standard_reply::{StandardTypes, StandardCodes};

#[test]
fn format_response() {
Expand Down Expand Up @@ -1155,4 +1172,20 @@ mod test {
cmd
);
}

#[test]
fn parse_standard_reply() {
let msg = "FAIL BOX BOXES_INVALID STACK CLOCKWISE :Given boxes are not supported".parse::<Message>().unwrap();

assert_eq!(
msg.command,
Command::StandardResponse(StandardTypes::Fail, "BOX".to_string(), StandardCodes::Custom("BOXES_INVALID".to_string()), vec!["STACK".to_string(), "CLOCKWISE".to_string()], "Given boxes are not supported".to_string())
);

let msg = "NOTE * OPER_MESSAGE :Registering new accounts and channels has been disabled temporarily while we deal with the spam. Thanks for flying ExampleNet! -dan".parse::<Message>().unwrap();
assert_eq!(
msg.command,
Command::StandardResponse(StandardTypes::Note, "*".to_string(), StandardCodes::Custom("OPER_MESSAGE".to_string()), vec![], "Registering new accounts and channels has been disabled temporarily while we deal with the spam. Thanks for flying ExampleNet! -dan".to_string())
);
}
}
8 changes: 8 additions & 0 deletions irc-proto/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ pub enum MessageParseError {
/// The invalid subcommand.
sub: String,
},

/// The standard reply missed a description
#[error("missing description in standard reply")]
MissingDescriptionInStandardReply,

/// Invalid standard reply type
#[error("invalid standard reply type: {}", .0)]
InvalidStandardReplyType(&'static str)
}

/// Errors that occur while parsing mode strings.
Expand Down
1 change: 1 addition & 0 deletions irc-proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod message;
pub mod mode;
pub mod prefix;
pub mod response;
pub mod standard_reply;

pub use self::caps::{Capability, NegotiationVersion};
pub use self::chan::ChannelExt;
Expand Down
286 changes: 286 additions & 0 deletions irc-proto/src/standard_reply.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
use std::str::FromStr;

/// Support for https://ircv3.net/specs/extensions/standard-replies
/// Implements the list of reply codes in the IRCv3 registry: https://ircv3.net/registry

trait FromCode {
fn from_code(code: &str) -> Option<Self> where Self: Sized;
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MultilineCodes {
MaxBytes,
MaxLines,
InvalidTarget,
Invalid
}

impl FromCode for MultilineCodes {
fn from_code(code: &str) -> Option<Self> {
Some(match code {
"MULTILINE_MAX_BYTES" => Self::MaxBytes,
"MULTILINE_MAX_LINES" => Self::MaxLines,
"MULTILINE_INVALID_TARGET" => Self::InvalidTarget,
"MULTILINE_INVALID" => Self::Invalid,
_ => return None,
})
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChatHistoryCodes {
InvalidParams,
InvalidTarget,
MessageError,
NeedMoreParams,
UnknownCommand
}

impl FromCode for ChatHistoryCodes {
fn from_code(code: &str) -> Option<Self> {
Some(match code {
"INVALID_PARAMS" => Self::InvalidParams,
"INVALID_TARGET" => Self::InvalidTarget,
"MESSAGE_ERROR" => Self::MessageError,
"NEED_MORE_PARAMS" => Self::NeedMoreParams,
"UNKNOWN_COMMAND" => Self::UnknownCommand,
_ => return None,
})
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JoinCodes {
ChannelRenamed
}

impl FromCode for JoinCodes {
fn from_code(code: &str) -> Option<Self> {
Some(match code {
"CHANNEL_RENAMED" => Self::ChannelRenamed,
_ => return None
})
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NickCodes {
Reserved
}

impl FromCode for NickCodes {
fn from_code(code: &str) -> Option<Self> {
Some(match code {
"NICKNAME_RESERVED" => Self::Reserved,
_ => return None
})
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RedactCodes {
InvalidTarget,
Forbidden,
WindowExpired,
UnknownMsgid,
}

impl FromCode for RedactCodes {
fn from_code(code: &str) -> Option<Self> {
Some(match code {
"INVALID_TARGET" => Self::InvalidTarget,
"REACT_FORBIDDEN" => Self::Forbidden,
"REACT_WINDOW_EXPIRED" => Self::WindowExpired,
"UNKNOWN_MSGID" => Self::UnknownMsgid,
_ => return None
})
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegisterCodes {
AccountExists,
AccountNameMustBeNick,
AlreadyAuthenticated,
BadAccountName,
CompleteConnectionRequired,
InvalidEmail,
NeedNick,
TemporarilyUnavailable,
UnacceptableEmail,
UnacceptablePassword,
WeakPassword
}

impl FromCode for RegisterCodes {
fn from_code(code: &str) -> Option<Self> {
Some(match code {
"ACCOUNT_EXISTS" => Self::AccountExists,
"ACCOUNT_NAME_MUST_BE_NICK" => Self::AccountNameMustBeNick,
"ALREADY_AUTHENTICATED" => Self::AlreadyAuthenticated,
"BAD_ACCOUNT_NAME" => Self::BadAccountName,
"COMPLETE_CONNECTION_REQUIRED" => Self::CompleteConnectionRequired,
"INVALID_EMAIL" => Self::InvalidEmail,
"NEED_NICK" => Self::NeedNick,
"TEMPORARILY_UNAVAILABLE" => Self::TemporarilyUnavailable,
"UNACCEPTABLE_EMAIL" => Self::UnacceptableEmail,
"UNACCEPTABLE_PASSWORD" => Self::UnacceptablePassword,
"WEAK_PASSWORD" => Self::WeakPassword,
_ => return None,
})
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenameCodes {
ChannelNameInUse,
CannotRename
}

impl FromCode for RenameCodes {
fn from_code(code: &str) -> Option<Self> {
Some(match code {
"CHANNEL_NAME_IN_USE" => Self::ChannelNameInUse,
"CANNOT_RENAME" => Self::CannotRename,
_ => return None,
})
}
}


#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetNameCodes {
CannotChangeRealname,
InvalidRealname
}

impl FromCode for SetNameCodes {
fn from_code(code: &str) -> Option<Self> {
Some(match code {
"CANNOT_CHANGE_REALNAME" => Self::CannotChangeRealname,
"INVALID_REALNAME" => Self::InvalidRealname,
_ => return None,
})
}
}


#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerifyCodes {
AlreadyAuthenticated,
InvalidCode,
CompleteConnectionRequired,
TemporarilyUnavailable
}

impl FromCode for VerifyCodes {
fn from_code(code: &str) -> Option<Self> {
Some(match code {
"ALREADY_AUTHENTICATED" => Self::AlreadyAuthenticated,
"INVALID_CODE" => Self::InvalidCode,
"COMPLETE_CONNECTION_REQUIRED" => Self::CompleteConnectionRequired,
"TEMPORARILY_UNAVAILABLE" => Self::TemporarilyUnavailable,
_ => return None,
})
}
}


#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StandardCodes {
AccountRequired,
InvalidUtf8,

Multiline(MultilineCodes),
ChatHistory(ChatHistoryCodes),
Join(JoinCodes),
Nick(NickCodes),
Redact(RedactCodes),
Register(RegisterCodes),
Rename(RenameCodes),
SetName(SetNameCodes),
Verify(VerifyCodes),

Custom(String)
}

impl StandardCodes {
fn known_from_message(command: &str, code: &str) -> Option<Self> {
Some(match command {
"BATCH" => Self::Multiline(MultilineCodes::from_code(code)?),
"CHATHISTORY" => Self::ChatHistory(ChatHistoryCodes::from_code(code)?),
"JOIN" => Self::Join(JoinCodes::from_code(code)?),
"NICK" => Self::Nick(NickCodes::from_code(code)?),
"REDACT" => Self::Redact(RedactCodes::from_code(code)?),
"REGISTER" => Self::Register(RegisterCodes::from_code(code)?),
"RENAME" => Self::Rename(RenameCodes::from_code(code)?),
"SETNAME" => Self::SetName(SetNameCodes::from_code(code)?),
"VERIFY" => Self::Verify(VerifyCodes::from_code(code)?),
_ => {
match code {
"ACCOUNT_REQUIRED" => Self::AccountRequired,
"INVALID_UTF8" => Self::InvalidUtf8,
_ => Self::Custom(code.to_string())
}
}
})
}

pub fn from_message(command: &str, code: &str) -> Self {
Self::known_from_message(command, code).unwrap_or_else(|| Self::Custom(code.to_string()))
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StandardTypes {
Fail,
Warn,
Note
}

impl StandardTypes {
pub fn is_standard_type(s: &str) -> bool {
s.eq_ignore_ascii_case("FAIL") || s.eq_ignore_ascii_case("WARN") || s.eq_ignore_ascii_case("NOTE")
}
}

impl FromStr for StandardTypes {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_uppercase().as_str() {
"FAIL" => Ok(Self::Fail),
"WARN" => Ok(Self::Warn),
"NOTE" => Ok(Self::Note),
_ => Err("Unexpected standard response type, neither fail, warn or note.")
}
}
}

#[cfg(test)]
mod test {
use super::StandardCodes;

#[test]
fn parse_spec_example1() {
let (command, code) = ("ACC", "REG_INVALID_CALLBACK");
assert_eq!(StandardCodes::Custom("REG_INVALID_CALLBACK".to_string()), StandardCodes::from_message(command, code));
}

#[test]
fn parse_spec_example2() {
let (command, code) = ("BOX", "BOXES_INVALID");
assert_eq!(StandardCodes::Custom("BOXES_INVALID".to_string()), StandardCodes::from_message(command, code));
}

#[test]
fn parse_spec_example3() {
let (command, code) = ("*", "ACCOUNT_REQUIRED_TO_CONNECT");
assert_eq!(StandardCodes::Custom("ACCOUNT_REQUIRED_TO_CONNECT".to_string()), StandardCodes::from_message(command, code));
}

#[test]
fn parse_batch_example() {
let (command, code) = ("BATCH", "MULTILINE_MAX_BYTES");
assert_eq!(StandardCodes::Multiline(crate::standard_reply::MultilineCodes::MaxBytes), StandardCodes::from_message(command, code));
}
}