feat(irc-proto/ircv3): add standard replies support
Only parsing is supported here, not reserialization. Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
This commit is contained in:
parent
264820077f
commit
c87644229e
4 changed files with 328 additions and 0 deletions
|
@ -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
|
||||
|
@ -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>),
|
||||
}
|
||||
|
@ -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<_>>())
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
286
irc-proto/src/standard_reply.rs
Normal file
286
irc-proto/src/standard_reply.rs
Normal file
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue