1
0
Fork 0
mirror of https://gitlab.com/niansa/SomeBot.git synced 2025-03-06 20:48:26 +01:00
SomeBot/modules/ticket.cpp
2023-12-07 01:50:33 +01:00

483 lines
27 KiB
C++

#include "../bot.hpp"
#include "../util.hpp"
#include <sstream>
#include <thread>
#include <exception>
#include <algorithm>
namespace PingMode {
constexpr uint8_t all = 0b11,
creator = 0b10,
managers = 0b01,
none = 0b00;
}
class Ticket {
Bot *bot;
std::unordered_map<std::string, std::string> 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<dpp::snowflake, dpp::message> 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<dpp::snowflake>(category_opt), [this, event](const dpp::confirmation_callback_t& ccb) {
if (!ccb.is_error()) {
auto channel = ccb.get<dpp::channel>();
// 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<dpp::snowflake>(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<dpp::snowflake>(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<dpp::snowflake>(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<std::string>(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<bool>(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<bool>(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<dpp::snowflake>(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<std::string>(title_opt):"Support tickets",
text = text_opt.index()!=0?std::get<std::string>(text_opt):"Click the button below to create a ticket!",
footer_text = footer_text_opt.index()!=0?std::get<std::string>(footer_text_opt):"";
unsigned color = Util::str_to_color(color_opt.index()!=0?std::get<std::string>(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<dpp::channel>();
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<std::string>(event.components.at(0).components.at(0).value),
description = std::get<std::string>(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<dpp::channel>();
// 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<dpp::message>();
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);