Fix command registration and add command infra
This commit is contained in:
+14
-1
@@ -487,7 +487,20 @@ file(COPY ${_libs} DESTINATION "@CONCORD_PREFIX@/lib")
|
||||
# the environment rather than the make command line because Concord's
|
||||
# Makefile appends its own -I flags to it; a command-line assignment would
|
||||
# override those and break the build.
|
||||
set(CONCORD_CFLAGS "-O2 -g $<JOIN:${HAMMY_INSTRUMENT_COMPILE}, >")
|
||||
# ... with one carve-out. chash.h's string hash is djb2-style:
|
||||
# (hash) = (((hash) << 1) + (hash)) + key[i];
|
||||
# on a signed accumulator, so it deliberately relies on wraparound. That is
|
||||
# two counts of UB per character -- the shift of a negative value and the
|
||||
# overflowing add -- plus a third in the __chash_abs() that follows. With
|
||||
# -fno-sanitize-recover it aborts the first time a ratelimit key hashes past
|
||||
# LONG_MAX, which a guild command endpoint manages immediately.
|
||||
#
|
||||
# The wraparound is benign and the code is not ours to fix, so drop these
|
||||
# two checks for Concord's build only. Everything else UBSan looks at stays
|
||||
# on, and our own translation units keep the full set -- these flags are not
|
||||
# in HAMMY_SANITIZER_FLAGS, only here.
|
||||
set(CONCORD_UB_CARVEOUTS "-fno-sanitize=signed-integer-overflow -fno-sanitize=shift")
|
||||
set(CONCORD_CFLAGS "-O2 -g $<JOIN:${HAMMY_INSTRUMENT_COMPILE}, > $<${HAMMY_IS_SANITIZED}:${CONCORD_UB_CARVEOUTS}>")
|
||||
|
||||
# gencodecs/Makefile hardcodes CC/HOSTCC/CPP to "cc" for its host-side code
|
||||
# generator, with plain '=' assignments that the environment cannot override.
|
||||
|
||||
+23
-5
@@ -10,8 +10,10 @@
|
||||
|
||||
struct hammy_bot_t {
|
||||
struct discord* client; // Owning reference to the client - handoff from main.c
|
||||
bool commands_registered; // A flag if commands have been registered. Avoid re-registering every reconnect.
|
||||
vector_t* commands; // vector_t of commands. Owning.
|
||||
bool commandsRegistered; // A flag if commands have been registered. Avoid re-registering every reconnect.
|
||||
vector_t* commands; // vector_t of commands. Owning the vector, NOT the elements.
|
||||
hammy_pool_t* pool; // Owning reference to the thread pool.
|
||||
u64snowflake appId; // Learned from the ready event.
|
||||
};
|
||||
|
||||
// Alloc on heap and create
|
||||
@@ -20,13 +22,29 @@ hammy_bot_t* hammy_bot_create();
|
||||
// Sets the on_ready() call function pointer
|
||||
bool hammy_bot_set_on_ready(hammy_bot_t* bot, void (*func)(struct discord* client, const struct discord_ready* event));
|
||||
|
||||
// Copies every entry from hammy_command_builtins() into the vector.
|
||||
bool hammy_bot_load_builtins(hammy_bot_t* bot);
|
||||
|
||||
// Starts the worker pool. Pass 0 for the defaults. Call this from the ready
|
||||
// callback, NOT before hammy_bot_run(): the workers are built on
|
||||
// discord_clone(), which only succeeds inside a gateway dispatch.
|
||||
// Returns true if the pool is already running.
|
||||
bool hammy_bot_start_pool(hammy_bot_t* bot, size_t nWorkers, size_t queueCap);
|
||||
|
||||
// Runs the bot. Return true on succeed, false on failure.
|
||||
bool hammy_bot_run(hammy_bot_t* bot);
|
||||
|
||||
// Adds a command to the vector as a COPY, get rid of the old struct.
|
||||
bool hammy_bot_add_command(hammy_bot_t* bot, hammy_command_t* command);
|
||||
// Adds a command to the vector as a COPY. Commands own nothing, so the source
|
||||
// struct needs no cleanup and may be a compound literal.
|
||||
bool hammy_bot_add_command(hammy_bot_t* bot, const hammy_command_t* command);
|
||||
|
||||
// Registers all commands. Return true if succeeded or already registered; false if failure.
|
||||
// Looks a command up by name. Returns a pointer into the vector's storage, or
|
||||
// NULL. Valid until the vector is next modified, which after startup is never.
|
||||
const hammy_command_t* hammy_bot_find_command(const hammy_bot_t* bot, const char* name);
|
||||
|
||||
// Registers all commands with Discord. Return true if succeeded or already
|
||||
// registered; false if failure. Requires bot->application_id to be set, so call
|
||||
// this from on_ready, not before.
|
||||
bool hammy_bot_register_commands(hammy_bot_t* bot);
|
||||
|
||||
// Destroy bot, NULLs the passing reference
|
||||
|
||||
+7
-1
@@ -30,12 +30,14 @@ struct hammy_job_t {
|
||||
hammy_arg_t* args; // Array of size nArgs. Owning, array and contents.
|
||||
size_t nArgs;
|
||||
int64_t queuedAt; // Staleness checks, in ms. From discord_timestamp().
|
||||
|
||||
hammy_bot_t* bot; // NOT owning, used to access the command table
|
||||
};
|
||||
|
||||
// Deep-copies everything the job needs out of the interaction event, so the
|
||||
// event may be freed by Concord the moment the handler returns.
|
||||
// Returns NULL on allocation failure.
|
||||
hammy_job_t* hammy_job_create(struct discord* client, const struct discord_interaction* event);
|
||||
hammy_job_t* hammy_job_create(hammy_bot_t* bot, const struct discord_interaction* event);
|
||||
|
||||
// Frees the job and everything it owns. NULLs the passing reference.
|
||||
// Safe to call with NULL or with a pointer to NULL.
|
||||
@@ -56,4 +58,8 @@ void hammy_job_run(hammy_job_t* job, struct discord* client);
|
||||
// Used by hammy_job_run() and by the pool's error paths.
|
||||
void hammy_job_reply(const hammy_job_t* job, struct discord* client, const char* content);
|
||||
|
||||
// Sends a fresh interaction response with a plain text body.
|
||||
// Only valid on the instant path, where nothing has been sent yet.
|
||||
void hammy_job_respond(const hammy_job_t* job, struct discord* client, const char* content);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -43,6 +43,9 @@ struct hammy_pool_t {
|
||||
// Creates the pool and starts n_workers threads, each with its own
|
||||
// discord_clone() of client. Pass 0 for either size to take the defaults.
|
||||
// Returns NULL on failure; no threads are left running in that case.
|
||||
// MUST be called from inside a gateway dispatch callback (on_ready is the
|
||||
// earliest one): discord_clone() copies the gateway's current payload and
|
||||
// fails with CCORD_ERRNO when there isn't one.
|
||||
hammy_pool_t* hammy_pool_create(struct discord* client, size_t nWorkers, size_t queueCap);
|
||||
|
||||
// Enqueues a job. See hammy_push_result_t for who owns the job afterwards.
|
||||
@@ -56,7 +59,8 @@ hammy_push_result_t hammy_pool_push(hammy_pool_t* pool, hammy_job_t* job);
|
||||
void hammy_pool_shutdown(hammy_pool_t* pool);
|
||||
|
||||
// As above but discards anything still queued (each dropped job gets an
|
||||
// apology reply if it can be sent quickly).
|
||||
// apology reply). Call from the gateway thread only - the apologies are
|
||||
// serialised through the original client, which that thread owns.
|
||||
void hammy_pool_shutdown_now(hammy_pool_t* pool);
|
||||
|
||||
// Frees the pool. Runs hammy_pool_shutdown() first if it has not happened yet.
|
||||
|
||||
@@ -15,6 +15,8 @@ struct hammy_worker_t {
|
||||
|
||||
// Clones client, spawns the thread. Returns false on clone or spawn failure,
|
||||
// leaving the worker safe to pass to hammy_worker_join().
|
||||
// Inherits hammy_pool_create()'s precondition: the clone only succeeds from
|
||||
// inside a gateway dispatch callback.
|
||||
bool hammy_worker_start(hammy_worker_t* worker, hammy_pool_t* pool, struct discord* client, int id);
|
||||
|
||||
// Joins the thread if it was started and cleans up the clone.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#include <concord/discord.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <hammy/command.h>
|
||||
#include <hammy/job.h>
|
||||
|
||||
// Discord epoch, for turning a snowflake back into a wall-clock time.
|
||||
#define HAMMY_DISCORD_EPOCH_MS 1420070400000LL
|
||||
|
||||
static int64_t hammy_snowflake_to_ms(u64snowflake id) {
|
||||
return (int64_t)(id >> 22) + HAMMY_DISCORD_EPOCH_MS;
|
||||
}
|
||||
|
||||
// Instant command: runs on the gateway thread, sends a fresh response.
|
||||
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client) {
|
||||
// Gateway heartbeat round trip, as measured by Concord.
|
||||
int gatewayMs = discord_get_ping(client);
|
||||
|
||||
// Time from Discord minting the interaction to us handling it. This is the
|
||||
// number that degrades under load, so it is the one worth showing.
|
||||
int64_t handledMs = (int64_t)discord_timestamp(client) - hammy_snowflake_to_ms(job->id);
|
||||
|
||||
char body[256];
|
||||
snprintf(body, sizeof(body),
|
||||
"Pong!\nGateway: `%d ms`\nHandled in: `%" PRId64 " ms`",
|
||||
gatewayMs, handledMs);
|
||||
|
||||
hammy_job_respond(job, client, body);
|
||||
}
|
||||
+227
-34
@@ -1,14 +1,103 @@
|
||||
#include <concord/discord.h>
|
||||
#include <concord/log.h>
|
||||
#include <dlibc/vector.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <hammy/bot.h>
|
||||
#include <hammy/command.h>
|
||||
#include <hammy/job.h>
|
||||
#include <hammy/pool.h>
|
||||
|
||||
static void hammy_bot_attach(struct discord* client, hammy_bot_t* bot) {
|
||||
discord_set_data(client, bot);
|
||||
}
|
||||
|
||||
static hammy_bot_t* hammy_bot_from_client(struct discord* client) {
|
||||
return (hammy_bot_t*)discord_get_data(client);
|
||||
}
|
||||
|
||||
static void hammy_bot_on_interaction(struct discord* client, const struct discord_interaction* event) {
|
||||
if (event->type != DISCORD_INTERACTION_APPLICATION_COMMAND) {
|
||||
return;
|
||||
}
|
||||
|
||||
hammy_bot_t* bot = hammy_bot_from_client(client);
|
||||
if (!bot) {
|
||||
log_warn("[bot] No bot data attached to client. Cannot handle interaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
const char* name = (event->data && event->data->name) ? event->data->name : NULL;
|
||||
const hammy_command_t* command = hammy_bot_find_command(bot, name);
|
||||
if (!command) {
|
||||
log_warn("[bot] No command found for interaction '%s'.", name ? name : "(null)");
|
||||
|
||||
struct discord_interaction_response params = {
|
||||
.type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
.data = &(struct discord_interaction_callback_data){
|
||||
.content = "I don't know that command!"
|
||||
}
|
||||
};
|
||||
|
||||
CCORDcode code = discord_create_interaction_response(client, event->id, event->token, ¶ms, NULL);
|
||||
|
||||
if (code != CCORD_OK) {
|
||||
log_warn("[job] Failed to send interaction error response for interaction %" PRIu64 ": %d", event->id, code);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
hammy_job_t* job = hammy_job_create(bot, event);
|
||||
if (!job) {
|
||||
log_error("[bot] Failed to create job for interaction '%s'.", name ? name : "(null)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Instant path: no defer or queue, answered on the spot
|
||||
if (command->instant) {
|
||||
command->handler(job, client);
|
||||
hammy_job_destroy(&job);
|
||||
return;
|
||||
}
|
||||
|
||||
// Deferred path: ACK first, 3 second window by discord, then queue to worker pool
|
||||
struct discord_interaction_response deferred = {
|
||||
.type = DISCORD_INTERACTION_DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
};
|
||||
|
||||
discord_create_interaction_response(client, event->id, event->token, &deferred, NULL);
|
||||
|
||||
switch (hammy_pool_push(bot->pool, job)) {
|
||||
case HAMMY_PUSH_OK:
|
||||
log_info("[bot] Queued job for interaction '%s'.", name ? name : "(null)");
|
||||
break;
|
||||
case HAMMY_PUSH_FULL:
|
||||
log_warn("[bot] Queue pool full. Cannot queue job for interaction '%s'.", name ? name : "(null)");
|
||||
hammy_job_reply(job, client, "I am a bit busy right now! Please try again later.");
|
||||
hammy_job_destroy(&job);
|
||||
break;
|
||||
case HAMMY_PUSH_SHUTDOWN:
|
||||
// Also covers the window before on_ready starts the pool, since
|
||||
// hammy_pool_push() reports a NULL pool the same way.
|
||||
log_error("[bot] Worker pool unavailable. Cannot queue job for interaction '%s'.", name ? name : "(null)");
|
||||
hammy_job_reply(job, client, "Hammy is not accepting commands right now. Please try again shortly.");
|
||||
hammy_job_destroy(&job);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hammy_bot_t* hammy_bot_create() {
|
||||
hammy_bot_t* bot = (hammy_bot_t*)malloc(sizeof(hammy_bot_t));
|
||||
hammy_bot_t* bot = (hammy_bot_t*)calloc(1, sizeof(*bot));
|
||||
if (!bot) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
bot->client = NULL;
|
||||
bot->commands_registered = false;
|
||||
bot->commandsRegistered = false;
|
||||
bot->commands = vector_create(sizeof(hammy_command_t));
|
||||
|
||||
// Check if vector allocation errored
|
||||
@@ -17,37 +106,60 @@ hammy_bot_t* hammy_bot_create() {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Init the discord client - even though this is an owning reference, the main() and others may call functions on it (if we don't stay singlethread anymore then FIX THIS)
|
||||
bot->client = discord_from_json("config.json");
|
||||
if (!bot->client) {
|
||||
vector_destroy(&bot->commands);
|
||||
free(bot);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
bool hammy_bot_set_on_ready(hammy_bot_t* bot, void (*func)(struct discord* client, const struct discord_ready* event)) {
|
||||
if (!bot || !func || !bot->client) { return false; }
|
||||
bool hammy_bot_add_command(hammy_bot_t* bot, const hammy_command_t* command) {
|
||||
if (!bot || !bot->commands || !command || !command->name) { return false; }
|
||||
|
||||
discord_set_on_ready(bot->client, func);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hammy_bot_run(hammy_bot_t* bot) {
|
||||
if (!bot || !bot->client) { return false; }
|
||||
|
||||
discord_run(bot->client);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hammy_bot_add_command(hammy_bot_t* bot, hammy_command_t* command) {
|
||||
if (!bot || !command) { return false; }
|
||||
|
||||
// Push into commands vector
|
||||
// Copy the command into the vector. The command owns nothing, so this is safe.
|
||||
if (vector_push_back(bot->commands, command) < 0) {
|
||||
log_error("[bot] Failed to add command '%s' to bot.", command->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hammy_bot_load_builtins(hammy_bot_t* bot) {
|
||||
if (!bot) { return false; }
|
||||
|
||||
size_t count = 0;
|
||||
const hammy_command_t* builtins = hammy_command_builtins(&count);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (!hammy_bot_add_command(bot, &builtins[i])) {
|
||||
log_error("[bot] Failed to load builtin command '%s'.", builtins[i].name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
log_info("[bot] Loaded %zu builtin commands.", count);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const hammy_command_t* hammy_bot_find_command(const hammy_bot_t* bot, const char* name) {
|
||||
if (!bot || !bot->commands || !name) { return NULL; }
|
||||
|
||||
// Vector guarantees contiguous and gapless storage, so a plain-array search is fine.
|
||||
const hammy_command_t* commands = (const hammy_command_t*)vector_as_c_array(bot->commands);
|
||||
|
||||
return hammy_command_find(commands, vector_size(bot->commands), name);
|
||||
}
|
||||
|
||||
bool hammy_bot_start_pool(hammy_bot_t* bot, size_t nWorkers, size_t queueCap) {
|
||||
if (!bot || !bot->client) { return false; }
|
||||
if (bot->pool) {
|
||||
// A reconnect re-fires READY, so this is routine rather than a problem.
|
||||
log_debug("[bot] Worker pool already started. No-op.");
|
||||
return true; // Already started
|
||||
}
|
||||
|
||||
bot->pool = hammy_pool_create(bot->client, nWorkers, queueCap);
|
||||
|
||||
if (!bot->pool) {
|
||||
log_error("[bot] Failed to create worker pool.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -55,20 +167,101 @@ bool hammy_bot_add_command(hammy_bot_t* bot, hammy_command_t* command) {
|
||||
}
|
||||
|
||||
bool hammy_bot_register_commands(hammy_bot_t* bot) {
|
||||
if (!bot) { return false; }
|
||||
if (!bot || !bot->client) { return false; }
|
||||
if (bot->commandsRegistered) { return true; } // Already registered
|
||||
|
||||
// TODO: Figure out registration logic
|
||||
if (!bot->appId) {
|
||||
log_error("[bot] No application ID set. Cannot register commands. This should be set in the on_ready() callback.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Temporary - dev guild ID for testing.
|
||||
const char* devGuild = getenv("HAMMY_DEV_GUILD");
|
||||
u64snowflake guildId = devGuild ? (u64snowflake)strtoull(devGuild, NULL, 10) : 0;
|
||||
|
||||
size_t count = vector_size(bot->commands);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const hammy_command_t* cmd = (const hammy_command_t*)vector_get_const(bot->commands, i);
|
||||
|
||||
if (!cmd || !cmd->name || !cmd->description) {
|
||||
log_warn("[bot] Invalid command at index %zu. Cannot register with Discord. Skipping!", i);
|
||||
continue; // Skip this command, but try to register the rest
|
||||
}
|
||||
|
||||
CCORDcode code;
|
||||
|
||||
if (guildId) {
|
||||
struct discord_create_guild_application_command params = {
|
||||
.name = (char*)cmd->name,
|
||||
.description = (char*)cmd->description,
|
||||
.options = cmd->options
|
||||
};
|
||||
|
||||
code = discord_create_guild_application_command(bot->client, bot->appId, guildId, ¶ms, NULL);
|
||||
} else {
|
||||
struct discord_create_global_application_command params = {
|
||||
.name = (char*)cmd->name,
|
||||
.description = (char*)cmd->description,
|
||||
.options = cmd->options
|
||||
};
|
||||
|
||||
code = discord_create_global_application_command(bot->client, bot->appId, ¶ms, NULL);
|
||||
}
|
||||
|
||||
// Passing NULL for the return handle makes these fire-and-forget, so
|
||||
// a successful enqueue reports CCORD_PENDING rather than CCORD_OK.
|
||||
if (code != CCORD_OK && code != CCORD_PENDING) {
|
||||
log_error("[bot] Failed to register command '%s' with Discord. Error code: %d", cmd->name, code);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bot->commandsRegistered = true;
|
||||
log_info("[bot] Successfully registered %zu commands with Discord %s.", count, guildId ? "to dev guild" : "globally");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hammy_bot_set_on_ready(hammy_bot_t* bot, void (*func)(struct discord* client, const struct discord_ready* event)) {
|
||||
if (!bot || !func || !bot->client) { return false; }
|
||||
|
||||
discord_set_on_ready(bot->client, func);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hammy_bot_run(hammy_bot_t* bot) {
|
||||
if (!bot || !bot->client) { return false; }
|
||||
|
||||
hammy_bot_attach(bot->client, bot);
|
||||
discord_set_on_interaction_create(bot->client, &hammy_bot_on_interaction);
|
||||
|
||||
return discord_run(bot->client) == CCORD_OK;
|
||||
}
|
||||
|
||||
bool hammy_bot_destroy(hammy_bot_t** bot) {
|
||||
if (!bot || !(*bot)) { return false; }
|
||||
|
||||
if ((*bot)->client) {
|
||||
discord_cleanup((*bot)->client);
|
||||
hammy_bot_t* b = *bot;
|
||||
|
||||
// Workers hold clones of the client
|
||||
if (b->pool) {
|
||||
hammy_pool_destroy(&b->pool);
|
||||
}
|
||||
|
||||
// No element destructor, commands own nothing.
|
||||
if (b->commands) {
|
||||
vector_destroy(&b->commands);
|
||||
}
|
||||
|
||||
if (b->client) {
|
||||
discord_cleanup(b->client);
|
||||
b->client = NULL;
|
||||
}
|
||||
|
||||
// TODO: Potential other stuff here
|
||||
vector_destroy(&(*bot)->commands); // commands is NULL form hereon, calls destructors on commands automatically
|
||||
free(*bot);
|
||||
*bot = NULL;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
+1
-5
@@ -6,11 +6,7 @@
|
||||
// Handlers live in src/hammy/commands/. Declared here rather than in a header
|
||||
// so adding a command touches exactly two places: its own .c file and this
|
||||
// table.
|
||||
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client) {
|
||||
(void)job;
|
||||
(void)client;
|
||||
log_info("[command] ping");
|
||||
}
|
||||
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client);
|
||||
|
||||
// The command table. Pure data - no allocation, no lifetime, no destructor.
|
||||
// Copying an entry into the bot's vector is a plain struct copy, and the vector
|
||||
|
||||
+33
-9
@@ -4,6 +4,8 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <hammy/bot.h>
|
||||
#include <hammy/command.h>
|
||||
#include <hammy/job.h>
|
||||
|
||||
// strdup() is POSIX, so we'll keep a local and keep the code portable.
|
||||
@@ -28,17 +30,19 @@ static u64snowflake hammy_job_extract_user(const struct discord_interaction* eve
|
||||
return 0;
|
||||
}
|
||||
|
||||
hammy_job_t* hammy_job_create(struct discord* client, const struct discord_interaction* event) {
|
||||
if (!client || !event) { return NULL; }
|
||||
hammy_job_t* hammy_job_create(hammy_bot_t* bot, const struct discord_interaction* event) {
|
||||
if (!bot || !bot->client || !event) { return NULL; }
|
||||
|
||||
hammy_job_t* job = (hammy_job_t*)calloc(1, sizeof(*job));
|
||||
if (!job) { return NULL; }
|
||||
|
||||
job->bot = bot;
|
||||
job->id = event->id;
|
||||
job->appId = event->application_id;
|
||||
job->user = hammy_job_extract_user(event);
|
||||
job->token = hammy_strdup(event->token);
|
||||
job->queuedAt = (int64_t)discord_timestamp(client);
|
||||
job->queuedAt = (int64_t)discord_timestamp(bot->client);
|
||||
|
||||
job->appId = event->application_id ? event->application_id : bot->appId;
|
||||
|
||||
if (!job->token) {
|
||||
goto fail;
|
||||
@@ -135,13 +139,33 @@ void hammy_job_reply(const hammy_job_t* job, struct discord* client, const char*
|
||||
}
|
||||
}
|
||||
|
||||
void hammy_job_respond(const hammy_job_t* job, struct discord* client, const char* content) {
|
||||
if (!job || !client || !content) { return; }
|
||||
|
||||
struct discord_interaction_response params = {
|
||||
.type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
.data = &(struct discord_interaction_callback_data){
|
||||
.content = (char*)content
|
||||
}
|
||||
};
|
||||
|
||||
CCORDcode code = discord_create_interaction_response(client, job->id, job->token, ¶ms, NULL);
|
||||
|
||||
if (code != CCORD_OK) {
|
||||
log_warn("[job] Failed to send interaction response for interaction %" PRIu64 ": %d", job->id, code);
|
||||
}
|
||||
}
|
||||
|
||||
void hammy_job_run(hammy_job_t* job, struct discord* client) {
|
||||
if (!job || !client) { return; }
|
||||
|
||||
// TODO: look job->command up in the bot's command vector and call its
|
||||
// handler with (job, client). Placeholder until command.h grows a
|
||||
// dispatch entry point.
|
||||
log_info("[job] Running command '%s' for interaction %" PRIu64, job->command ? job->command : "unknown", job->id);
|
||||
|
||||
hammy_job_reply(job, client, "This is a placeholder reply. The command handler is not yet implemented.");
|
||||
const hammy_command_t* command = hammy_bot_find_command(job->bot, job->command);
|
||||
if (!command || !command->handler) {
|
||||
log_warn("[job] No handler found for command '%s'.", job->command ? job->command : "unknown");
|
||||
hammy_job_reply(job, client, "I don't know that command!");
|
||||
return;
|
||||
}
|
||||
|
||||
command->handler(job, client);
|
||||
}
|
||||
|
||||
+26
-2
@@ -2,6 +2,7 @@
|
||||
#include <concord/log.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <hammy/bot.h>
|
||||
#include <hammy/job.h>
|
||||
#include <hammy/pool.h>
|
||||
#include <hammy/worker.h>
|
||||
@@ -104,13 +105,22 @@ static void hammy_pool_stop(hammy_pool_t* pool, bool drain) {
|
||||
|
||||
pool->shutdown = true;
|
||||
|
||||
if (!drain) {
|
||||
// Detach the queue under the lock, but apologise outside it - a REST
|
||||
// enqueue has no business happening while the workers are blocked on it.
|
||||
hammy_job_t** dropped = NULL;
|
||||
size_t nDropped = 0;
|
||||
|
||||
if (!drain && pool->count > 0) {
|
||||
dropped = (hammy_job_t**)calloc(pool->count, sizeof(*dropped));
|
||||
|
||||
while (pool->count > 0) {
|
||||
hammy_job_t* job = pool->jobs[pool->head];
|
||||
pool->head = (pool->head + 1) % pool->cap;
|
||||
pool->count--;
|
||||
|
||||
hammy_job_destroy(&job);
|
||||
// Out of memory on the way out is not worth a leak, so drop it quietly.
|
||||
if (dropped) { dropped[nDropped++] = job; }
|
||||
else { hammy_job_destroy(&job); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +128,20 @@ static void hammy_pool_stop(hammy_pool_t* pool, bool drain) {
|
||||
pthread_cond_broadcast(&pool->notEmpty);
|
||||
pthread_mutex_unlock(&pool->lock);
|
||||
|
||||
for (size_t i = 0; i < nDropped; i++) {
|
||||
hammy_job_t* job = dropped[i];
|
||||
|
||||
// The job's bot back-reference carries the original client, which is
|
||||
// the one this thread is allowed to serialise through.
|
||||
if (job->bot && job->bot->client) {
|
||||
hammy_job_reply(job, job->bot->client, "Hammy is shutting down, so this command was dropped. Please try again once it is back.");
|
||||
}
|
||||
|
||||
hammy_job_destroy(&job);
|
||||
}
|
||||
|
||||
free(dropped);
|
||||
|
||||
for (size_t i = 0; i < pool->nWorkers; i++) {
|
||||
hammy_worker_join(&pool->workers[i]);
|
||||
}
|
||||
|
||||
+22
-4
@@ -1,4 +1,5 @@
|
||||
#include <concord/discord.h>
|
||||
#include <concord/discord-internal.h>
|
||||
#include <concord/log.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
@@ -6,6 +7,20 @@
|
||||
#include <hammy/pool.h>
|
||||
#include <hammy/worker.h>
|
||||
|
||||
// Workaround for a concord 3.0.1 bug. _discord_clone_gateway_cleanup()
|
||||
// (discord-client.c:833) frees payload.json.table and then frees payload.data
|
||||
// as well, but payload.data is a jsmnf_pair pointing INTO that table, not a
|
||||
// separate allocation. Every discord_cleanup() of a clone is therefore an
|
||||
// invalid free. Clearing the field first skips that free; the table it points
|
||||
// into is still released, so nothing leaks.
|
||||
static void hammy_worker_cleanup_clone(struct discord* clone) {
|
||||
if (!clone) { return; }
|
||||
|
||||
clone->gw.payload.data = NULL;
|
||||
|
||||
discord_cleanup(clone);
|
||||
}
|
||||
|
||||
static void* hammy_worker_main(void* arg) {
|
||||
hammy_worker_t* worker = (hammy_worker_t*)arg;
|
||||
hammy_pool_t* pool = worker->pool;
|
||||
@@ -63,8 +78,11 @@ bool hammy_worker_start(hammy_worker_t* worker, hammy_pool_t* pool, struct disco
|
||||
worker->clientCopy = NULL;
|
||||
worker->started = false;
|
||||
|
||||
// According to the concord spec, each thread must have its own discord client, so we clone it here.
|
||||
// However, concord's buffers, URLs, headers, etc. are NOT shared-safe. They're per-client.
|
||||
// Each thread needs its own client. The queues, pollers and timers are all
|
||||
// shared through pointers, but client->registry - the reflect-c registry
|
||||
// that serialises request bodies on the calling thread - is not.
|
||||
// discord_clone() copies the gateway's *current* payload, so this only
|
||||
// works while a dispatch is in flight. See hammy_pool_create().
|
||||
worker->clientCopy = discord_clone(client);
|
||||
if (!worker->clientCopy) {
|
||||
log_error("[worker %d] Failed to clone client", id);
|
||||
@@ -73,7 +91,7 @@ bool hammy_worker_start(hammy_worker_t* worker, hammy_pool_t* pool, struct disco
|
||||
|
||||
if (pthread_create(&worker->thread, NULL, &hammy_worker_main, worker) != 0) {
|
||||
log_error("[worker %d] Failed to create thread", id);
|
||||
discord_cleanup(worker->clientCopy);
|
||||
hammy_worker_cleanup_clone(worker->clientCopy);
|
||||
worker->clientCopy = NULL;
|
||||
|
||||
return false;
|
||||
@@ -93,7 +111,7 @@ void hammy_worker_join(hammy_worker_t* worker) {
|
||||
}
|
||||
|
||||
if (worker->clientCopy) {
|
||||
discord_cleanup(worker->clientCopy);
|
||||
hammy_worker_cleanup_clone(worker->clientCopy);
|
||||
worker->clientCopy = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
+67
-21
@@ -1,44 +1,90 @@
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <concord/discord.h>
|
||||
#include <concord/log.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <hammy/bot.h>
|
||||
#include <hammy/pool.h>
|
||||
|
||||
static void on_signal(int sig) {
|
||||
#define HAMMY_CONFIG_PATH "config.json"
|
||||
|
||||
static hammy_bot_t* g_bot = NULL;
|
||||
|
||||
static void hammy_on_sigint(int sig) {
|
||||
(void)sig;
|
||||
discord_shutdown_all();
|
||||
|
||||
// Only sets a flag inside Concord and makes discord_run() return. The real
|
||||
// teardown happens back in main().
|
||||
if (g_bot && g_bot->client) discord_shutdown(g_bot->client);
|
||||
}
|
||||
|
||||
void on_ready(struct discord* client, const struct discord_ready* event) {
|
||||
(void)client;
|
||||
log_info("[master] Logged in as %s", event->user->username);
|
||||
static void hammy_on_ready(struct discord* client, const struct discord_ready* event) {
|
||||
hammy_bot_t* bot = (hammy_bot_t*)discord_get_data(client);
|
||||
if (!bot) return;
|
||||
|
||||
log_info("[hammy] logged in as %s", event->user->username);
|
||||
|
||||
// The application id is only available now, and both registration and
|
||||
// response editing need it.
|
||||
bot->appId = event->application->id;
|
||||
|
||||
// discord_clone() deep-copies the gateway's *current* payload, so it only
|
||||
// succeeds from inside a dispatch callback. READY is the first one we get.
|
||||
if (!hammy_bot_start_pool(bot, 0, 0)) { // 0, 0 = defaults
|
||||
log_fatal("[hammy] Could not start the worker pool. Shutting down.");
|
||||
discord_shutdown(client);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
hammy_bot_register_commands(bot);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
struct sigaction sa = { 0 };
|
||||
sa.sa_handler = &on_signal;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0; /* deliberately no SA_RESTART */
|
||||
|
||||
sigaction(SIGTERM, &sa, NULL);
|
||||
sigaction(SIGINT, &sa, NULL);
|
||||
|
||||
ccord_global_init();
|
||||
|
||||
hammy_bot_t* bot = hammy_bot_create();
|
||||
if (!bot) {
|
||||
log_error("[master] Hammy Bot creation returned NULL! Bailing!");
|
||||
fprintf(stderr, "could not create bot\n");
|
||||
ccord_global_cleanup();
|
||||
|
||||
return 1;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
hammy_bot_set_on_ready(bot, &on_ready);
|
||||
hammy_bot_run(bot); // This will block and exit afterwards
|
||||
// Token and logging settings both come out of config.json.
|
||||
bot->client = discord_config_init(HAMMY_CONFIG_PATH);
|
||||
if (!bot->client) {
|
||||
fprintf(stderr, "could not init discord client from %s\n", HAMMY_CONFIG_PATH);
|
||||
hammy_bot_destroy(&bot);
|
||||
ccord_global_cleanup();
|
||||
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
bool ok = false; // Declared before the gotos so they don't jump over it.
|
||||
|
||||
g_bot = bot;
|
||||
signal(SIGINT, &hammy_on_sigint);
|
||||
signal(SIGTERM, &hammy_on_sigint);
|
||||
|
||||
// Order matters: commands loaded before the ready callback can register
|
||||
// them. The pool starts from on_ready, not here - see the comment there.
|
||||
if (!hammy_bot_load_builtins(bot)) goto fail;
|
||||
if (!hammy_bot_set_on_ready(bot, &hammy_on_ready)) goto fail;
|
||||
|
||||
// Blocks until shutdown. A NULL pool afterwards means on_ready bailed out
|
||||
// before it could start one.
|
||||
ok = hammy_bot_run(bot) && bot->pool != NULL;
|
||||
|
||||
hammy_bot_destroy(&bot);
|
||||
|
||||
ccord_global_cleanup();
|
||||
|
||||
return 0;
|
||||
return ok ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
|
||||
fail:
|
||||
hammy_bot_destroy(&bot);
|
||||
ccord_global_cleanup();
|
||||
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user