// Discord bot library - provides all Discord API functionality #include "../DPP/include/dpp/dpp.h" // Standard input/output - for console logging (std::cout, std::cerr) #include // String handling - for std::string class and std::to_string() function #include // Terminal I/O control - for hiding password input in getHiddenInput() #include // Unix standard definitions - provides STDIN_FILENO constant #include // Time functions - for std::time() to generate timestamps #include // Dynamic arrays - for std::vector to hold image byte data #include // Threading support - for worker thread and synchronization #include #include #include #include // Time utilities - for std::chrono::seconds() delays #include // Algorithm functions - for std::transform (string case conversion) #include // SQLite3 database - for persistent work queue storage #include // STB Image library - single header for loading images (JPEG, PNG, etc.) #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" // STB Image Write library - single header for saving images #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" // Embedded image data - contains the JPEG as a byte array (our generated header) #include "embedded_image.h" /** * Structure representing a work item in the processing queue */ struct WorkItem { int64_t id; // Unique database ID std::string attachment_url; // Discord attachment URL to download std::string original_filename; // Original filename from Discord uint64_t channel_id; // Discord channel ID for response uint64_t user_id; // Discord user ID for mentions int64_t created_at; // Unix timestamp when job was created int retry_count; // Number of times this job has been retried WorkItem() : id(0), channel_id(0), user_id(0), created_at(0), retry_count(0) {} }; /** * SQLite-backed FIFO work queue for managing JPEG processing jobs * Provides persistence across system crashes and sequential processing */ class WorkQueue { private: sqlite3* db; std::mutex queue_mutex; std::condition_variable queue_condition; std::atomic shutdown_requested{false}; public: WorkQueue() : db(nullptr) {} ~WorkQueue() { if (db) { sqlite3_close(db); } } /** * Initialize SQLite database and create work queue table */ bool initialize(const std::string& db_path = "work_queue.db") { std::lock_guard lock(queue_mutex); // Open SQLite database (creates file if it doesn't exist) int rc = sqlite3_open(db_path.c_str(), &db); if (rc != SQLITE_OK) { std::cerr << "āŒ Failed to open SQLite database: " << sqlite3_errmsg(db) << std::endl; return false; } // Create work queue table if it doesn't exist const char* create_table_sql = R"( CREATE TABLE IF NOT EXISTS work_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, attachment_url TEXT NOT NULL, original_filename TEXT NOT NULL, channel_id INTEGER NOT NULL, user_id INTEGER NOT NULL, created_at INTEGER NOT NULL, retry_count INTEGER DEFAULT 0, status TEXT DEFAULT 'pending' ); CREATE INDEX IF NOT EXISTS idx_status_created ON work_queue(status, created_at); )"; char* error_msg = nullptr; rc = sqlite3_exec(db, create_table_sql, nullptr, nullptr, &error_msg); if (rc != SQLITE_OK) { std::cerr << "āŒ Failed to create work queue table: " << error_msg << std::endl; sqlite3_free(error_msg); return false; } std::cout << "āœ… Work queue database initialized: " << db_path << std::endl; // Log any existing pending jobs on startup int pending_count = getPendingJobCount(); if (pending_count > 0) { std::cout << "šŸ“‹ Found " << pending_count << " pending jobs from previous session" << std::endl; } return true; } /** * Add a new work item to the queue */ bool enqueue(const WorkItem& item) { std::lock_guard lock(queue_mutex); const char* insert_sql = R"( INSERT INTO work_queue (attachment_url, original_filename, channel_id, user_id, created_at, retry_count) VALUES (?, ?, ?, ?, ?, ?); )"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { std::cerr << "āŒ Failed to prepare insert statement: " << sqlite3_errmsg(db) << std::endl; return false; } // Bind parameters sqlite3_bind_text(stmt, 1, item.attachment_url.c_str(), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, item.original_filename.c_str(), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, item.channel_id); sqlite3_bind_int64(stmt, 4, item.user_id); sqlite3_bind_int64(stmt, 5, item.created_at); sqlite3_bind_int(stmt, 6, item.retry_count); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { std::cerr << "āŒ Failed to insert work item: " << sqlite3_errmsg(db) << std::endl; return false; } std::cout << "šŸ“„ Enqueued job: " << item.original_filename << " (ID: " << sqlite3_last_insert_rowid(db) << ")" << std::endl; // Notify worker thread that new work is available queue_condition.notify_one(); return true; } /** * Get the next work item from the queue (FIFO order) * Blocks until work is available or shutdown is requested */ bool dequeue(WorkItem& item) { std::unique_lock lock(queue_mutex); while (!shutdown_requested) { const char* select_sql = R"( SELECT id, attachment_url, original_filename, channel_id, user_id, created_at, retry_count FROM work_queue WHERE status = 'pending' ORDER BY created_at ASC LIMIT 1; )"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(db, select_sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { std::cerr << "āŒ Failed to prepare select statement: " << sqlite3_errmsg(db) << std::endl; return false; } rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { // Found a work item item.id = sqlite3_column_int64(stmt, 0); item.attachment_url = (const char*)sqlite3_column_text(stmt, 1); item.original_filename = (const char*)sqlite3_column_text(stmt, 2); item.channel_id = sqlite3_column_int64(stmt, 3); item.user_id = sqlite3_column_int64(stmt, 4); item.created_at = sqlite3_column_int64(stmt, 5); item.retry_count = sqlite3_column_int(stmt, 6); sqlite3_finalize(stmt); // Mark as processing markProcessing(item.id); std::cout << "šŸ“¤ Dequeued job " << item.id << ": " << item.original_filename << std::endl; return true; } else if (rc == SQLITE_DONE) { // No work available, wait for notification sqlite3_finalize(stmt); queue_condition.wait(lock); } else { // Error occurred std::cerr << "āŒ Failed to select work item: " << sqlite3_errmsg(db) << std::endl; sqlite3_finalize(stmt); return false; } } return false; // Shutdown requested } /** * Mark a work item as completed and remove it from the queue */ bool markCompleted(int64_t item_id) { std::lock_guard lock(queue_mutex); const char* delete_sql = "DELETE FROM work_queue WHERE id = ?;"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(db, delete_sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { std::cerr << "āŒ Failed to prepare delete statement: " << sqlite3_errmsg(db) << std::endl; return false; } sqlite3_bind_int64(stmt, 1, item_id); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { std::cerr << "āŒ Failed to delete completed work item: " << sqlite3_errmsg(db) << std::endl; return false; } std::cout << "āœ… Completed job " << item_id << std::endl; return true; } /** * Mark a work item as failed and update retry count */ bool markFailed(int64_t item_id, int max_retries = 3) { std::lock_guard lock(queue_mutex); // First, get current retry count const char* select_sql = "SELECT retry_count FROM work_queue WHERE id = ?;"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(db, select_sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { std::cerr << "āŒ Failed to prepare retry count select: " << sqlite3_errmsg(db) << std::endl; return false; } sqlite3_bind_int64(stmt, 1, item_id); rc = sqlite3_step(stmt); if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); std::cerr << "āŒ Work item " << item_id << " not found for retry update" << std::endl; return false; } int current_retries = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); if (current_retries >= max_retries) { // Too many retries, remove from queue std::cout << "šŸ’€ Job " << item_id << " failed permanently after " << current_retries << " retries" << std::endl; const char* delete_sql = "DELETE FROM work_queue WHERE id = ?;"; sqlite3_prepare_v2(db, delete_sql, -1, &stmt, nullptr); sqlite3_bind_int64(stmt, 1, item_id); sqlite3_step(stmt); sqlite3_finalize(stmt); } else { // Increment retry count and mark as pending for retry std::cout << "šŸ”„ Job " << item_id << " failed, retry " << (current_retries + 1) << "/" << max_retries << std::endl; const char* update_sql = "UPDATE work_queue SET retry_count = ?, status = 'pending' WHERE id = ?;"; sqlite3_prepare_v2(db, update_sql, -1, &stmt, nullptr); sqlite3_bind_int(stmt, 1, current_retries + 1); sqlite3_bind_int64(stmt, 2, item_id); sqlite3_step(stmt); sqlite3_finalize(stmt); // Notify worker thread to try again queue_condition.notify_one(); } return true; } /** * Request shutdown of the work queue */ void requestShutdown() { shutdown_requested = true; queue_condition.notify_all(); } private: /** * Mark a work item as being processed */ bool markProcessing(int64_t item_id) { const char* update_sql = "UPDATE work_queue SET status = 'processing' WHERE id = ?;"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { return false; } sqlite3_bind_int64(stmt, 1, item_id); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return rc == SQLITE_DONE; } /** * Get count of pending jobs in the queue */ int getPendingJobCount() { const char* count_sql = "SELECT COUNT(*) FROM work_queue WHERE status = 'pending';"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(db, count_sql, -1, &stmt, nullptr); if (rc != SQLITE_OK) { return 0; } rc = sqlite3_step(stmt); int count = (rc == SQLITE_ROW) ? sqlite3_column_int(stmt, 0) : 0; sqlite3_finalize(stmt); return count; } }; /** * Function to securely input text without displaying it on screen (like password input) * This is used for Discord bot tokens to keep them private in the terminal * * @param prompt - The text to display before asking for input * @return std::string - The hidden text that was typed */ std::string getHiddenInput(const std::string& prompt) { // Display the prompt to the user std::cout << prompt; std::cout.flush(); // Force immediate output (don't wait for newline) // Save current terminal settings so we can restore them later termios oldt; tcgetattr(STDIN_FILENO, &oldt); // Get current terminal attributes // Create new terminal settings that disable echo (hide typed characters) termios newt = oldt; // Copy current settings newt.c_lflag &= ~ECHO; // Remove the ECHO flag (disables character display) tcsetattr(STDIN_FILENO, TCSANOW, &newt); // Apply new settings immediately // Read the hidden input std::string input; std::getline(std::cin, input); // Read entire line (including spaces) // Restore original terminal settings (re-enable echo) tcsetattr(STDIN_FILENO, TCSANOW, &oldt); std::cout << std::endl; // Add newline since user's Enter wasn't displayed return input; } /** * Function to vertically flip JPEG image data using STB libraries (upside-down) * * @param image_data - The JPEG data as bytes * @return std::vector - The vertically flipped JPEG image data */ std::vector flipImageVertically(const std::vector& image_data) { std::cout << "šŸ“ø Flipping image (" << image_data.size() << " bytes)..." << std::endl; // Step 1: Decode JPEG to raw pixel data int width, height, channels; unsigned char* decoded_data = stbi_load_from_memory( image_data.data(), // Input JPEG data image_data.size(), // Size of input data &width, // Output: image width &height, // Output: image height &channels, // Output: number of color channels 0 // Desired channels (0 = keep original) ); if (!decoded_data) { std::cout << "āŒ Failed to decode JPEG image: " << stbi_failure_reason() << std::endl; return image_data; // Return original if decode fails } std::cout << "āœ… Decoded image: " << width << "x" << height << " pixels, " << channels << " channels" << std::endl; // Step 2: Create buffer for flipped image data int bytes_per_pixel = channels; int row_size = width * bytes_per_pixel; std::vector flipped_pixels(width * height * bytes_per_pixel); // Step 3: Flip vertically (reverse row order to make image upside-down) for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // Calculate source and destination pixel positions int src_index = (y * width + x) * bytes_per_pixel; // Original position int dst_index = ((height - 1 - y) * width + x) * bytes_per_pixel; // Flipped position (vertically) // Copy all color channels for this pixel for (int c = 0; c < bytes_per_pixel; c++) { flipped_pixels[dst_index + c] = decoded_data[src_index + c]; } } } // Step 4: Encode flipped pixels back to JPEG std::vector output_jpeg; // Helper function to capture JPEG data written by stb_image_write // This is a "callback" - STB calls this function whenever it has encoded data to write auto write_func = [](void* context, void* data, int size) { // Cast void* back to our vector (context is the &output_jpeg from below) std::vector* output = static_cast*>(context); uint8_t* bytes = static_cast(data); // Append the new JPEG bytes to our output vector output->insert(output->end(), bytes, bytes + size); }; // Write flipped pixels to JPEG format (quality: 90/100) int success = stbi_write_jpg_to_func( write_func, // Callback function to capture data &output_jpeg, // Context passed to callback width, // Image width height, // Image height channels, // Number of channels flipped_pixels.data(), // Pixel data 90 // JPEG quality (0-100) ); // Clean up decoded image memory stbi_image_free(decoded_data); if (!success) { std::cout << "āŒ Failed to encode flipped image to JPEG" << std::endl; return image_data; // Return original if encode fails } return output_jpeg; } /** * Worker thread function that processes the work queue sequentially * This replaces the previous approach of creating one thread per image */ void workerThread(dpp::cluster* bot, WorkQueue* work_queue) { std::cout << "šŸ”§ Worker thread started" << std::endl; WorkItem item; while (work_queue->dequeue(item)) { std::cout << "\n--- PROCESSING JPEG IMAGE (Job " << item.id << ") ---" << std::endl; std::cout << "Downloading: " << item.original_filename << std::endl; std::cout << "From URL: " << item.attachment_url << std::endl; // Download the image using DPP's HTTP client std::cout << "🌐 Starting image download..." << std::endl; // Use a promise/future to make the async download synchronous for this worker std::mutex download_mutex; std::condition_variable download_cv; bool download_complete = false; bool download_success = false; std::string download_data; bot->request(item.attachment_url, dpp::m_get, [&](const dpp::http_request_completion_t& response) { std::lock_guard lock(download_mutex); if (response.status == 200) { std::cout << "āœ… Downloaded image (" << response.body.size() << " bytes)" << std::endl; download_data = response.body; download_success = true; } else { std::cout << "āŒ Failed to download image. Status: " << response.status << std::endl; download_success = false; } download_complete = true; download_cv.notify_one(); }); // Wait for download to complete { std::unique_lock lock(download_mutex); download_cv.wait(lock, [&]{ return download_complete; }); } if (download_success) { // Convert response body to vector for processing std::vector original_data(download_data.begin(), download_data.end()); // Flip the image vertically (upside-down) std::vector flipped_data = flipImageVertically(original_data); // Create new filename for flipped image std::string flipped_filename = "upside_down_" + item.original_filename; // Create message with flipped image std::string message_text = "šŸ”„ Here's your upside-down image! <@" + std::to_string(item.user_id) + ">"; dpp::message msg(item.channel_id, message_text); msg.add_file(flipped_filename, std::string(flipped_data.begin(), flipped_data.end())); // Send the flipped image std::mutex send_mutex; std::condition_variable send_cv; bool send_complete = false; bool send_success = false; bot->message_create(msg, [&](const dpp::confirmation_callback_t& callback) { std::lock_guard lock(send_mutex); if (callback.is_error()) { std::cout << "āŒ Failed to send flipped image: " << callback.get_error().message << std::endl; send_success = false; } else { std::cout << "āœ… Successfully sent flipped " << item.original_filename << "!" << std::endl; send_success = true; } send_complete = true; send_cv.notify_one(); }); // Wait for send to complete { std::unique_lock lock(send_mutex); send_cv.wait(lock, [&]{ return send_complete; }); } if (send_success) { work_queue->markCompleted(item.id); } else { work_queue->markFailed(item.id); } } else { // Download failed bot->message_create(dpp::message(item.channel_id, "āŒ Failed to download image for processing.")); work_queue->markFailed(item.id); } // Small delay between processing jobs to prevent overwhelming the system std::this_thread::sleep_for(std::chrono::milliseconds(500)); } std::cout << "šŸ”§ Worker thread shutting down" << std::endl; } /** * MAIN FUNCTION - Entry point of the Discord bot program * Sets up the bot, registers commands, and starts the event loop */ int main() { // Step 1: Initialize work queue database std::cout << "=== Discord Bot Startup ===" << std::endl; std::cout << "šŸ—„ļø Initializing work queue database..." << std::endl; WorkQueue work_queue; if (!work_queue.initialize()) { std::cerr << "āŒ Failed to initialize work queue database" << std::endl; return 1; } // Step 2: Get Discord bot token securely from user std::string BOT_TOKEN = getHiddenInput("Enter Discord Bot Token: "); // Validate that a token was provided if (BOT_TOKEN.empty()) { std::cerr << "Error: Bot token cannot be empty!" << std::endl; return 1; // Exit with error code } // Step 3: Create Discord bot instance // dpp::cluster is the main bot class that handles all Discord connections // i_default_intents gives basic permissions, i_message_content allows reading message text dpp::cluster bot(BOT_TOKEN, dpp::i_default_intents | dpp::i_message_content); // Step 4: Enable logging to see what the bot is doing bot.on_log(dpp::utility::cout_logger()); // Step 5: Start worker thread for processing the queue std::cout << "šŸ”§ Starting worker thread..." << std::endl; std::thread worker(workerThread, &bot, &work_queue); // Step 6: Set up event handler for file uploads // Lambda function that gets called whenever a message with attachments is posted bot.on_message_create([&work_queue](const dpp::message_create_t& event) { // Ignore messages sent by bots (including our own) to prevent loops if (event.msg.author.is_bot()) { return; // Early exit - don't process bot messages } // Check if this message has any file attachments if (!event.msg.attachments.empty()) { // Debug output to console (only you see this, not Discord users) std::cout << "\n=== FILE UPLOAD DETECTED ===" << std::endl; std::cout << "User: " << event.msg.author.username << "#" << event.msg.author.discriminator << std::endl; std::cout << "Channel ID: " << event.msg.channel_id << std::endl; std::cout << "Message ID: " << event.msg.id << std::endl; std::cout << "Attachments: " << event.msg.attachments.size() << std::endl; // Flag to track if we found any JPEG files bool found_jpeg = false; std::vector jpeg_attachments; // Store JPEG attachments for processing // Loop through each attached file for (const auto& attachment : event.msg.attachments) { // Print file details to console std::cout << " - File: " << attachment.filename << std::endl; std::cout << " Size: " << attachment.size << " bytes" << std::endl; std::cout << " URL: " << attachment.url << std::endl; // Check if filename ends with ".jpg" or ".jpeg" (case-insensitive) std::string filename_lower = attachment.filename; // Convert entire filename to lowercase for easier comparison std::transform(filename_lower.begin(), filename_lower.end(), filename_lower.begin(), ::tolower); // Check if file ends with .jpg (4 chars) or .jpeg (5 chars) // substr(length - N) gets the last N characters of the string if ((filename_lower.length() >= 4 && filename_lower.substr(filename_lower.length() - 4) == ".jpg") || (filename_lower.length() >= 5 && filename_lower.substr(filename_lower.length() - 5) == ".jpeg")) { std::cout << " āœ… JPEG FILE DETECTED!" << std::endl; found_jpeg = true; jpeg_attachments.push_back(attachment); // Store for processing } } // If we found JPEG files, enqueue them for processing if (found_jpeg) { std::cout << "\nšŸŽÆ ACTION: Enqueueing JPEG files for processing" << std::endl; // Add emoji reaction to the message (like clicking a reaction in Discord) event.reply("šŸ“ø JPEG detected! Adding to processing queue..."); // Enqueue each JPEG for sequential processing for (const auto& jpeg_attachment : jpeg_attachments) { WorkItem item; item.attachment_url = jpeg_attachment.url; item.original_filename = jpeg_attachment.filename; item.channel_id = event.msg.channel_id; item.user_id = event.msg.author.id; item.created_at = std::time(nullptr); item.retry_count = 0; if (work_queue.enqueue(item)) { std::cout << "āœ… Enqueued: " << jpeg_attachment.filename << std::endl; } else { std::cout << "āŒ Failed to enqueue: " << jpeg_attachment.filename << std::endl; } } } std::cout << "============================" << std::endl; } }); // Step 7: Set up slash command handler (commands that start with /) // [&bot] captures the bot variable by reference so we can use it inside the lambda bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { // Generate unique ID for this command execution (for debugging) static int command_counter = 0; // static = keeps value between function calls (like a global variable) int command_id = ++command_counter; // Pre-increment: add 1 then use value (so first command = 1) // Print detailed debug information about the command std::cout << "\n=== COMMAND RECEIVED #" << command_id << " ===" << std::endl; std::cout << "Command: " << event.command.get_command_name() << std::endl; std::cout << "User: " << event.command.get_issuing_user().username << "#" << event.command.get_issuing_user().discriminator << std::endl; std::cout << "User ID: " << event.command.get_issuing_user().id << std::endl; std::cout << "Guild ID: " << event.command.guild_id << std::endl; std::cout << "Channel ID: " << event.command.channel_id << std::endl; std::cout << "Timestamp: " << std::time(nullptr) << std::endl; // Unix timestamp std::cout << "Interaction ID: " << event.command.id << std::endl; // Check if this is the command we care about if (event.command.get_command_name() == "oomer") { std::cout << "Status: EXPECTED COMMAND (Execution #" << command_id << ")" << std::endl; std::cout << "========================" << std::endl; // CRITICAL: Must acknowledge Discord interaction within 3 seconds // or Discord shows "The application did not respond" error std::cout << "Sending interaction acknowledgment for command #" << command_id << "..." << std::endl; event.reply("šŸš€ Render started... (Command #" + std::to_string(command_id) + ")"); // Generate unique thread ID for debugging async operations static int thread_counter = 0; // Separate counter for threads int current_thread_id = ++thread_counter; // Start background thread to send image after delay // This prevents blocking the main Discord event loop std::thread([&bot, // Capture bot by reference channel_id = event.command.channel_id, // Copy channel ID user_id = event.command.get_issuing_user().id, // Copy user ID thread_id = current_thread_id, // Copy thread ID cmd_id = command_id // Copy command ID ]() { // Wait 5 seconds before sending image (simulates processing time) std::this_thread::sleep_for(std::chrono::seconds(5)); std::cout << "\n--- SENDING EMBEDDED IMAGE ---" << std::endl; std::cout << "Using embedded JPEG data (" << oomerbot_jpeg_size << " bytes)..." << std::endl; // Create vector from embedded image data (no file I/O needed!) // This copies data from the compiled-in byte array to a vector std::vector image_data(oomerbot_jpeg_data, oomerbot_jpeg_data + oomerbot_jpeg_size); // Sanity check - make sure we have image data if (!image_data.empty()) { // Create message text with user mention (@username becomes clickable) std::string message_text = "šŸŽØ Render complete! <@" + std::to_string(user_id) + ">"; // Create Discord message object dpp::message msg(channel_id, message_text); // Attach the image file to the message // First parameter: filename that appears in Discord // Second parameter: file data as string msg.add_file("oomerbot.jpeg", std::string(image_data.begin(), image_data.end())); // Send the message with image attachment // Lambda function handles the response (success or error) bot.message_create(msg, [thread_id, cmd_id](const dpp::confirmation_callback_t& callback) { if (callback.is_error()) { // Something went wrong - print error details std::cout << "āŒ Thread #" << thread_id << " (command #" << cmd_id << ") failed to send image: " << callback.get_error().message << std::endl; std::cout << "Error code: " << callback.get_error().code << std::endl; } else { // Success! std::cout << "āœ… Thread #" << thread_id << " (command #" << cmd_id << ") successfully sent embedded oomerbot.jpeg!" << std::endl; } }); } else { // This should never happen since image is embedded at compile time std::cout << "āŒ Embedded image data is empty" << std::endl; } }).detach(); // detach() = thread runs independently, don't wait for it } else { // Handle unexpected commands (probably old registrations) std::cout << "Status: UNEXPECTED COMMAND (probably old registration) - Command #" << command_id << std::endl; std::cout << "========================" << std::endl; event.reply("āš ļø This command is no longer supported. Please use `/oomer` instead."); } }); // Step 8: Set up bot ready event (called when bot successfully connects to Discord) bot.on_ready([&bot](const dpp::ready_t& event) { // run_once ensures this only happens on first connection, not reconnections if (dpp::run_once()) { std::cout << "Bot is ready! Starting command registration..." << std::endl; std::cout << "Bot user: " << bot.me.username << " (ID: " << bot.me.id << ")" << std::endl; // Clean up any old slash commands before registering new ones std::cout << "Clearing old global commands..." << std::endl; bot.global_commands_get([&bot](const dpp::confirmation_callback_t& callback) { if (!callback.is_error()) { // Get list of existing commands auto commands = std::get(callback.value); std::cout << "Found " << commands.size() << " existing commands" << std::endl; // Delete each old command for (auto& command : commands) { std::cout << "Deleting old command: " << command.second.name << std::endl; bot.global_command_delete(command.first, [](const dpp::confirmation_callback_t& del_callback) { if (del_callback.is_error()) { std::cout << "āŒ Failed to delete command: " << del_callback.get_error().message << std::endl; } else { std::cout << "āœ… Command deleted successfully" << std::endl; } }); } } else { std::cout << "āŒ Failed to get existing commands: " << callback.get_error().message << std::endl; } // Register our new slash command std::cout << "Registering oomer command..." << std::endl; // Parameters: command_name, description, bot_id bot.global_command_create(dpp::slashcommand("oomer", "render oj", bot.me.id), [](const dpp::confirmation_callback_t& reg_callback) { if (reg_callback.is_error()) { std::cout << "āŒ Failed to register command: " << reg_callback.get_error().message << std::endl; } else { std::cout << "āœ… Command registered successfully!" << std::endl; } }); }); } }); // Step 9: Set up graceful shutdown handler std::cout << "Starting bot event loop..." << std::endl; // Start the bot in a separate thread so we can handle shutdown gracefully std::thread bot_thread([&bot]() { bot.start(dpp::st_wait); }); // Wait for the bot thread to finish (which should be never unless there's an error) bot_thread.join(); // If we get here, the bot has shut down, so clean up the worker thread std::cout << "Bot shutting down, stopping worker thread..." << std::endl; work_queue.requestShutdown(); worker.join(); // This line should never be reached unless the bot shuts down return 0; }