/************************************************************************************ * * 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 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) { } command_source::command_source(const struct message_create_t& event) : guild_id(event.msg.guild_id), channel_id(event.msg.channel_id), command_id(0), issuer(event.msg.author), message_event(event), interaction_event(std::nullopt) { } command_source::command_source(const struct interaction_create_t& event) : guild_id(event.command.guild_id), channel_id(event.command.channel_id), command_id(event.command.id), command_token(event.command.token), issuer(event.command.usr), message_event(std::nullopt), interaction_event(event) { } 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) { interactions = o->on_slashcommand([this](const dpp::slashcommand_t &event) { this->route(event); }); messages = o->on_message_create([this](const dpp::message_create_t & event) { this->route(event); }); } else { interactions = messages = 0; } } commandhandler& commandhandler::set_owner(cluster* o) { owner = o; return *this; } commandhandler::~commandhandler() { if (messages && interactions) { owner->on_message_create.detach(messages); owner->on_slashcommand.detach(interactions); } } commandhandler& commandhandler::add_prefix(const std::string &prefix) { prefixes.emplace_back(prefix); if (prefix == "/") { /* Register existing slash commands */ 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.empty()) { if (owner->me.id.empty()) { throw dpp::logic_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 = co_string; 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; case pt_double: cot = co_number; 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) { if (bulk_registration_list_guild.find(guild_id) == bulk_registration_list_guild.end()) { bulk_registration_list_guild[guild_id] = {}; } bulk_registration_list_guild[guild_id].emplace_back(newcommand); } else { bulk_registration_list_global.emplace_back(newcommand); } } return *this; } commandhandler& commandhandler::register_commands() { for(auto & guild_commands : bulk_registration_list_guild) { owner->guild_bulk_command_create(guild_commands.second, guild_commands.first, [guild_commands, this](const dpp::confirmation_callback_t &callback) { if (callback.is_error()) { this->owner->log(dpp::ll_error, "Failed to register guild slash commands for guild id '" + std::to_string(guild_commands.first) + "': " + callback.http_info.body); } }); } owner->global_bulk_command_create(bulk_registration_list_global, [this](const dpp::confirmation_callback_t &callback) { if (callback.is_error()) { this->owner->log(dpp::ll_error, "Failed to register global slash commands: " + callback.http_info.body); } }); return *this; } bool commandhandler::string_has_prefix(std::string &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; } /* Note that message based command routing relies on cache to resolve ping types (e.g. user, channel ping). * There isn't really a way around this for many things because there is no 'resolved' member for it. * We only get resolved information for the user issuing the command. */ void commandhandler::route(const struct dpp::message_create_t& event) { std::string msg_content = event.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 != event.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)); 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)); 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)); user* u = dpp::find_user(uid); if (u) { dpp::resolved_user m; m.user = *u; dpp::guild* g = dpp::find_guild(event.msg.guild_id); if (g->members.find(uid) != g->members.end()) { m.member = g->members[uid]; } param = m; } } } break; case pt_integer: { int64_t x = 0; ss >> x; param = x; } break; case pt_double: { double x = 0; ss >> x; param = x; } break; 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.emplace_back(p.first, param); } /* Call command handler */ found_cmd->second.func(command, call_params, command_source(event)); } } } void commandhandler::route(const struct slashcommand_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); dpp::command_resolved res = event.command.resolved; if (p.second.optional && slash_parameter.index() == 0 /* std::monostate */) { /* Missing optional parameter, skip this */ continue; } 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) { /* Use cache if the role is in the cache */ param = *r; } else { /* Otherwise use interaction resolved fields */ if (res.roles.find(rid) != res.roles.end()) { param = res.roles[rid]; } } } break; case pt_channel: { snowflake cid = std::get(slash_parameter); channel* c = dpp::find_channel(cid); if (c) { /* Use cache if the channel is in the cache */ param = *c; } else { /* Otherwise use interaction resolved fields */ if (res.channels.find(cid) != res.channels.end()) { param = res.channels[cid]; } } } break; case pt_user: { snowflake uid = std::get(slash_parameter); /* TODO: Make this used resolved, not cache */ user* u = dpp::find_user(uid); if (u) { /* Use the cache if the user is in the cache */ dpp::resolved_user m; m.user = *u; dpp::guild* g = dpp::find_guild(event.command.guild_id); if (g->members.find(uid) != g->members.end()) { m.member = g->members[uid]; } param = m; } else { /* Otherwise use interaction resolved fields */ if ( event.command.resolved.users.find(uid) != event.command.resolved.users.end() && event.command.resolved.members.find(uid) != event.command.resolved.members.end() ) { /* Fill in both member and user info */ dpp::resolved_user m; m.member = res.members[uid]; m.user = res.users[uid]; param = m; } } } break; case pt_integer: { int64_t i = std::get(slash_parameter); param = i; } break; case pt_boolean: { bool b = std::get(slash_parameter); param = b; } break; case pt_double: { double b = std::get(slash_parameter); param = b; } break; } /* Add parameter to the list */ call_params.emplace_back(p.first, param); } /* Call command handler */ found_cmd->second.func(cmd.name, call_params, command_source(event)); } } void commandhandler::reply(const dpp::message &m, command_source source, command_completion_event_t callback) { dpp::message msg = m; msg.owner = this->owner; 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), callback); } else { owner->message_create(msg, callback); } } void commandhandler::thinking(command_source source, command_completion_event_t callback) { dpp::message msg(this->owner); msg.content = "*"; 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_deferred_channel_message_with_source, msg), callback); } } void commandhandler::thonk(command_source source, command_completion_event_t callback) { thinking(source, callback); } };