mirror of
https://gitlab.com/niansa/SomeBot.git
synced 2025-03-06 20:48:26 +01:00
483 lines
27 KiB
C++
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);
|