poomer-discord/poomer-discord.cpp

836 lines
36 KiB
C++

// 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 <iostream>
// String handling - for std::string class and std::to_string() function
#include <string>
// Terminal I/O control - for hiding password input in getHiddenInput()
#include <termios.h>
// Unix standard definitions - provides STDIN_FILENO constant
#include <unistd.h>
// Time functions - for std::time() to generate timestamps
#include <ctime>
// Dynamic arrays - for std::vector to hold image byte data
#include <vector>
// Threading support - for worker thread and synchronization
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
// Time utilities - for std::chrono::seconds() delays
#include <chrono>
// Algorithm functions - for std::transform (string case conversion)
#include <algorithm>
// SQLite3 database - for persistent work queue storage
#include <sqlite3.h>
// 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<bool> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<uint8_t> - The vertically flipped JPEG image data
*/
std::vector<uint8_t> flipImageVertically(const std::vector<uint8_t>& 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<unsigned char> 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<uint8_t> 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<uint8_t>* output = static_cast<std::vector<uint8_t>*>(context);
uint8_t* bytes = static_cast<uint8_t*>(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<std::mutex> 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<std::mutex> lock(download_mutex);
download_cv.wait(lock, [&]{ return download_complete; });
}
if (download_success) {
// Convert response body to vector for processing
std::vector<uint8_t> original_data(download_data.begin(), download_data.end());
// Flip the image vertically (upside-down)
std::vector<uint8_t> 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<std::mutex> 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<std::mutex> 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<dpp::attachment> 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<uint8_t> 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<struct register_bot_commands>()) {
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<dpp::slashcommand_map>(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;
}