Implemented new Mode API (fixes #48).

This commit is contained in:
Aaron Weiss 2017-06-22 13:59:18 -04:00
parent 54326e0047
commit d9f4f82051
No known key found for this signature in database
GPG key ID: 0237035D9BF03AE2
7 changed files with 396 additions and 88 deletions

View file

@ -42,7 +42,7 @@ pub struct Config {
pub channels: Option<Vec<String>>,
/// A mapping of channel names to keys for join-on-connect.
pub channel_keys: Option<HashMap<String, String>>,
/// User modes to set on connect. Example: "+RB-x"
/// User modes to set on connect. Example: "+RB -x"
pub umodes: Option<String>,
/// The text that'll be sent in response to CTCP USERINFO requests.
pub user_info: Option<String>,

View file

@ -3,6 +3,7 @@ use std::borrow::ToOwned;
use std::cmp::Ordering;
use std::cmp::Ordering::{Less, Equal, Greater};
use std::str::FromStr;
use proto::{Mode, ChannelMode};
/// IRC User data.
#[derive(Clone, Debug)]
@ -77,18 +78,18 @@ impl User {
}
/// Updates the user's access level.
pub fn update_access_level(&mut self, mode: &str) {
pub fn update_access_level(&mut self, mode: &Mode<ChannelMode>) {
match mode {
"+q" => self.add_access_level(AccessLevel::Owner),
"-q" => self.sub_access_level(AccessLevel::Owner),
"+a" => self.add_access_level(AccessLevel::Admin),
"-a" => self.sub_access_level(AccessLevel::Admin),
"+o" => self.add_access_level(AccessLevel::Oper),
"-o" => self.sub_access_level(AccessLevel::Oper),
"+h" => self.add_access_level(AccessLevel::HalfOp),
"-h" => self.sub_access_level(AccessLevel::HalfOp),
"+v" => self.add_access_level(AccessLevel::Voice),
"-v" => self.sub_access_level(AccessLevel::Voice),
&Mode::Plus(ChannelMode::Founder, _) => self.add_access_level(AccessLevel::Owner),
&Mode::Minus(ChannelMode::Founder, _) => self.sub_access_level(AccessLevel::Owner),
&Mode::Plus(ChannelMode::Admin, _) => self.add_access_level(AccessLevel::Admin),
&Mode::Minus(ChannelMode::Admin, _) => self.sub_access_level(AccessLevel::Admin),
&Mode::Plus(ChannelMode::Oper, _) => self.add_access_level(AccessLevel::Oper),
&Mode::Minus(ChannelMode::Oper, _) => self.sub_access_level(AccessLevel::Oper),
&Mode::Plus(ChannelMode::Halfop, _) => self.add_access_level(AccessLevel::HalfOp),
&Mode::Minus(ChannelMode::Halfop, _) => self.sub_access_level(AccessLevel::HalfOp),
&Mode::Plus(ChannelMode::Voice, _) => self.add_access_level(AccessLevel::Voice),
&Mode::Minus(ChannelMode::Voice, _) => self.sub_access_level(AccessLevel::Voice),
_ => {}
}
}
@ -225,6 +226,8 @@ impl Iterator for AccessLevelIterator {
mod test {
use super::{AccessLevel, User};
use super::AccessLevel::*;
use proto::Mode::*;
use proto::ChannelMode as M;
#[test]
fn parse_access_level() {
@ -300,25 +303,25 @@ mod test {
fn update_user_rank() {
let mut user = User::new("user");
assert_eq!(user.highest_access_level, Member);
user.update_access_level("+q");
user.update_access_level(&Plus(M::Founder, None));
assert_eq!(user.highest_access_level, Owner);
user.update_access_level("-q");
user.update_access_level(&Minus(M::Founder, None));
assert_eq!(user.highest_access_level, Member);
user.update_access_level("+a");
user.update_access_level(&Plus(M::Admin, None));
assert_eq!(user.highest_access_level, Admin);
user.update_access_level("-a");
user.update_access_level(&Minus(M::Admin, None));
assert_eq!(user.highest_access_level, Member);
user.update_access_level("+o");
user.update_access_level(&Plus(M::Oper, None));
assert_eq!(user.highest_access_level, Oper);
user.update_access_level("-o");
user.update_access_level(&Minus(M::Oper, None));
assert_eq!(user.highest_access_level, Member);
user.update_access_level("+h");
user.update_access_level(&Plus(M::Halfop, None));
assert_eq!(user.highest_access_level, HalfOp);
user.update_access_level("-h");
user.update_access_level(&Minus(M::Halfop, None));
assert_eq!(user.highest_access_level, Member);
user.update_access_level("+v");
user.update_access_level(&Plus(M::Voice, None));
assert_eq!(user.highest_access_level, Voice);
user.update_access_level("-v");
user.update_access_level(&Minus(M::Voice, None));
assert_eq!(user.highest_access_level, Member);
}
@ -330,19 +333,19 @@ mod test {
user.access_levels,
vec![Owner, Admin, Oper, HalfOp, Voice, Member]
);
user.update_access_level("-h");
user.update_access_level(&Minus(M::Halfop, None));
assert_eq!(user.highest_access_level, Owner);
assert_eq!(user.access_levels, vec![Owner, Admin, Oper, Member, Voice]);
user.update_access_level("-q");
user.update_access_level(&Minus(M::Founder, None));
assert_eq!(user.highest_access_level, Admin);
assert_eq!(user.access_levels, vec![Voice, Admin, Oper, Member]);
user.update_access_level("-a");
user.update_access_level(&Minus(M::Admin, None));
assert_eq!(user.highest_access_level, Oper);
assert_eq!(user.access_levels, vec![Voice, Member, Oper]);
user.update_access_level("-o");
user.update_access_level(&Minus(M::Oper, None));
assert_eq!(user.highest_access_level, Voice);
assert_eq!(user.access_levels, vec![Voice, Member]);
user.update_access_level("-v");
user.update_access_level(&Minus(M::Voice, None));
assert_eq!(user.highest_access_level, Member);
assert_eq!(user.access_levels, vec![Member]);
}

View file

@ -9,8 +9,8 @@ use client::conn::Connection;
use client::data::{Config, User};
use client::server::utils::ServerExt;
use client::transport::LogView;
use proto::{Command, Message, Response};
use proto::Command::{JOIN, NICK, NICKSERV, PART, PRIVMSG, MODE, QUIT};
use proto::{ChannelMode, Command, Message, Mode, Response};
use proto::Command::{JOIN, NICK, NICKSERV, PART, PRIVMSG, ChannelMODE, QUIT};
use futures::{Async, Poll, Future, Sink, Stream};
use futures::stream::SplitStream;
use futures::sync::mpsc;
@ -206,7 +206,7 @@ impl ServerState {
NICK(ref new_nick) => {
self.handle_nick_change(msg.source_nickname().unwrap_or(""), new_nick)
}
MODE(ref chan, ref mode, Some(ref user)) => self.handle_mode(chan, mode, user),
ChannelMODE(ref chan, ref modes) => self.handle_mode(chan, modes),
PRIVMSG(ref target, ref body) => {
if body.starts_with('\u{001}') {
let tokens: Vec<_> = {
@ -290,7 +290,7 @@ impl ServerState {
if self.config().umodes().is_empty() {
Ok(())
} else {
self.send_mode(self.current_nickname(), self.config().umodes(), "")
self.send_mode(self.current_nickname(), &Mode::as_user_modes(self.config().umodes())?)
}
}
@ -358,16 +358,23 @@ impl ServerState {
}
#[cfg(feature = "nochanlists")]
fn handle_mode(&self, chan: &str, mode: &str, user: &str) {}
fn handle_mode(&self, _: &str, _: &[Mode<ChannelMODE>]) {}
#[cfg(not(feature = "nochanlists"))]
fn handle_mode(&self, chan: &str, mode: &str, user: &str) {
fn handle_mode(&self, chan: &str, modes: &[Mode<ChannelMode>]) {
for mode in modes {
match mode {
&Mode::Plus(_, Some(ref user)) | &Mode::Minus(_, Some(ref user)) => {
if let Some(vec) = self.chanlists.lock().unwrap().get_mut(chan) {
if let Some(n) = vec.iter().position(|x| x.get_nickname() == user) {
vec[n].update_access_level(mode)
}
}
}
_ => (),
}
}
}
#[cfg(feature = "nochanlists")]
fn handle_namreply(&self, _: &[String], _: &Option<String>) {}
@ -556,6 +563,7 @@ mod test {
use client::data::Config;
#[cfg(not(feature = "nochanlists"))]
use client::data::User;
use proto::{ChannelMode, Mode};
use proto::command::Command::{PART, PRIVMSG};
use futures::{Future, Stream};
@ -893,7 +901,7 @@ mod test {
vec![User::new("@test"), User::new("~owner"), User::new("&admin")]
);
let mut exp = User::new("@test");
exp.update_access_level("+v");
exp.update_access_level(&Mode::Plus(ChannelMode::Voice, None));
assert_eq!(
server.list_users("#test").unwrap()[0].highest_access_level(),
exp.highest_access_level()

View file

@ -1,9 +1,9 @@
//! Utilities and shortcuts for working with IRC servers.
use std::borrow::ToOwned;
use error::Result;
use proto::{Capability, NegotiationVersion};
use proto::Command::{AUTHENTICATE, CAP, INVITE, JOIN, KICK, KILL, MODE, NICK, NOTICE};
use proto::Command::{OPER, PART, PASS, PONG, PRIVMSG, QUIT, SAMODE, SANICK, TOPIC, USER};
use proto::{Capability, Command, Mode, NegotiationVersion};
use proto::command::Command::*;
use proto::command::ModeType;
use client::server::Server;
use proto::command::CapSubCommand::{END, LS, REQ};
#[cfg(feature = "ctcp")]
@ -16,7 +16,7 @@ pub trait ServerExt: Server {
where
Self: Sized,
{
self.send(CAP(
self.send(Command::CAP(
None,
LS,
match version {
@ -201,21 +201,13 @@ pub trait ServerExt: Server {
))
}
/// Changes the mode of the target.
/// If `modeparmas` is an empty string, it won't be included in the message.
fn send_mode(&self, target: &str, mode: &str, modeparams: &str) -> Result<()>
/// Changes the modes for the specified target.
fn send_mode<T>(&self, target: &str, modes: &[Mode<T>]) -> Result<()>
where
Self: Sized,
T: ModeType,
{
self.send(MODE(
target.to_owned(),
mode.to_owned(),
if modeparams.is_empty() {
None
} else {
Some(modeparams.to_owned())
},
))
self.send(T::mode(target, modes))
}
/// Changes the mode of the target by force.
@ -358,6 +350,7 @@ mod test {
use client::data::Config;
use client::server::IrcServer;
use client::server::test::{get_server_value, test_config};
use proto::{ChannelMode, Mode};
#[test]
fn identify() {
@ -483,14 +476,15 @@ mod test {
#[test]
fn send_mode_no_modeparams() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_mode("#test", "+i", "").unwrap();
server.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap();
assert_eq!(&get_server_value(server)[..], "MODE #test +i\r\n");
}
#[test]
fn send_mode() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_mode("#test", "+o", "test").unwrap();
server.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))])
.unwrap();
assert_eq!(&get_server_value(server)[..], "MODE #test +o test\r\n");
}

View file

@ -30,6 +30,11 @@ error_chain! {
display("Failed to parse an IRC subcommand.")
}
ModeParsingFailed {
description("Failed to parse a mode correctly.")
display("Failed to parse a mode correctly.")
}
/// An error occurred on one of the internal channels of the `IrcServer`.
ChannelError {
description("An error occured on one of the IrcServer's internal channels.")

View file

@ -1,5 +1,6 @@
//! Enumeration of all available client commands.
use std::ascii::AsciiExt;
use std::fmt;
use std::str::FromStr;
use error;
use proto::Response;
@ -20,8 +21,7 @@ pub enum Command {
/// OPER name :password
OPER(String, String),
/// MODE nickname modes
/// MODE channel modes [modeparams]
MODE(String, String, Option<String>),
UserMODE(String, Vec<Mode<UserMode>>),
/// SERVICE nickname reserved distribution type reserved :info
SERVICE(String, String, String, String, String, String),
/// QUIT :comment
@ -34,8 +34,8 @@ pub enum Command {
JOIN(String, Option<String>, Option<String>),
/// PART chanlist :[comment]
PART(String, Option<String>),
// MODE is already defined.
// MODE(String, String, Option<String>),
/// MODE channel [modes [modeparams]]
ChannelMODE(String, Vec<Mode<ChannelMode>>),
/// TOPIC channel :[topic]
TOPIC(String, Option<String>),
/// NAMES [chanlist :[target]]
@ -191,8 +191,13 @@ impl<'a> From<&'a Command> for String {
Command::NICK(ref n) => stringify("NICK", &[], Some(n)),
Command::USER(ref u, ref m, ref r) => stringify("USER", &[u, m, "*"], Some(r)),
Command::OPER(ref u, ref p) => stringify("OPER", &[u], Some(p)),
Command::MODE(ref t, ref m, Some(ref p)) => stringify("MODE", &[t, m, p], None),
Command::MODE(ref t, ref m, None) => stringify("MODE", &[t, m], None),
Command::UserMODE(ref u, ref m) => {
format!("MODE {}{}", u, m.iter().fold(String::new(), |mut acc, mode| {
acc.push_str(" ");
acc.push_str(&mode.to_string());
acc
}))
}
Command::SERVICE(ref n, ref r, ref d, ref t, ref re, ref i) => {
stringify("SERVICE", &[n, r, d, t, re], Some(i))
}
@ -205,6 +210,13 @@ impl<'a> From<&'a Command> for String {
Command::JOIN(ref c, None, None) => stringify("JOIN", &[c], None),
Command::PART(ref c, Some(ref m)) => stringify("PART", &[c], Some(m)),
Command::PART(ref c, None) => stringify("PART", &[c], None),
Command::ChannelMODE(ref u, ref m) => {
format!("MODE {}{}", u, m.iter().fold(String::new(), |mut acc, mode| {
acc.push_str(" ");
acc.push_str(&mode.to_string());
acc
}))
}
Command::TOPIC(ref c, Some(ref t)) => stringify("TOPIC", &[c], Some(t)),
Command::TOPIC(ref c, None) => stringify("TOPIC", &[c], None),
Command::NAMES(Some(ref c), Some(ref t)) => stringify("NAMES", &[c], Some(t)),
@ -505,32 +517,14 @@ impl Command {
}
} else if cmd.eq_ignore_ascii_case("MODE") {
match suffix {
Some(suffix) => {
if args.len() == 2 {
Command::MODE(
args[0].to_owned(),
args[1].to_owned(),
Some(suffix.to_owned()),
)
} else if args.len() == 1 {
Command::MODE(args[0].to_owned(), suffix.to_owned(), None)
Some(suffix) => raw(cmd, args, Some(suffix)),
None => if args[0].is_channel_name() {
let arg = args[1..].join(" ");
Command::ChannelMODE(args[0].to_owned(), Mode::as_channel_modes(&arg)?)
} else {
raw(cmd, args, Some(suffix))
}
}
None => {
if args.len() == 3 {
Command::MODE(
args[0].to_owned(),
args[1].to_owned(),
Some(args[2].to_owned()),
)
} else if args.len() == 2 {
Command::MODE(args[0].to_owned(), args[1].to_owned(), None)
} else {
raw(cmd, args, suffix)
}
}
let arg = args[1..].join(" ");
Command::UserMODE(args[0].to_owned(), Mode::as_user_modes(&arg)?)
},
}
} else if cmd.eq_ignore_ascii_case("SERVICE") {
match suffix {
@ -1760,3 +1754,307 @@ impl FromStr for BatchSubCommand {
}
}
}
/// A marker trait for different kinds of Modes.
pub trait ModeType: fmt::Display + fmt::Debug + Clone + PartialEq {
/// Creates a command of this kind.
fn mode(target: &str, modes: &[Mode<Self>]) -> Command;
/// Returns true if this mode takes an argument, and false otherwise.
fn takes_arg(&self) -> bool;
}
/// User modes for the MODE command.
#[derive(Clone, Debug, PartialEq)]
pub enum UserMode {
/// a - user is flagged as away
Away,
/// i - marks a users as invisible
Invisible,
/// w - user receives wallops
Wallops,
/// r - restricted user connection
Restricted,
/// o - operator flag
Oper,
/// O - local operator flag
LocalOper,
/// s - marks a user for receipt of server notices
ServerNotices,
/// Any other unknown-to-the-crate mode.
Unknown(char),
}
impl ModeType for UserMode {
fn mode(target: &str, modes: &[Mode<Self>]) -> Command {
Command::UserMODE(target.to_owned(), modes.to_owned())
}
fn takes_arg(&self) -> bool {
false
}
}
impl UserMode {
fn from_char(c: char) -> error::Result<UserMode> {
use self::UserMode::*;
Ok(match c {
'a' => Away,
'i' => Invisible,
'w' => Wallops,
'r' => Restricted,
'o' => Oper,
'O' => LocalOper,
's' => ServerNotices,
_ => Unknown(c),
})
}
}
impl fmt::Display for UserMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::UserMode::*;
write!(f, "{}", match *self {
Away => 'a',
Invisible => 'i',
Wallops => 'w',
Restricted => 'r',
Oper => 'o',
LocalOper => 'O',
ServerNotices => 's',
Unknown(c) => c,
})
}
}
/// Channel modes for the MODE command.
#[derive(Clone, Debug, PartialEq)]
pub enum ChannelMode {
/// b - ban the user from joining or speaking in the channel
Ban,
/// e - exemptions from bans
Exception,
/// l - limit the maximum number of users in a channel
Limit,
/// i - channel becomes invite-only
InviteOnly,
/// I - exception to invite-only rule
InviteException,
/// k - specify channel key
Key,
/// m - channel is in moderated mode
Moderated,
/// s - channel is hidden from listings
Secret,
/// t - require permissions to edit topic
ProtectedTopic,
/// n - users must join channels to message them
NoExternalMessages,
/// q - user gets founder permission
Founder,
/// a - user gets admin or protected permission
Admin,
/// o - user gets oper permission
Oper,
/// h - user gets halfop permission
Halfop,
/// v - user gets voice permission
Voice,
/// Any other unknown-to-the-crate mode.
Unknown(char),
}
impl ModeType for ChannelMode {
fn mode(target: &str, modes: &[Mode<Self>]) -> Command {
Command::ChannelMODE(target.to_owned(), modes.to_owned())
}
fn takes_arg(&self) -> bool {
use self::ChannelMode::*;
match *self {
Ban | Exception | Limit | InviteException | Key | Founder | Admin | Oper | Halfop |
Voice => true,
_ => false,
}
}
}
impl ChannelMode {
fn from_char(c: char) -> error::Result<ChannelMode> {
use self::ChannelMode::*;
Ok(match c {
'b' => Ban,
'e' => Exception,
'l' => Limit,
'i' => InviteOnly,
'I' => InviteException,
'k' => Key,
'm' => Moderated,
's' => Secret,
't' => ProtectedTopic,
'n' => NoExternalMessages,
'q' => Founder,
'a' => Admin,
'o' => Oper,
'h' => Halfop,
'v' => Voice,
_ => Unknown(c),
})
}
}
impl fmt::Display for ChannelMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::ChannelMode::*;
write!(f, "{}", match *self {
Ban => 'b',
Exception => 'e',
Limit => 'l',
InviteOnly => 'i',
InviteException => 'I',
Key => 'k',
Moderated => 'm',
Secret => 's',
ProtectedTopic => 't',
NoExternalMessages => 'n',
Founder => 'q',
Admin => 'a',
Oper => 'o',
Halfop => 'h',
Voice => 'v',
Unknown(c) => c,
})
}
}
/// A mode argument for the MODE command.
#[derive(Clone, Debug, PartialEq)]
pub enum Mode<T>
where
T: ModeType,
{
/// Adding the specified mode, optionally with an argument.
Plus(T, Option<String>),
/// Removing the specified mode, optionally with an argument.
Minus(T, Option<String>),
}
impl<T> fmt::Display for Mode<T>
where
T: ModeType,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
&Mode::Plus(ref mode, Some(ref arg)) => write!(f, "{}{} {}", "+", mode, arg),
&Mode::Minus(ref mode, Some(ref arg)) => write!(f, "{}{} {}", "-", mode, arg),
&Mode::Plus(ref mode, None) => write!(f, "{}{}", "+", mode),
&Mode::Minus(ref mode, None) => write!(f, "{}{}", "-", mode),
}
}
}
enum PlusMinus {
Plus,
Minus,
}
// MODE user [modes]
impl Mode<UserMode> {
// TODO: turning more edge cases into errors.
/// Parses the specified mode string as user modes.
pub fn as_user_modes(s: &str) -> error::Result<Vec<Mode<UserMode>>> {
use self::PlusMinus::*;
let mut res = vec![];
let mut pieces = s.split(" ");
for term in pieces.clone() {
if term.starts_with("+") || term.starts_with("-") {
let _ = pieces.next();
let mut chars = term.chars();
let init = match chars.next() {
Some('+') => Plus,
Some('-') => Minus,
_ => return Err(error::ErrorKind::ModeParsingFailed.into()),
};
for c in chars {
let mode = UserMode::from_char(c)?;
let arg = if mode.takes_arg() {
pieces.next()
} else {
None
};
res.push(match init {
Plus => Mode::Plus(mode, arg.map(|s| s.to_owned())),
Minus => Mode::Minus(mode, arg.map(|s| s.to_owned())),
})
}
}
}
Ok(res)
}
}
// MODE channel [modes [modeparams]]
impl Mode<ChannelMode> {
// TODO: turning more edge cases into errors.
/// Parses the specified mode string as channel modes.
pub fn as_channel_modes(s: &str) -> error::Result<Vec<Mode<ChannelMode>>> {
use self::PlusMinus::*;
let mut res = vec![];
let mut pieces = s.split(" ");
for term in pieces.clone() {
if term.starts_with("+") || term.starts_with("-") {
let _ = pieces.next();
let mut chars = term.chars();
let init = match chars.next() {
Some('+') => Plus,
Some('-') => Minus,
_ => return Err(error::ErrorKind::ModeParsingFailed.into()),
};
for c in chars {
let mode = ChannelMode::from_char(c)?;
let arg = if mode.takes_arg() {
pieces.next()
} else {
None
};
res.push(match init {
Plus => Mode::Plus(mode, arg.map(|s| s.to_owned())),
Minus => Mode::Minus(mode, arg.map(|s| s.to_owned())),
})
}
}
}
Ok(res)
}
}
/// An extension trait giving strings a function to check if they are a channel.
pub trait ChannelExt {
/// Returns true if the specified name is a channel name.
fn is_channel_name(&self) -> bool;
}
impl<'a> ChannelExt for &'a str {
fn is_channel_name(&self) -> bool {
self.starts_with("#") ||
self.starts_with("&") ||
self.starts_with("+") ||
self.starts_with("!")
}
}

View file

@ -8,7 +8,7 @@ pub mod message;
pub mod response;
pub use self::caps::{Capability, NegotiationVersion};
pub use self::command::{BatchSubCommand, CapSubCommand, Command};
pub use self::command::{BatchSubCommand, CapSubCommand, ChannelMode, Command, Mode, UserMode};
pub use self::irc::IrcCodec;
pub use self::message::Message;
pub use self::response::Response;