1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
use super::BotCmdHandler;
use super::Error;
use super::Module;
use super::MsgMetadata;
use super::Reaction;
use super::Result;
use super::State;
use std;
use std::borrow::Cow;
use std::sync::Arc;
use util;
use yaml_rust::Yaml;

pub struct BotCommand {
    pub name: Cow<'static, str>,
    pub provider: Arc<Module>,
    pub auth_lvl: BotCmdAuthLvl,
    pub(super) handler: Arc<BotCmdHandler>,
    pub usage_str: Cow<'static, str>,
    pub(super) usage_yaml: Yaml,
    pub help_msg: Cow<'static, str>,
}

#[derive(Debug)]
pub enum BotCmdAttr {}

#[derive(Debug)]
pub enum BotCmdResult {
    /// The command was processed successfully. Pass through a `Reaction`.
    Ok(Reaction),

    /// A user invoked the command without having sufficient authorization to do so. A reply will
    /// be sent informing the user of this.
    Unauthorized,

    /// A user invoked the command with incorrect syntax. A reply will be sent informing the user
    /// of the correct syntax.
    SyntaxErr,

    /// A user invoked the command without providing a required argument, named by the given
    /// string. This is a more specific version of `SyntaxErr` and should be preferred where
    /// applicable.
    ArgMissing(Cow<'static, str>),

    /// A user invoked the command in one-to-one communication (a.k.a. "query" and "PM") without
    /// providing an argument that is required only in one-to-one communication (such as a channel
    /// name, which could normally default to the name of the channel in which the command was
    /// used), named by the given string. This is a more specific version of `ArgMissing` and
    /// should be preferred where applicable.
    ArgMissing1To1(Cow<'static, str>),

    /// Pass through an instance of the framework's `Error` type.
    LibErr(Error),

    /// A user made some miscellaneous error in invoking the command. The given string will be
    /// included in a reply informing the user of their error.
    UserErrMsg(Cow<'static, str>),

    /// A miscellaneous error that doesn't seem to be the user's fault occurred while the bot was
    /// processing the command. The given string will be included in a reply informing the user of
    /// this.
    BotErrMsg(Cow<'static, str>),
}

impl From<Reaction> for BotCmdResult {
    fn from(r: Reaction) -> Self {
        BotCmdResult::Ok(r)
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BotCmdAuthLvl {
    Public,
    Admin,
}

pub(super) fn run(
    state: &State,
    cmd_name: &str,
    cmd_args: &str,
    metadata: &MsgMetadata,
) -> Result<Option<BotCmdResult>> {
    let &BotCommand {
        ref name,
        ref provider,
        ref auth_lvl,
        ref handler,
        ref usage_yaml,
        usage_str: _,
        help_msg: _,
    } = match state.commands.get(cmd_name) {
        Some(c) => c,
        None => return Ok(None),
    };

    let user_authorized = match auth_lvl {
        &BotCmdAuthLvl::Public => Ok(true),
        &BotCmdAuthLvl::Admin => state.have_admin(metadata.prefix),
    };

    let arg = match parse_arg(usage_yaml, cmd_args) {
        Ok(arg) => arg,
        Err(res) => return Ok(Some(res)),
    };

    let result = match user_authorized {
        Ok(true) => {
            debug!("Running bot command {:?} with arg: {:?}", name, arg);
            match util::run_handler("command", name.clone(), || {
                handler.run(state, &metadata, &arg)
            }) {
                Ok(r) => r,
                Err(e) => BotCmdResult::LibErr(e),
            }
        }
        Ok(false) => BotCmdResult::Unauthorized,
        Err(e) => BotCmdResult::LibErr(e),
    };

    // TODO: Filter `QUIT`s in `irc_send` instead, and check `Reaction::RawMsg`s as well.
    match result {
        BotCmdResult::Ok(Reaction::Quit(ref s)) if *auth_lvl != BotCmdAuthLvl::Admin => {
            Ok(Some(BotCmdResult::BotErrMsg(
                format!(
                    "Only commands at authorization level {auth_lvl_owner:?} \
                     may tell the bot to quit, but the command {cmd_name:?} \
                     from module {provider_name:?}, at authorization level \
                     {cmd_auth_lvl:?}, has told the bot to quit with quit \
                     message {quit_msg:?}.",
                    auth_lvl_owner = BotCmdAuthLvl::Admin,
                    cmd_name = name,
                    provider_name = provider.name,
                    cmd_auth_lvl = auth_lvl,
                    quit_msg = s
                ).into(),
            )))
        }
        r => Ok(Some(r)),
    }
}

fn parse_arg<'s>(syntax: &'s Yaml, arg_str: &str) -> std::result::Result<Yaml, BotCmdResult> {
    use util::yaml as uy;

    match uy::parse_and_check_node(arg_str, syntax, "<argument>", || {
        Yaml::Hash(Default::default())
    }) {
        Ok(arg) => Ok(arg),
        Err(uy::Error(uy::ErrorKind::YamlScan(_), _)) => Err(BotCmdResult::SyntaxErr),
        Err(err) => Err(BotCmdResult::LibErr(err.into())),
    }
}