/************************************************************************************ * * D++, A Lightweight C++ library for Discord * * Copyright 2021 Craig Edwards and D++ contributors * (https://github.com/brainboxdotcc/DPP/graphs/contributors) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ************************************************************************************/ #include #include #include #include #include #include #include #include #include namespace dpp { param_info::param_info(parameter_type t, bool o, const std::string &d, const std::map &opts) : type(t), optional(o), description(d), choices(opts) { } commandhandler::commandhandler(cluster* o, bool auto_hook_events, snowflake application_id) : slash_commands_enabled(false), owner(o), app_id(application_id) { if (!application_id && o->me.id) { app_id = o->me.id; } if (auto_hook_events) { o->on_interaction_create([this](const dpp::interaction_create_t &event) { this->route(event); }); o->on_message_create([this](const dpp::message_create_t & event) { this->route(*event.msg); }); } } commandhandler& commandhandler::set_owner(cluster* o) { owner = o; return *this; } commandhandler::~commandhandler() { } commandhandler& commandhandler::add_prefix(const std::string &prefix) { prefixes.push_back(prefix); if (prefix == "/") { if (!slash_commands_enabled) { /* Register existing slash commands */ slash_commands_enabled = true; } else { slash_commands_enabled = true; } } return *this; } commandhandler& commandhandler::add_command(const std::string &command, const parameter_registration_t ¶meters, command_handler handler, const std::string &description, snowflake guild_id) { command_info_t i; i.func = handler; i.guild_id = guild_id; i.parameters = parameters; commands[lowercase(command)] = i; if (slash_commands_enabled) { if (this->app_id == 0) { if (owner->me.id == 0) { throw dpp::exception("Command handler not ready (i don't know my application ID)"); } else { this->app_id = owner->me.id; } } dpp::slashcommand newcommand; /* Create a new global command on ready event */ newcommand.set_name(lowercase(command)).set_description(description).set_application_id(this->app_id); for (auto& parameter : parameters) { command_option_type cot; switch (parameter.second.type) { case pt_boolean: cot = co_boolean; break; case pt_integer: cot = co_integer; break; case pt_string: cot = co_string; break; case pt_user: cot = co_user; break; case pt_role: cot = co_role; break; case pt_channel: cot = co_channel; break; } command_option opt(cot, parameter.first, parameter.second.description, !parameter.second.optional); if (!parameter.second.choices.empty()) { for (auto& c : parameter.second.choices) { opt.add_choice(dpp::command_option_choice(c.second, c.first)); } } newcommand.add_option(opt); } /* Register the command */ if (guild_id) { owner->guild_command_create(newcommand, guild_id, [command, this](const dpp::confirmation_callback_t &callback) { if (callback.is_error()) { this->owner->log(dpp::ll_error, fmt::format("Failed to register guild slash command '{}': {}", command, callback.http_info.body)); } }); } else { owner->global_command_create(newcommand, [command, this](const dpp::confirmation_callback_t &callback) { if (callback.is_error()) { this->owner->log(dpp::ll_error, fmt::format("Failed to register global slash command '{}': {}", command, callback.http_info.body)); } }); } } return *this; } bool commandhandler::string_has_prefix(std::string &str) { size_t str_length = utility::utf8len(str); for (auto& p : prefixes) { size_t prefix_length = utility::utf8len(p); if (utility::utf8substr(str, 0, prefix_length) == p) { str.erase(str.begin(), str.begin() + prefix_length); return true; } } return false; } void commandhandler::route(const dpp::message& msg) { std::string msg_content = msg.content; if (string_has_prefix(msg_content)) { /* Put the string into stringstream to parse parameters at spaces. * We use stringstream as it handles multiple spaces etc nicely. */ std::stringstream ss(msg_content); std::string command; ss >> command; /* Prefixed command, the prefix was removed */ auto found_cmd = commands.find(lowercase(command)); if (found_cmd != commands.end()) { /* Filter out guild specific commands that are not for the current guild */ if (found_cmd->second.guild_id && found_cmd->second.guild_id != msg.guild_id) { return; } parameter_list_t call_params; /* Command found; parse parameters */ for (auto& p : found_cmd->second.parameters) { command_parameter param; /* Check for end of stream */ if (!ss) { /* If it's an optional param, we dont care */ if (!p.second.optional) { /* Trigger missing parameter handler? */ } break; } switch (p.second.type) { case pt_string: { std::string x; ss >> x; param = x; } break; case pt_role: { std::string x; ss >> x; if (x.length() > 4 && x[0] == '<' && x[1] == '&') { snowflake rid = from_string(x.substr(2, x.length() - 1), std::dec); role* r = dpp::find_role(rid); if (r) { param = *r; } } } break; case pt_channel: { std::string x; ss >> x; if (x.length() > 4 && x[0] == '<' && x[1] == '#') { snowflake cid = from_string(x.substr(2, x.length() - 1), std::dec); channel* c = dpp::find_channel(cid); if (c) { param = *c; } } } break; case pt_user: { std::string x; ss >> x; if (x.length() > 4 && x[0] == '<' && x[1] == '@') { snowflake uid = from_string(x.substr(2, x.length() - 1), std::dec); user* u = dpp::find_user(uid); if (u) { param = *u; } } } break; case pt_integer: { int32_t x = 0; ss >> x; param = x; } case pt_boolean: { std::string x; bool y = false; ss >> x; x = lowercase(x); if (x == "yes" || x == "1" || x == "true") { y = true; } param = y; } break; } /* Add parameter to the list */ call_params.push_back(std::make_pair(p.first, param)); } /* Call command handler */ command_source source; source.command_id = 0; source.guild_id = msg.guild_id; source.channel_id = msg.channel_id; source.issuer = msg.author; found_cmd->second.func(command, call_params, source); } } } void commandhandler::route(const interaction_create_t & event) { /* We don't need to check for prefixes here, slash command interactions * dont have prefixes at all. */ command_interaction cmd = std::get(event.command.data); auto found_cmd = commands.find(lowercase(cmd.name)); if (found_cmd != commands.end()) { /* Command found; parse parameters */ parameter_list_t call_params; for (auto& p : found_cmd->second.parameters) { command_parameter param; const command_value& slash_parameter = event.get_parameter(p.first); if (p.second.optional && slash_parameter.valueless_by_exception()) { /* Missing optional parameter, skip this */ continue; } try { switch (p.second.type) { case pt_string: { std::string s = std::get(slash_parameter); param = s; } break; case pt_role: { snowflake rid = std::get(slash_parameter); role* r = dpp::find_role(rid); if (r) { param = *r; } } break; case pt_channel: { snowflake cid = std::get(slash_parameter); channel* c = dpp::find_channel(cid); if (c) { param = *c; } } break; case pt_user: { snowflake uid = std::get(slash_parameter); user* u = dpp::find_user(uid); if (u) { param = *u; } } break; case pt_integer: { int32_t i = std::get(slash_parameter); param = i; } case pt_boolean: { bool b = std::get(slash_parameter); param = b; } break; } } catch (const std::bad_variant_access& e) { /* Missing optional parameter, skip this */ continue; } /* Add parameter to the list */ call_params.push_back(std::make_pair(p.first, param)); } /* Call command handler */ command_source source; source.command_id = event.command.id; source.command_token = event.command.token; source.guild_id = event.command.guild_id; source.channel_id = event.command.channel_id; source.issuer = (user*)&event.command.usr; found_cmd->second.func(cmd.name, call_params, source); } } void commandhandler::reply(const dpp::message &m, command_source source) { dpp::message msg = m; msg.guild_id = source.guild_id; msg.channel_id = source.channel_id; if (!source.command_token.empty() && source.command_id) { owner->interaction_response_create(source.command_id, source.command_token, dpp::interaction_response(ir_channel_message_with_source, msg)); } else { owner->message_create(msg); } } };