From be4a45d6ae9a7ce89ffcc1dadd71922284c3ab3a Mon Sep 17 00:00:00 2001 From: unko Date: Mon, 14 Sep 2026 08:24:55 -0300 Subject: [PATCH 01/10] fix: prevent duplicate open tickets --- src/commands/ticket_cmd.cpp | 53 ++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/commands/ticket_cmd.cpp b/src/commands/ticket_cmd.cpp index 7546cf1..8cfe492 100644 --- a/src/commands/ticket_cmd.cpp +++ b/src/commands/ticket_cmd.cpp @@ -1,8 +1,20 @@ #include "commands.h" #include "../globals/globals.h" +#include "../ticket_registry.h" void cmd::ticketCommand(dpp::cluster& bot, const dpp::slashcommand_t& event) { + const auto guildId = event.command.guild_id; + const auto userId = event.command.get_issuing_user().id; + + if (const auto existingTicket = tickets::registry.find(guildId, userId)) + { + event.reply(dpp::message( + "You already have an open ticket: <#" + existingTicket->str() + ">") + .set_flags(dpp::m_ephemeral)); + return; + } + dpp::message message(event.command.channel_id, "Creating ticket..."); event.reply(message.set_flags(dpp::m_ephemeral)); @@ -14,10 +26,10 @@ void cmd::ticketCommand(dpp::cluster& bot, const dpp::slashcommand_t& event) dpp::channel ticketChannel = dpp::channel() .set_name(event.command.get_issuing_user().username) .set_type(dpp::CHANNEL_TEXT) - .set_guild_id(event.command.guild_id) + .set_guild_id(guildId) .set_parent_id(globals::category::ticketId) - .set_permission_overwrite(event.command.guild_id, dpp::overwrite_type::ot_role, 0, dpp::p_view_channel) - .set_permission_overwrite(event.command.get_issuing_user().id, dpp::overwrite_type::ot_member, ticketPerms, 0) + .set_permission_overwrite(guildId, dpp::overwrite_type::ot_role, 0, dpp::p_view_channel) + .set_permission_overwrite(userId, dpp::overwrite_type::ot_member, ticketPerms, 0) .set_permission_overwrite(globals::role::staffId, dpp::overwrite_type::ot_role, ticketPerms, 0); const dpp::command_interaction cmdData = event.command.get_command_interaction(); @@ -26,22 +38,37 @@ void cmd::ticketCommand(dpp::cluster& bot, const dpp::slashcommand_t& event) if (const auto option = cmdData.options[0]; option.type == dpp::co_user) { const auto participantId = std::get(option.value); - ticketChannel.set_permission_overwrite(participantId, dpp::overwrite_type::ot_member,ticketPerms, 0); + ticketChannel.set_permission_overwrite( + participantId, + dpp::overwrite_type::ot_member, + ticketPerms, + 0); } } - bot.channel_create(ticketChannel, [&bot, event](const dpp::confirmation_callback_t& callback) { - if (!callback.is_error()) + bot.channel_create(ticketChannel, [&bot, event, guildId, userId]( + const dpp::confirmation_callback_t& callback) { + if (callback.is_error()) { - const auto ticketChannel = std::get(callback.value); - const auto pingMessage = dpp::message(ticketChannel.id, event.command.get_issuing_user().get_mention() + " opened this ticket."); - bot.message_create(pingMessage); - - event.edit_response("Ticket " + ticketChannel.get_mention() + " created!"); + event.edit_response("Failed to create ticket channel."); + return; } - else + + const auto createdChannel = std::get(callback.value); + + if (!tickets::registry.add(guildId, userId, createdChannel.id)) { - event.edit_response("Failed to create ticket channel!"); + bot.channel_delete(createdChannel.id); + event.edit_response("You already have an open ticket."); + return; } + + const auto pingMessage = dpp::message( + createdChannel.id, + event.command.get_issuing_user().get_mention() + " opened this ticket."); + + bot.message_create(pingMessage); + + event.edit_response("Ticket " + createdChannel.get_mention() + " created!"); }); } From 2bbecd6898ea76023b4c209870b14785241f7acd Mon Sep 17 00:00:00 2001 From: unko Date: Mon, 14 Sep 2026 08:26:20 -0300 Subject: [PATCH 02/10] fix: clear ticket registry when closing tickets --- src/commands/close_cmd.cpp | 64 +++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/src/commands/close_cmd.cpp b/src/commands/close_cmd.cpp index 4dcf845..5b814b0 100644 --- a/src/commands/close_cmd.cpp +++ b/src/commands/close_cmd.cpp @@ -1,5 +1,6 @@ -#include "commands.h" +#include "commands.h" #include "../globals/globals.h" +#include "../ticket_registry.h" #include #include @@ -10,23 +11,32 @@ void cmd::closeCommand(dpp::cluster& bot, const dpp::slashcommand_t& event) { if (event.command.channel.get_type() == dpp::channel_type::CHANNEL_PUBLIC_THREAD) { - bot.thread_get(event.command.channel_id, [&bot, event](const dpp::confirmation_callback_t& callback) { + bot.thread_get(event.command.channel_id, [&bot, event]( + const dpp::confirmation_callback_t& callback) { if (callback.is_error()) - return event.reply(dpp::message("[!] Callback error").set_flags(dpp::m_ephemeral)); + return event.reply( + dpp::message("[!] Callback error").set_flags(dpp::m_ephemeral)); auto thread = callback.get(); if (event.command.channel.owner_id != event.command.member.user_id) - return event.reply(dpp::message("You can only close your own posts.").set_flags(dpp::m_ephemeral)); + return event.reply( + dpp::message("You can only close your own posts.") + .set_flags(dpp::m_ephemeral)); thread.metadata.locked = true; - const std::string newThreadName = dpp::unicode_emoji::lock + std::string(" ") + thread.name; + const std::string newThreadName = + dpp::unicode_emoji::lock + std::string(" ") + thread.name; + thread.set_name(newThreadName); - bot.thread_edit(thread, [event](const dpp::confirmation_callback_t& callback2) { + bot.thread_edit(thread, [event]( + const dpp::confirmation_callback_t& callback2) { if (callback2.is_error()) - return event.reply(dpp::message("[!] Unable to close post.").set_flags(dpp::m_ephemeral)); + return event.reply( + dpp::message("[!] Unable to close post.") + .set_flags(dpp::m_ephemeral)); const dpp::embed embed = dpp::embed() .set_color(globals::color::defaultColor) @@ -39,26 +49,42 @@ void cmd::closeCommand(dpp::cluster& bot, const dpp::slashcommand_t& event) } else if (event.command.channel.parent_id == globals::category::ticketId) { - event.reply(dpp::message("Closed ticket!")); - - bot.channel_get(event.command.channel.id, [&bot, event](const dpp::confirmation_callback_t& callback) { - if (!callback.is_error()) + bot.channel_get(event.command.channel.id, [&bot, event]( + const dpp::confirmation_callback_t& callback) { + if (callback.is_error()) { - dpp::channel ticketChannel = std::get(callback.value); - std::vector overwrites = ticketChannel.permission_overwrites; + event.reply( + dpp::message("Unable to find this ticket.") + .set_flags(dpp::m_ephemeral)); + return; + } - for (const auto& overwrite : overwrites) + const auto ticketChannel = std::get(callback.value); + + for (const auto& overwrite : ticketChannel.permission_overwrites) + { + if (overwrite.type == dpp::overwrite_type::ot_member) { - if (overwrite.type == dpp::overwrite_type::ot_member) - { - bot.channel_edit_permissions(ticketChannel, overwrite.id, 0, dpp::p_view_channel, true); - } + bot.channel_edit_permissions( + ticketChannel, + overwrite.id, + 0, + dpp::p_view_channel, + true); } } + + tickets::registry.removeByChannel( + event.command.guild_id, + ticketChannel.id); + + event.reply(dpp::message("Closed ticket!")); }); } else { - event.reply(dpp::message("This command only operates within tickets and threads.").set_flags(dpp::m_ephemeral)); + event.reply( + dpp::message("This command only operates within tickets and threads.") + .set_flags(dpp::m_ephemeral)); } } From c83e2fcd2601d9ae7df856611bca866fae4f203b Mon Sep 17 00:00:00 2001 From: unko Date: Mon, 14 Sep 2026 08:27:46 -0300 Subject: [PATCH 03/10] Add ticket registry class for managing tickets --- src/ticket_registry.h | 69 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/ticket_registry.h diff --git a/src/ticket_registry.h b/src/ticket_registry.h new file mode 100644 index 0000000..b25a0a3 --- /dev/null +++ b/src/ticket_registry.h @@ -0,0 +1,69 @@ +#ifndef TICKET_REGISTRY_H +#define TICKET_REGISTRY_H + +#include + +#include +#include +#include +#include +#include + +namespace tickets +{ + class Registry + { + public: + std::optional find( + dpp::snowflake guildId, + dpp::snowflake userId) const; + + bool add( + dpp::snowflake guildId, + dpp::snowflake userId, + dpp::snowflake channelId); + + void removeByChannel( + dpp::snowflake guildId, + dpp::snowflake channelId); + + private: + struct Key + { + dpp::snowflake guildId{}; + dpp::snowflake userId{}; + + bool operator==(const Key& other) const + { + return guildId == other.guildId + && userId == other.userId; + } + }; + + struct KeyHash + { + std::size_t operator()(const Key& key) const + { + const auto guildHash = + std::hash{}( + static_cast(key.guildId)); + + const auto userHash = + std::hash{}( + static_cast(key.userId)); + + return guildHash + ^ (userHash + 0x9e3779b9 + + (guildHash << 6) + + (guildHash >> 2)); + } + }; + + mutable std::mutex mutex; + std::unordered_map tickets; + }; + + extern Registry registry; +} + +#endif // TICKET_REGISTRY_H From 87ce507200f8dbe7adf9ff095a2f21c02499c59a Mon Sep 17 00:00:00 2001 From: unko Date: Mon, 14 Sep 2026 08:28:24 -0300 Subject: [PATCH 04/10] Add ticket registry functionality Implement ticket registry with find, add, and removeByChannel methods. --- src/ticket_registry.cpp | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/ticket_registry.cpp diff --git a/src/ticket_registry.cpp b/src/ticket_registry.cpp new file mode 100644 index 0000000..7838011 --- /dev/null +++ b/src/ticket_registry.cpp @@ -0,0 +1,47 @@ +#include "ticket_registry.h" + +#include + +namespace tickets +{ + std::optional Registry::find( + const dpp::snowflake guildId, + const dpp::snowflake userId) const + { + std::scoped_lock lock(mutex); + + const auto ticket = tickets.find({guildId, userId}); + + if (ticket == tickets.end()) + return std::nullopt; + + return ticket->second; + } + + bool Registry::add( + const dpp::snowflake guildId, + const dpp::snowflake userId, + const dpp::snowflake channelId) + { + std::scoped_lock lock(mutex); + + return tickets.emplace( + Key{guildId, userId}, + channelId).second; + } + + void Registry::removeByChannel( + const dpp::snowflake guildId, + const dpp::snowflake channelId) + { + std::scoped_lock lock(mutex); + + std::erase_if(tickets, [guildId, channelId](const auto& ticket) { + return ticket.first.guildId == guildId + && ticket.second == channelId; + }); + } + + Registry registry; +} + From 0f5ac4206aa71b3a076fe6657eab3c53aa597416 Mon Sep 17 00:00:00 2001 From: unko Date: Mon, 14 Sep 2026 08:29:13 -0300 Subject: [PATCH 05/10] fix: implement ticket registry From d2f861294cb428284fca998d3f44b53cea878863 Mon Sep 17 00:00:00 2001 From: unko Date: Mon, 14 Sep 2026 09:24:24 -0300 Subject: [PATCH 06/10] fix: identify suggestion authors by user id --- src/utils/suggestion/suggestion.cpp | 39 ++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/utils/suggestion/suggestion.cpp b/src/utils/suggestion/suggestion.cpp index e85514a..0515fe6 100644 --- a/src/utils/suggestion/suggestion.cpp +++ b/src/utils/suggestion/suggestion.cpp @@ -1,6 +1,34 @@ #include "suggestion.h" #include "../../globals/globals.h" +#include +#include +#include + +namespace +{ + constexpr std::string_view authorFooterPrefix = "author_id:"; + + std::optional getSuggestionAuthorId(const dpp::message& message) + { + if (message.embeds.empty() || !message.embeds.front().footer) + return std::nullopt; + + const auto& footer = message.embeds.front().footer; + if (!footer->text.starts_with(authorFooterPrefix)) + return std::nullopt; + + try + { + return dpp::snowflake(footer->text.substr(authorFooterPrefix.size())); + } + catch (const std::exception&) + { + return std::nullopt; + } + } +} + void utils::suggestion::createSuggestion(dpp::cluster& bot, const dpp::message_create_t& event) { dpp::user user = event.msg.author; @@ -27,6 +55,7 @@ void utils::suggestion::createSuggestion(dpp::cluster& bot, const dpp::message_c .set_color(globals::color::defaultColor) .set_title("Suggestion") .set_author(user.format_username(), "", user.get_avatar_url()) + .set_footer("author_id:" + user.id.str()) .set_description(event.msg.content); dpp::message msg(event.msg.channel_id, result); @@ -86,10 +115,9 @@ void utils::suggestion::createSuggestion(dpp::cluster& bot, const dpp::message_c void utils::suggestion::deleteSuggestion(dpp::cluster& bot, const dpp::button_click_t& event) { - std::string clicker = event.command.get_issuing_user().format_username(); - std::string originalAuthor = event.command.msg.embeds[0].author->name; + const auto originalAuthorId = getSuggestionAuthorId(event.command.msg); - if (clicker == originalAuthor) + if (originalAuthorId && event.command.get_issuing_user().id == *originalAuthorId) bot.message_delete(event.command.msg.id, event.command.msg.channel_id); else event.reply(dpp::message("You can only delete your own suggestions.").set_flags(dpp::m_ephemeral)); @@ -97,10 +125,9 @@ void utils::suggestion::deleteSuggestion(dpp::cluster& bot, const dpp::button_cl void utils::suggestion::editSuggestion(dpp::cluster& bot, const dpp::button_click_t& event) { - std::string clicker = event.command.get_issuing_user().format_username(); - std::string originalAuthor = event.command.msg.embeds[0].author->name; + const auto originalAuthorId = getSuggestionAuthorId(event.command.msg); - if (clicker == originalAuthor) + if (originalAuthorId && event.command.get_issuing_user().id == *originalAuthorId) { dpp::interaction_modal_response modal("editModal", "Edit suggestion"); From 3bde6301b9e8fd1afb79acf3fbdf560879b5ba26 Mon Sep 17 00:00:00 2001 From: unko Date: Mon, 14 Sep 2026 09:45:07 -0300 Subject: [PATCH 07/10] add close ticket button --- src/commands/ticket_cmd.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/commands/ticket_cmd.cpp b/src/commands/ticket_cmd.cpp index 8cfe492..1f1f27d 100644 --- a/src/commands/ticket_cmd.cpp +++ b/src/commands/ticket_cmd.cpp @@ -38,16 +38,11 @@ void cmd::ticketCommand(dpp::cluster& bot, const dpp::slashcommand_t& event) if (const auto option = cmdData.options[0]; option.type == dpp::co_user) { const auto participantId = std::get(option.value); - ticketChannel.set_permission_overwrite( - participantId, - dpp::overwrite_type::ot_member, - ticketPerms, - 0); + ticketChannel.set_permission_overwrite(participantId, dpp::overwrite_type::ot_member,ticketPerms, 0); } } - bot.channel_create(ticketChannel, [&bot, event, guildId, userId]( - const dpp::confirmation_callback_t& callback) { + bot.channel_create(ticketChannel, [&bot, event, guildId, userId](const dpp::confirmation_callback_t& callback) { if (callback.is_error()) { event.edit_response("Failed to create ticket channel."); @@ -55,7 +50,6 @@ void cmd::ticketCommand(dpp::cluster& bot, const dpp::slashcommand_t& event) } const auto createdChannel = std::get(callback.value); - if (!tickets::registry.add(guildId, userId, createdChannel.id)) { bot.channel_delete(createdChannel.id); @@ -67,7 +61,16 @@ void cmd::ticketCommand(dpp::cluster& bot, const dpp::slashcommand_t& event) createdChannel.id, event.command.get_issuing_user().get_mention() + " opened this ticket."); - bot.message_create(pingMessage); + dpp::message ticketMessage = pingMessage; + ticketMessage.add_component( + dpp::component().add_component( + dpp::component() + .set_label("Close ticket") + .set_type(dpp::cot_button) + .set_style(dpp::cos_danger) + .set_id("closeTicket"))); + + bot.message_create(ticketMessage); event.edit_response("Ticket " + createdChannel.get_mention() + " created!"); }); From 3b159bdf3c11349815b63813b0e08c34f6cc8ab9 Mon Sep 17 00:00:00 2001 From: unko Date: Mon, 14 Sep 2026 09:46:16 -0300 Subject: [PATCH 08/10] handle close ticket button --- src/commands/close_cmd.cpp | 74 +++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/commands/close_cmd.cpp b/src/commands/close_cmd.cpp index 5b814b0..5b07074 100644 --- a/src/commands/close_cmd.cpp +++ b/src/commands/close_cmd.cpp @@ -11,32 +11,23 @@ void cmd::closeCommand(dpp::cluster& bot, const dpp::slashcommand_t& event) { if (event.command.channel.get_type() == dpp::channel_type::CHANNEL_PUBLIC_THREAD) { - bot.thread_get(event.command.channel_id, [&bot, event]( - const dpp::confirmation_callback_t& callback) { + bot.thread_get(event.command.channel_id, [&bot, event](const dpp::confirmation_callback_t& callback) { if (callback.is_error()) - return event.reply( - dpp::message("[!] Callback error").set_flags(dpp::m_ephemeral)); + return event.reply(dpp::message("[!] Callback error").set_flags(dpp::m_ephemeral)); auto thread = callback.get(); if (event.command.channel.owner_id != event.command.member.user_id) - return event.reply( - dpp::message("You can only close your own posts.") - .set_flags(dpp::m_ephemeral)); + return event.reply(dpp::message("You can only close your own posts.").set_flags(dpp::m_ephemeral)); thread.metadata.locked = true; - const std::string newThreadName = - dpp::unicode_emoji::lock + std::string(" ") + thread.name; - + const std::string newThreadName = dpp::unicode_emoji::lock + std::string(" ") + thread.name; thread.set_name(newThreadName); - bot.thread_edit(thread, [event]( - const dpp::confirmation_callback_t& callback2) { + bot.thread_edit(thread, [event](const dpp::confirmation_callback_t& callback2) { if (callback2.is_error()) - return event.reply( - dpp::message("[!] Unable to close post.") - .set_flags(dpp::m_ephemeral)); + return event.reply(dpp::message("[!] Unable to close post.").set_flags(dpp::m_ephemeral)); const dpp::embed embed = dpp::embed() .set_color(globals::color::defaultColor) @@ -49,42 +40,53 @@ void cmd::closeCommand(dpp::cluster& bot, const dpp::slashcommand_t& event) } else if (event.command.channel.parent_id == globals::category::ticketId) { - bot.channel_get(event.command.channel.id, [&bot, event]( - const dpp::confirmation_callback_t& callback) { + bot.channel_get(event.command.channel.id, [&bot, event](const dpp::confirmation_callback_t& callback) { if (callback.is_error()) { - event.reply( - dpp::message("Unable to find this ticket.") - .set_flags(dpp::m_ephemeral)); + event.reply(dpp::message("Unable to find this ticket.").set_flags(dpp::m_ephemeral)); return; } const auto ticketChannel = std::get(callback.value); - for (const auto& overwrite : ticketChannel.permission_overwrites) { if (overwrite.type == dpp::overwrite_type::ot_member) - { - bot.channel_edit_permissions( - ticketChannel, - overwrite.id, - 0, - dpp::p_view_channel, - true); - } + bot.channel_edit_permissions(ticketChannel, overwrite.id, 0, dpp::p_view_channel, true); } - tickets::registry.removeByChannel( - event.command.guild_id, - ticketChannel.id); - + tickets::registry.removeByChannel(event.command.guild_id, ticketChannel.id); event.reply(dpp::message("Closed ticket!")); }); } else { - event.reply( - dpp::message("This command only operates within tickets and threads.") - .set_flags(dpp::m_ephemeral)); + event.reply(dpp::message("This command only operates within tickets and threads.").set_flags(dpp::m_ephemeral)); } } + +void cmd::closeTicketButton(dpp::cluster& bot, const dpp::button_click_t& event) +{ + if (event.command.channel.parent_id != globals::category::ticketId) + { + event.reply(dpp::message("This button only works inside a ticket.").set_flags(dpp::m_ephemeral)); + return; + } + + bot.channel_get(event.command.channel.id, [&bot, event](const dpp::confirmation_callback_t& callback) { + if (callback.is_error()) + { + event.reply(dpp::message("Unable to find this ticket.").set_flags(dpp::m_ephemeral)); + return; + } + + const auto ticketChannel = std::get(callback.value); + for (const auto& overwrite : ticketChannel.permission_overwrites) + { + if (overwrite.type == dpp::overwrite_type::ot_member) + bot.channel_edit_permissions(ticketChannel, overwrite.id, 0, dpp::p_view_channel, true); + } + + tickets::registry.removeByChannel(event.command.guild_id, ticketChannel.id); + event.reply(dpp::message("Closed ticket!")); + }); +} From d57321576f72cf7fd8945fdd58658ab4faf3051a Mon Sep 17 00:00:00 2001 From: unko Date: Mon, 14 Sep 2026 09:48:27 -0300 Subject: [PATCH 09/10] declare close ticket button handler --- src/commands/commands.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/commands/commands.h b/src/commands/commands.h index edfc965..0690430 100644 --- a/src/commands/commands.h +++ b/src/commands/commands.h @@ -29,6 +29,13 @@ namespace cmd */ void closeCommand(dpp::cluster& bot, const dpp::slashcommand_t& event); + /** + * @brief Closes a ticket from its message button + * @param bot cluster + * @param event button click event + */ + void closeTicketButton(dpp::cluster& bot, const dpp::button_click_t& event); + /** * @brief Opens a ticket * @param bot cluster From 32393b2f0c2685653fb4ac02229d7c98657ecfbd Mon Sep 17 00:00:00 2001 From: unko Date: Mon, 14 Sep 2026 09:49:41 -0300 Subject: [PATCH 10/10] route close ticket button events --- src/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index c1bacad..af8f9aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -84,7 +84,9 @@ int main() }); bot.on_button_click([&bot](const dpp::button_click_t& event) { - if (event.custom_id == "delSuggestion") + if (event.custom_id == "closeTicket") + cmd::closeTicketButton(bot, event); + else if (event.custom_id == "delSuggestion") utils::suggestion::deleteSuggestion(bot, event); else if (event.custom_id == "editSuggestion") utils::suggestion::editSuggestion(bot, event);