irc_proto/
mode.rs

1//! A module defining an API for IRC user and channel modes.
2use std::fmt;
3
4use crate::command::Command;
5use crate::error::MessageParseError;
6
7/// A marker trait for different kinds of Modes.
8pub trait ModeType: fmt::Display + fmt::Debug + Clone + PartialEq {
9    /// Creates a command of this kind.
10    fn mode(target: &str, modes: &[Mode<Self>]) -> Command;
11
12    /// Returns true if this mode takes an argument, and false otherwise.
13    fn takes_arg(&self) -> bool;
14
15    /// Creates a Mode from a given char.
16    fn from_char(c: char) -> Self;
17}
18
19/// User modes for the MODE command.
20#[derive(Clone, Debug, PartialEq)]
21pub enum UserMode {
22    /// a - user is flagged as away
23    Away,
24    /// i - marks a users as invisible
25    Invisible,
26    /// w - user receives wallops
27    Wallops,
28    /// r - restricted user connection
29    Restricted,
30    /// o - operator flag
31    Oper,
32    /// O - local operator flag
33    LocalOper,
34    /// s - marks a user for receipt of server notices
35    ServerNotices,
36    /// x - masked hostname
37    MaskedHost,
38
39    /// Any other unknown-to-the-crate mode.
40    Unknown(char),
41}
42
43impl ModeType for UserMode {
44    fn mode(target: &str, modes: &[Mode<Self>]) -> Command {
45        Command::UserMODE(target.to_owned(), modes.to_owned())
46    }
47
48    fn takes_arg(&self) -> bool {
49        false
50    }
51
52    fn from_char(c: char) -> UserMode {
53        use self::UserMode::*;
54
55        match c {
56            'a' => Away,
57            'i' => Invisible,
58            'w' => Wallops,
59            'r' => Restricted,
60            'o' => Oper,
61            'O' => LocalOper,
62            's' => ServerNotices,
63            'x' => MaskedHost,
64            _ => Unknown(c),
65        }
66    }
67}
68
69impl fmt::Display for UserMode {
70    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
71        use self::UserMode::*;
72
73        write!(
74            f,
75            "{}",
76            match *self {
77                Away => 'a',
78                Invisible => 'i',
79                Wallops => 'w',
80                Restricted => 'r',
81                Oper => 'o',
82                LocalOper => 'O',
83                ServerNotices => 's',
84                MaskedHost => 'x',
85                Unknown(c) => c,
86            }
87        )
88    }
89}
90
91/// Channel modes for the MODE command.
92#[derive(Clone, Debug, PartialEq)]
93pub enum ChannelMode {
94    /// b - ban the user from joining or speaking in the channel
95    Ban,
96    /// e - exemptions from bans
97    Exception,
98    /// l - limit the maximum number of users in a channel
99    Limit,
100    /// i - channel becomes invite-only
101    InviteOnly,
102    /// I - exception to invite-only rule
103    InviteException,
104    /// k - specify channel key
105    Key,
106    /// m - channel is in moderated mode
107    Moderated,
108    /// r - entry for registered users only
109    RegisteredOnly,
110    /// s - channel is hidden from listings
111    Secret,
112    /// t - require permissions to edit topic
113    ProtectedTopic,
114    /// n - users must join channels to message them
115    NoExternalMessages,
116
117    /// q - user gets founder permission
118    Founder,
119    /// a - user gets admin or protected permission
120    Admin,
121    /// o - user gets oper permission
122    Oper,
123    /// h - user gets halfop permission
124    Halfop,
125    /// v - user gets voice permission
126    Voice,
127
128    /// Any other unknown-to-the-crate mode.
129    Unknown(char),
130}
131
132impl ModeType for ChannelMode {
133    fn mode(target: &str, modes: &[Mode<Self>]) -> Command {
134        Command::ChannelMODE(target.to_owned(), modes.to_owned())
135    }
136
137    fn takes_arg(&self) -> bool {
138        use self::ChannelMode::*;
139
140        matches!(
141            *self,
142            Ban | Exception
143                | Limit
144                | InviteException
145                | Key
146                | Founder
147                | Admin
148                | Oper
149                | Halfop
150                | Voice
151        )
152    }
153
154    fn from_char(c: char) -> ChannelMode {
155        use self::ChannelMode::*;
156
157        match c {
158            'b' => Ban,
159            'e' => Exception,
160            'l' => Limit,
161            'i' => InviteOnly,
162            'I' => InviteException,
163            'k' => Key,
164            'm' => Moderated,
165            'r' => RegisteredOnly,
166            's' => Secret,
167            't' => ProtectedTopic,
168            'n' => NoExternalMessages,
169            'q' => Founder,
170            'a' => Admin,
171            'o' => Oper,
172            'h' => Halfop,
173            'v' => Voice,
174            _ => Unknown(c),
175        }
176    }
177}
178
179impl fmt::Display for ChannelMode {
180    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
181        use self::ChannelMode::*;
182
183        write!(
184            f,
185            "{}",
186            match *self {
187                Ban => 'b',
188                Exception => 'e',
189                Limit => 'l',
190                InviteOnly => 'i',
191                InviteException => 'I',
192                Key => 'k',
193                Moderated => 'm',
194                RegisteredOnly => 'r',
195                Secret => 's',
196                ProtectedTopic => 't',
197                NoExternalMessages => 'n',
198                Founder => 'q',
199                Admin => 'a',
200                Oper => 'o',
201                Halfop => 'h',
202                Voice => 'v',
203                Unknown(c) => c,
204            }
205        )
206    }
207}
208
209/// A mode argument for the MODE command.
210#[derive(Clone, Debug, PartialEq)]
211pub enum Mode<T>
212where
213    T: ModeType,
214{
215    /// Adding the specified mode, optionally with an argument.
216    Plus(T, Option<String>),
217    /// Removing the specified mode, optionally with an argument.
218    Minus(T, Option<String>),
219    /// No prefix mode, used to query ban list on channel join.
220    NoPrefix(T),
221}
222
223impl<T> Mode<T>
224where
225    T: ModeType,
226{
227    /// Creates a plus mode with an `&str` argument.
228    pub fn plus(inner: T, arg: Option<&str>) -> Mode<T> {
229        Mode::Plus(inner, arg.map(|s| s.to_owned()))
230    }
231
232    /// Creates a minus mode with an `&str` argument.
233    pub fn minus(inner: T, arg: Option<&str>) -> Mode<T> {
234        Mode::Minus(inner, arg.map(|s| s.to_owned()))
235    }
236
237    /// Create a no prefix mode with an `&str` argument.
238    pub fn no_prefix(inner: T) -> Mode<T> {
239        Mode::NoPrefix(inner)
240    }
241
242    /// Gets the mode flag associated with this mode with a + or - prefix as needed.
243    pub fn flag(&self) -> String {
244        match self {
245            Mode::Plus(mode, _) => format!("+{}", mode),
246            Mode::Minus(mode, _) => format!("-{}", mode),
247            Mode::NoPrefix(mode) => mode.to_string(),
248        }
249    }
250
251    /// Gets the arg associated with this mode, if any. Only some channel modes support arguments,
252    /// e.g. b (ban) or o (oper).
253    pub fn arg(&self) -> Option<&str> {
254        match self {
255            Mode::Plus(_, arg) | Mode::Minus(_, arg) => arg.as_deref(),
256            _ => None,
257        }
258    }
259}
260
261impl<T> fmt::Display for Mode<T>
262where
263    T: ModeType,
264{
265    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
266        match *self {
267            Mode::Plus(_, Some(ref arg)) | Mode::Minus(_, Some(ref arg)) => {
268                write!(f, "{} {}", self.flag(), arg)
269            }
270            _ => write!(f, "{}", self.flag()),
271        }
272    }
273}
274
275enum PlusMinus {
276    Plus,
277    Minus,
278    NoPrefix,
279}
280
281// MODE user [modes]
282impl Mode<UserMode> {
283    // TODO: turning more edge cases into errors.
284    /// Parses the specified mode string as user modes.
285    pub fn as_user_modes(pieces: &[&str]) -> Result<Vec<Mode<UserMode>>, MessageParseError> {
286        parse_modes(pieces)
287    }
288}
289
290// MODE channel [modes [modeparams]]
291impl Mode<ChannelMode> {
292    // TODO: turning more edge cases into errors.
293    /// Parses the specified mode string as channel modes.
294    pub fn as_channel_modes(pieces: &[&str]) -> Result<Vec<Mode<ChannelMode>>, MessageParseError> {
295        parse_modes(pieces)
296    }
297}
298
299fn parse_modes<T>(pieces: &[&str]) -> Result<Vec<Mode<T>>, MessageParseError>
300where
301    T: ModeType,
302{
303    use self::PlusMinus::*;
304
305    let mut res = vec![];
306
307    if let Some((first, rest)) = pieces.split_first() {
308        let mut modes = first.chars();
309        let mut args = rest.iter();
310
311        let mut cur_mod = match modes.next() {
312            Some('+') => Plus,
313            Some('-') => Minus,
314            Some(_) => {
315                // rewind modes
316                modes = first.chars();
317                NoPrefix
318            }
319            None => {
320                // No modifier
321                return Ok(res);
322            }
323        };
324
325        for c in modes {
326            match c {
327                '+' => cur_mod = Plus,
328                '-' => cur_mod = Minus,
329                _ => {
330                    let mode = T::from_char(c);
331                    let arg = if mode.takes_arg() {
332                        // TODO: if there's no arg, this should error
333                        args.next()
334                    } else {
335                        None
336                    };
337                    res.push(match cur_mod {
338                        Plus => Mode::Plus(mode, arg.map(|s| s.to_string())),
339                        Minus => Mode::Minus(mode, arg.map(|s| s.to_string())),
340                        NoPrefix => Mode::NoPrefix(mode),
341                    })
342                }
343            }
344        }
345
346        // TODO: if there are extra args left, this should error
347    } else {
348        // No modifier
349    };
350
351    Ok(res)
352}
353
354#[cfg(test)]
355mod test {
356    use super::{ChannelMode, Mode};
357    use crate::Command;
358    use crate::Message;
359
360    #[test]
361    fn parse_channel_mode() {
362        let cmd = "MODE #foo +r".parse::<Message>().unwrap().command;
363        assert_eq!(
364            Command::ChannelMODE(
365                "#foo".to_string(),
366                vec![Mode::Plus(ChannelMode::RegisteredOnly, None)]
367            ),
368            cmd
369        );
370    }
371
372    #[test]
373    fn parse_no_mode() {
374        let cmd = "MODE #foo".parse::<Message>().unwrap().command;
375        assert_eq!(Command::ChannelMODE("#foo".to_string(), vec![]), cmd);
376    }
377
378    #[test]
379    fn parse_no_plus() {
380        let cmd = "MODE #foo b".parse::<Message>().unwrap().command;
381        assert_eq!(
382            Command::ChannelMODE("#foo".to_string(), vec![Mode::NoPrefix(ChannelMode::Ban)]),
383            cmd
384        );
385    }
386}