#include "../bot.hpp" #include "../util.hpp" #include #include #include #include namespace PingMode { constexpr uint8_t all = 0b11, creator = 0b10, managers = 0b01, none = 0b00; } class Ticket { Bot *bot; std::unordered_map uuids; std::shared_mutex uuid_mutex; void db_add_guild(dpp::snowflake guild_id) { bot->db << "INSERT OR IGNORE INTO ticket_guild_settings (id, channel_name, management_role, any_close, ping) VALUES (?, 'ticket-', '0', FALSE, 0);" << std::to_string(guild_id); } std::string build_channel_topic(dpp::snowflake author_id, dpp::snowflake moderator_id = 0) { return "Ticket created by <@"+std::to_string(author_id)+">"+(moderator_id?", taken care of by <@"+std::to_string(moderator_id)+">":""); } void close_ticket(const dpp::interaction_create_t& event) { // Get ticket from database std::string author_id, author_full_name; try { bot->db << "SELECT author_id, author_full_name FROM tickets " "WHERE channel_id = ?;" << std::to_string(event.command.channel_id) >> std::tie(author_id, author_full_name); } catch (...) { event.reply(dpp::message("Error. Is this actually a ticket channel?").set_flags(dpp::message_flags::m_ephemeral)); return; } // Get guild from database db_add_guild(event.command.guild_id); std::string management_role_id, logs_channel_id; unsigned any_close; bot->db << "SELECT management_role, logs_channel, any_close FROM ticket_guild_settings " "WHERE id = ?;" << std::to_string(event.command.guild_id) >> std::tie(management_role_id, logs_channel_id, any_close); // Check permissions if (!any_close) { if (!can_change_ticket(event, management_role_id)) { event.reply(dpp::message("You can not close this ticket!").set_flags(dpp::message_flags::m_ephemeral)); return; } } // Send log into logs channel if (!logs_channel_id.empty()) { event.thinking(true); // Fetch all messages from channel std::thread([this, event, author_id, author_full_name, logs_channel_id]() { // Generate log auto log = generate_final_log(event.command.channel_id, author_id, author_full_name, event.command.usr); // Upload file dpp::message msg; auto ticketID = Util::int_as_hex(event.command.channel_id); msg.set_channel_id(logs_channel_id); msg.set_content("Ticket #"+ticketID); msg.add_file("ticket-"+ticketID+".txt", std::move(log)); bot->cluster.message_create(msg, [&](const dpp::confirmation_callback_t& ccb) { if (ccb.is_error()) { bot->db << "UPDATE ticket_guild_settings " "SET logs_channel = NULL " "WHERE id = ?;" << std::to_string(event.command.guild_id); } }); // Delete channel bot->cluster.channel_delete(event.command.channel_id); }).detach(); } else { // Delete channel bot->cluster.channel_delete(event.command.channel_id); } } std::string generate_final_log(dpp::snowflake channel_id, dpp::snowflake author_id, std::string_view author_full_name, const dpp::user& closing_user) noexcept { // Get title and description std::string title, description; try { bot->db << "SELECT title, description FROM tickets " "WHERE channel_id = ?;" << std::to_string(channel_id) >> std::tie(title, description); } catch (...) { title = "Invalid ticket"; description = "Data could not be retrieved from the database."; } // Begin log std::ostringstream log; log << title << '\n' << description << "\n\n" "Ticket created by " << author_full_name << " (" << author_id << ")\n"; // Collect all messages constexpr unsigned fetch_block = 100; size_t last_message_count = fetch_block; std::map messages; while (last_message_count == fetch_block) { try { // Get messages dpp::snowflake last_msg; if (!messages.empty()) { last_msg = (*messages.begin()).second.id; } else { last_msg = 0; } auto new_messages = bot->cluster.messages_get_sync(channel_id, 0, last_msg, 0, fetch_block); // Update counters last_message_count = new_messages.size(); // Add them to sorted vector for (auto& [message_id, message] : new_messages) { messages[message_id] = std::move(message); } } catch (...) { break; } } // Append messages to log unsigned no = 1; for (const auto& [message_id, message] : messages) { // Make sure message has content if (message.content.empty()) { continue; } // Sanitize message auto content = message.content; for (auto& c : content) { if (c == '\n') c = ' '; } // Get referenced message if needed if (message.message_reference.message_id && !message.is_source_message_deleted()) { auto res = messages.find(message.message_reference.message_id); if (res != messages.end()) { auto reference_msg_no = std::distance(messages.begin(), res)-1; content = "(Reply to "+dpp::leading_zeroes(reference_msg_no, 3)+") "+content; } } // Append message to log log << dpp::leading_zeroes(no++, 3) << " <" << message.author.format_username() << "> " << content << '\n'; } log << "Ticket has been closed by " << closing_user.format_username() + " (" << closing_user.id << ")."; return log.str(); } bool can_change_ticket(const dpp::interaction_create_t& event, dpp::snowflake management_role_id) { const auto& roles = event.command.member.get_roles(); return event.command.get_guild().base_permissions(event.command.member).has(dpp::permissions::p_manage_channels) || (management_role_id && std::find_if(roles.begin(), roles.end(), [management_role_id](auto a) { return a == management_role_id; }) != roles.end()); } public: Ticket(Bot *_bot) : bot(_bot) { bot->db << "CREATE TABLE IF NOT EXISTS ticket_guild_settings (" " id TEXT PRIMARY KEY NOT NULL," " category TEXT," " management_role TEXT," " logs_channel TEXT," " channel_name TEXT," " any_close INTEGER," " ping INTEGER," " UNIQUE(id)" ");"; bot->db << "CREATE TABLE IF NOT EXISTS tickets (" " guild_id TEXT NOT NULL," " channel_id TEXT NOT NULL," " author_id TEXT NOT NULL," " author_full_name TEXT," " title TEXT," " description TEXT," " UNIQUE(guild_id, channel_id, author_id)" ");"; bot->add_chatcommand(Bot::ChatCommand({"ticket_settings", "ticket_setup"}, "Set up tickets", dpp::slashcommand().add_option(dpp::command_option(dpp::command_option_type::co_channel, "category", "Category in which tickets are created", false)).add_option(dpp::command_option(dpp::command_option_type::co_role, "role", "Role that can manage tickets", false)).add_option(dpp::command_option(dpp::command_option_type::co_channel, "logs_channel", "Channel in which ticket logs are saved", false)).add_option(dpp::command_option(dpp::command_option_type::co_string, "channel_name_prefix", "Text to prefix channel names with", false)).add_option(dpp::command_option(dpp::command_option_type::co_boolean, "any_close", "Whether everyone is allowed to close tickets", false)).add_option(dpp::command_option(dpp::command_option_type::co_boolean, "ping", "Whether role should be notified about new tickets", false))), [&](const dpp::slashcommand_t& event) { // Check that user has the correct permissions if (!event.command.get_guild().base_permissions(event.command.member).has(dpp::permissions::p_manage_channels | dpp::permissions::p_manage_roles)) { event.reply(dpp::message("You need channel and role management permissions to use this command.").set_flags(dpp::message_flags::m_ephemeral)); return; } // Get parameters auto category_opt = event.get_parameter("category"), role_opt = event.get_parameter("role"), logs_channel_opt = event.get_parameter("logs_channel"), channel_name_opt = event.get_parameter("channel_name_prefix"), any_close_opt = event.get_parameter("any_close"), ping_opt = event.get_parameter("ping"); // Update database db_add_guild(event.command.guild_id); if (category_opt.index() != 0) { bot->cluster.channel_get(std::get(category_opt), [this, event](const dpp::confirmation_callback_t& ccb) { if (!ccb.is_error()) { auto channel = ccb.get(); // Ensure channel is part of THIS guild if (channel.guild_id != event.command.guild_id) { return; } // Use channel or parent channel as category dpp::snowflake target; if (channel.is_category()) { target = channel.id; } else if (channel.parent_id) { target = channel.parent_id; } else { return; } // Update database bot->db << "UPDATE ticket_guild_settings " "SET category = ? " "WHERE id = ?;" << std::to_string(target) << std::to_string(channel.guild_id); } }); bot->db << "UPDATE ticket_guild_settings " "SET category = ? " "WHERE id = ?;" << std::to_string(std::get(category_opt)) << std::to_string(event.command.guild_id); } if (role_opt.index() != 0) { bot->db << "UPDATE ticket_guild_settings " "SET management_role = ? " "WHERE id = ?;" << std::to_string(std::get(role_opt)) << std::to_string(event.command.guild_id); } if (logs_channel_opt.index() != 0) { bot->db << "UPDATE ticket_guild_settings " "SET logs_channel = ? " "WHERE id = ?;" << std::to_string(std::get(logs_channel_opt)) << std::to_string(event.command.guild_id); } if (channel_name_opt.index() != 0) { bot->db << "UPDATE ticket_guild_settings " "SET channel_name = ? " "WHERE id = ?;" << std::get(channel_name_opt) << std::to_string(event.command.guild_id); } if (any_close_opt.index() != 0) { bot->db << "UPDATE ticket_guild_settings " "SET any_close = ? " "WHERE id = ?;" << unsigned(std::get(any_close_opt)) << std::to_string(event.command.guild_id); } if (ping_opt.index() != 0) { bot->db << "UPDATE ticket_guild_settings " "SET ping = ? " "WHERE id = ?;" << unsigned(std::get(ping_opt)?PingMode::all:PingMode::none) << std::to_string(event.command.guild_id); } // Send reply event.reply(dpp::message("Done!").set_flags(dpp::message_flags::m_ephemeral)); }); bot->add_chatcommand(Bot::ChatCommand({"tickets", "ticket_embed"}, "Create a ticket creation embed", dpp::slashcommand().add_option(dpp::command_option(dpp::command_option_type::co_channel, "channel", "Channel in which tickets should be creatable", true)).add_option(dpp::command_option(dpp::command_option_type::co_string, "title", "Title", false)).add_option(dpp::command_option(dpp::command_option_type::co_string, "text", "Description", false)).add_option(dpp::command_option(dpp::command_option_type::co_string, "footer_text", "Footer", false)).add_option(dpp::command_option(dpp::command_option_type::co_string, "color", "Color", false))), [&](const dpp::slashcommand_t& event) { // Check that user has the correct permissions if (!event.command.get_guild().base_permissions(event.command.member).has(dpp::permissions::p_manage_channels | dpp::permissions::p_manage_roles)) { event.reply(dpp::message("You need channel and role management permissions to use this command.").set_flags(dpp::message_flags::m_ephemeral)); return; } // Get parameters auto channel_id = std::get(event.get_parameter("channel")); auto title_opt = event.get_parameter("title"), text_opt = event.get_parameter("text"), color_opt = event.get_parameter("color"); auto footer_text_opt = event.get_parameter("footer_text"); auto title = title_opt.index()!=0?std::get(title_opt):"Support tickets", text = text_opt.index()!=0?std::get(text_opt):"Click the button below to create a ticket!", footer_text = footer_text_opt.index()!=0?std::get(footer_text_opt):""; unsigned color = Util::str_to_color(color_opt.index()!=0?std::get(color_opt):"#ff2217"); // Send embed there dpp::message msg; msg.set_channel_id(channel_id) .add_embed(dpp::embed() .set_title(title) .set_description(text) .set_footer(footer_text, "") .set_color(color!=-1U?color:0xffffff)) .add_component(dpp::component() .add_component(dpp::component() .set_label("Create ticket") .set_type(dpp::component_type::cot_button) .set_emoji("🎟️") .set_style(dpp::component_style::cos_primary) .set_id("ticket_create"))); bot->cluster.message_create(msg); // Get ticket management role to potentially warn about std::string management_role_id; db_add_guild(event.command.guild_id); bot->db << "SELECT management_role FROM ticket_guild_settings " "WHERE id = ?;" << std::to_string(event.command.guild_id) >> std::tie(management_role_id); // Report success and potentially warn about missing ticket management role configuration event.reply("Ticket creation is now possible in <#"+std::to_string(channel_id)+">!\n" "**Tip:** To disable tickets, it is sufficient to delete the created embed."+ (color==-1U?"\n**Error:** The specified color was not recognized. Try a color code!":"")+ (management_role_id=="0"?"\n**Warning:** You should define a ticket management role using `/ticket_settings`!":"")); }); bot->add_chatcommand(Bot::ChatCommand({"ticket_close"}, "Close a ticket"), [&](const dpp::slashcommand_t& event) { close_ticket(event); }); bot->cluster.on_button_click([this](const dpp::button_click_t& event) { if (event.custom_id == "ticket_create") { // Create and send ticket creation embed dpp::interaction_modal_response modal("ticket_modal", "Support ticket creation"); modal.add_component(dpp::component() .set_label("Summary") .set_id("title") .set_type(dpp::cot_text) .set_placeholder("Summary of your request") .set_min_length(5) .set_max_length(50) .set_text_style(dpp::text_short) .set_required(true)) .add_row() .add_component(dpp::component() .set_label("Description") .set_id("description") .set_type(dpp::cot_text) .set_placeholder("Detailed description of your request") .set_min_length(20) .set_max_length(2000) .set_text_style(dpp::text_paragraph) .set_required(false)); event.dialog(modal); } else if (event.custom_id == "ticket_take") { // Get guild from database db_add_guild(event.command.guild_id); std::string management_role_id; bot->db << "SELECT management_role FROM ticket_guild_settings " "WHERE id = ?;" << std::to_string(event.command.guild_id) >> std::tie(management_role_id); // Check permissions if (!can_change_ticket(event, management_role_id)) { event.reply(dpp::message("You may not take care of this ticket!").set_flags(dpp::message_flags::m_ephemeral)); return; } // Get ticket info from database std::string author_id; try { bot->db << "SELECT author_id FROM tickets " "WHERE channel_id = ?;" << std::to_string(event.command.channel_id) >> std::tie(author_id); } catch (...) { event.reply(dpp::message("**Error:** Is this actually a ticket channel?").set_flags(dpp::message_flags::m_ephemeral)); return; } // Send message event.reply(dpp::message("Done!").set_flags(dpp::message_flags::m_ephemeral)); bot->cluster.message_create(dpp::message().set_channel_id(event.command.channel_id).set_content("This ticket is not taken care of by "+event.command.usr.get_mention()+"!")); // Update channel topic bot->cluster.channel_get(event.command.channel_id, [this, author_id, event](const dpp::confirmation_callback_t& ccb) { if (!ccb.is_error()) { auto channel = ccb.get(); channel.set_topic(build_channel_topic(author_id, event.command.usr.id)); bot->cluster.channel_edit(channel); } }); } else if (event.custom_id == "ticket_close") { close_ticket(event); } }); bot->cluster.on_form_submit([&](const dpp::form_submit_t& event) { if (event.custom_id == "ticket_modal") { try { // Get title and text auto title = std::get(event.components.at(0).components.at(0).value), description = std::get(event.components.at(1).components.at(0).value); // Get server settings std::string category_id, channel_name, management_role; unsigned ping; db_add_guild(event.command.guild_id); bot->db << "SELECT category, channel_name, management_role, ping FROM ticket_guild_settings " "WHERE id = ?;" << std::to_string(event.command.guild_id) >> std::tie(category_id, channel_name, management_role, ping); // Remove management ping if management role is unknown if (management_role.empty()) { ping &= ~PingMode::managers; } // Create channel dpp::channel new_channel; new_channel.set_guild_id(event.command.guild_id) .set_name(channel_name+event.command.usr.username) .set_topic(build_channel_topic(event.command.usr.id)) .add_permission_overwrite(bot->cluster.me.id, dpp::overwrite_type::ot_member, dpp::permissions::p_view_channel, 0) // Bot can see channel .add_permission_overwrite(event.command.usr.id, dpp::overwrite_type::ot_member, dpp::permissions::p_view_channel, 0) // Ticket creator can see channel .add_permission_overwrite(event.command.guild_id, dpp::overwrite_type::ot_role, 0, dpp::permissions::p_view_channel); // Everyone else can't see channel if (!management_role.empty()) { new_channel.add_permission_overwrite(management_role, dpp::overwrite_type::ot_role, dpp::permissions::p_view_channel, 0); // Ticket managers can see channel } if (!category_id.empty()) { new_channel.set_parent_id(category_id); } bot->cluster.channel_create(new_channel, [this, event, title, description, management_role, ping](const dpp::confirmation_callback_t& ccb) { // Check for error if (ccb.is_error()) { event.reply("**ERROR:** Ticket settings are invalid.\n**To server administration:** Please check if the target category still exists and set a new one if necessary!```json"+ccb.get_error().message+"```"); return; } // Get created channel auto channel = ccb.get(); // Create database entry for channel bot->db << "INSERT OR IGNORE INTO tickets (guild_id, channel_id, author_id, author_full_name, title, description) VALUES (?, ?, ?, ?, ?, ?);" << std::to_string(channel.guild_id) << std::to_string(channel.id) << std::to_string(event.command.usr.id) << event.command.usr.format_username() << title << description; // Create top-most message dpp::message msg; msg.set_channel_id(channel.id) .set_allowed_mentions(true, true, false, {}, {}, {}) .set_content((ping&PingMode::creator?event.command.usr.get_mention():"")+(ping&PingMode::managers?("<@&"+management_role+'>'):"")) .add_embed(dpp::embed() .set_title(title) .set_description(description) .set_footer("Ticket #"+Util::int_as_hex(channel.id), "")) .add_component(dpp::component() .add_component(dpp::component() .set_label("Take care of ticket") .set_type(dpp::component_type::cot_button) .set_emoji("✅") .set_style(dpp::component_style::cos_primary) .set_id("ticket_take")) .add_component(dpp::component() .set_label("Close ticket") .set_type(dpp::component_type::cot_button) .set_emoji("🗑️") .set_style(dpp::component_style::cos_danger) .set_id("ticket_close"))); bot->cluster.message_create(msg, [this](const dpp::confirmation_callback_t& ccb) { if (!ccb.is_error()) { const auto& message = ccb.get(); bot->cluster.message_pin(message.channel_id, message.id); } }); // Report success event.reply(dpp::message("Your ticket has been created in "+channel.get_mention()+"!").set_flags(dpp::message_flags::m_ephemeral)); }); } catch (std::exception& e) { event.reply(std::string("An internal error has occured: `")+e.what()+'`'); } } }); bot->cluster.on_channel_delete([this](const dpp::channel_delete_t& event) { // Delete channel from tickets database bot->db << "DELETE FROM tickets " "WHERE channel_id = ?;" << std::to_string(event.deleted->id); }); bot->cluster.on_guild_delete([this](const dpp::guild_delete_t& event) { // Delete guild from tickets database bot->db << "DELETE FROM tickets " "WHERE guild_id = ?;" << std::to_string(event.deleted->id); }); } }; BOT_ADD_MODULE(Ticket);