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
|
# the environment rather than the make command line because Concord's
|
||||||
# Makefile appends its own -I flags to it; a command-line assignment would
|
# Makefile appends its own -I flags to it; a command-line assignment would
|
||||||
# override those and break the build.
|
# 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
|
# gencodecs/Makefile hardcodes CC/HOSTCC/CPP to "cc" for its host-side code
|
||||||
# generator, with plain '=' assignments that the environment cannot override.
|
# generator, with plain '=' assignments that the environment cannot override.
|
||||||
|
|||||||
+23
-5
@@ -10,8 +10,10 @@
|
|||||||
|
|
||||||
struct hammy_bot_t {
|
struct hammy_bot_t {
|
||||||
struct discord* client; // Owning reference to the client - handoff from main.c
|
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.
|
bool commandsRegistered; // A flag if commands have been registered. Avoid re-registering every reconnect.
|
||||||
vector_t* commands; // vector_t of commands. Owning.
|
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
|
// Alloc on heap and create
|
||||||
@@ -20,13 +22,29 @@ hammy_bot_t* hammy_bot_create();
|
|||||||
// Sets the on_ready() call function pointer
|
// 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));
|
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.
|
// Runs the bot. Return true on succeed, false on failure.
|
||||||
bool hammy_bot_run(hammy_bot_t* bot);
|
bool hammy_bot_run(hammy_bot_t* bot);
|
||||||
|
|
||||||
// Adds a command to the vector as a COPY, get rid of the old struct.
|
// Adds a command to the vector as a COPY. Commands own nothing, so the source
|
||||||
bool hammy_bot_add_command(hammy_bot_t* bot, hammy_command_t* command);
|
// 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);
|
bool hammy_bot_register_commands(hammy_bot_t* bot);
|
||||||
|
|
||||||
// Destroy bot, NULLs the passing reference
|
// 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.
|
hammy_arg_t* args; // Array of size nArgs. Owning, array and contents.
|
||||||
size_t nArgs;
|
size_t nArgs;
|
||||||
int64_t queuedAt; // Staleness checks, in ms. From discord_timestamp().
|
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
|
// Deep-copies everything the job needs out of the interaction event, so the
|
||||||
// event may be freed by Concord the moment the handler returns.
|
// event may be freed by Concord the moment the handler returns.
|
||||||
// Returns NULL on allocation failure.
|
// 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.
|
// Frees the job and everything it owns. NULLs the passing reference.
|
||||||
// Safe to call with NULL or with a pointer to NULL.
|
// 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.
|
// 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);
|
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
|
#endif
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ struct hammy_pool_t {
|
|||||||
// Creates the pool and starts n_workers threads, each with its own
|
// 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.
|
// 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.
|
// 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);
|
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.
|
// 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);
|
void hammy_pool_shutdown(hammy_pool_t* pool);
|
||||||
|
|
||||||
// As above but discards anything still queued (each dropped job gets an
|
// 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);
|
void hammy_pool_shutdown_now(hammy_pool_t* pool);
|
||||||
|
|
||||||
// Frees the pool. Runs hammy_pool_shutdown() first if it has not happened yet.
|
// 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,
|
// Clones client, spawns the thread. Returns false on clone or spawn failure,
|
||||||
// leaving the worker safe to pass to hammy_worker_join().
|
// 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);
|
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.
|
// 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/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* 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) {
|
if (!bot) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set defaults
|
// Set defaults
|
||||||
bot->client = NULL;
|
bot->client = NULL;
|
||||||
bot->commands_registered = false;
|
bot->commandsRegistered = false;
|
||||||
bot->commands = vector_create(sizeof(hammy_command_t));
|
bot->commands = vector_create(sizeof(hammy_command_t));
|
||||||
|
|
||||||
// Check if vector allocation errored
|
// Check if vector allocation errored
|
||||||
@@ -17,37 +106,60 @@ hammy_bot_t* hammy_bot_create() {
|
|||||||
return NULL;
|
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;
|
return bot;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hammy_bot_set_on_ready(hammy_bot_t* bot, void (*func)(struct discord* client, const struct discord_ready* event)) {
|
bool hammy_bot_add_command(hammy_bot_t* bot, const hammy_command_t* command) {
|
||||||
if (!bot || !func || !bot->client) { return false; }
|
if (!bot || !bot->commands || !command || !command->name) { return false; }
|
||||||
|
|
||||||
discord_set_on_ready(bot->client, func);
|
// Copy the command into the vector. The command owns nothing, so this is safe.
|
||||||
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
|
|
||||||
if (vector_push_back(bot->commands, command) < 0) {
|
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;
|
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) {
|
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) {
|
bool hammy_bot_destroy(hammy_bot_t** bot) {
|
||||||
if (!bot || !(*bot)) { return false; }
|
if (!bot || !(*bot)) { return false; }
|
||||||
|
|
||||||
if ((*bot)->client) {
|
hammy_bot_t* b = *bot;
|
||||||
discord_cleanup((*bot)->client);
|
|
||||||
|
// 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);
|
free(*bot);
|
||||||
*bot = NULL;
|
*bot = NULL;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -6,11 +6,7 @@
|
|||||||
// Handlers live in src/hammy/commands/. Declared here rather than in a header
|
// 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
|
// so adding a command touches exactly two places: its own .c file and this
|
||||||
// table.
|
// table.
|
||||||
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client) {
|
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client);
|
||||||
(void)job;
|
|
||||||
(void)client;
|
|
||||||
log_info("[command] ping");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The command table. Pure data - no allocation, no lifetime, no destructor.
|
// 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
|
// Copying an entry into the bot's vector is a plain struct copy, and the vector
|
||||||
|
|||||||
+32
-8
@@ -4,6 +4,8 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <hammy/bot.h>
|
||||||
|
#include <hammy/command.h>
|
||||||
#include <hammy/job.h>
|
#include <hammy/job.h>
|
||||||
|
|
||||||
// strdup() is POSIX, so we'll keep a local and keep the code portable.
|
// 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
if (!client || !event) { return NULL; }
|
if (!bot || !bot->client || !event) { return NULL; }
|
||||||
|
|
||||||
hammy_job_t* job = (hammy_job_t*)calloc(1, sizeof(*job));
|
hammy_job_t* job = (hammy_job_t*)calloc(1, sizeof(*job));
|
||||||
if (!job) { return NULL; }
|
if (!job) { return NULL; }
|
||||||
|
|
||||||
|
job->bot = bot;
|
||||||
job->id = event->id;
|
job->id = event->id;
|
||||||
job->appId = event->application_id;
|
|
||||||
job->user = hammy_job_extract_user(event);
|
job->user = hammy_job_extract_user(event);
|
||||||
job->token = hammy_strdup(event->token);
|
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) {
|
if (!job->token) {
|
||||||
goto fail;
|
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) {
|
void hammy_job_run(hammy_job_t* job, struct discord* client) {
|
||||||
if (!job || !client) { return; }
|
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);
|
log_info("[job] Running command '%s' for interaction %" PRIu64, job->command ? job->command : "unknown", job->id);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
hammy_job_reply(job, client, "This is a placeholder reply. The command handler is not yet implemented.");
|
command->handler(job, client);
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-2
@@ -2,6 +2,7 @@
|
|||||||
#include <concord/log.h>
|
#include <concord/log.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include <hammy/bot.h>
|
||||||
#include <hammy/job.h>
|
#include <hammy/job.h>
|
||||||
#include <hammy/pool.h>
|
#include <hammy/pool.h>
|
||||||
#include <hammy/worker.h>
|
#include <hammy/worker.h>
|
||||||
@@ -104,13 +105,22 @@ static void hammy_pool_stop(hammy_pool_t* pool, bool drain) {
|
|||||||
|
|
||||||
pool->shutdown = true;
|
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) {
|
while (pool->count > 0) {
|
||||||
hammy_job_t* job = pool->jobs[pool->head];
|
hammy_job_t* job = pool->jobs[pool->head];
|
||||||
pool->head = (pool->head + 1) % pool->cap;
|
pool->head = (pool->head + 1) % pool->cap;
|
||||||
pool->count--;
|
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_cond_broadcast(&pool->notEmpty);
|
||||||
pthread_mutex_unlock(&pool->lock);
|
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++) {
|
for (size_t i = 0; i < pool->nWorkers; i++) {
|
||||||
hammy_worker_join(&pool->workers[i]);
|
hammy_worker_join(&pool->workers[i]);
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-4
@@ -1,4 +1,5 @@
|
|||||||
#include <concord/discord.h>
|
#include <concord/discord.h>
|
||||||
|
#include <concord/discord-internal.h>
|
||||||
#include <concord/log.h>
|
#include <concord/log.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
@@ -6,6 +7,20 @@
|
|||||||
#include <hammy/pool.h>
|
#include <hammy/pool.h>
|
||||||
#include <hammy/worker.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) {
|
static void* hammy_worker_main(void* arg) {
|
||||||
hammy_worker_t* worker = (hammy_worker_t*)arg;
|
hammy_worker_t* worker = (hammy_worker_t*)arg;
|
||||||
hammy_pool_t* pool = worker->pool;
|
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->clientCopy = NULL;
|
||||||
worker->started = false;
|
worker->started = false;
|
||||||
|
|
||||||
// According to the concord spec, each thread must have its own discord client, so we clone it here.
|
// Each thread needs its own client. The queues, pollers and timers are all
|
||||||
// However, concord's buffers, URLs, headers, etc. are NOT shared-safe. They're per-client.
|
// 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);
|
worker->clientCopy = discord_clone(client);
|
||||||
if (!worker->clientCopy) {
|
if (!worker->clientCopy) {
|
||||||
log_error("[worker %d] Failed to clone client", id);
|
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) {
|
if (pthread_create(&worker->thread, NULL, &hammy_worker_main, worker) != 0) {
|
||||||
log_error("[worker %d] Failed to create thread", id);
|
log_error("[worker %d] Failed to create thread", id);
|
||||||
discord_cleanup(worker->clientCopy);
|
hammy_worker_cleanup_clone(worker->clientCopy);
|
||||||
worker->clientCopy = NULL;
|
worker->clientCopy = NULL;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -93,7 +111,7 @@ void hammy_worker_join(hammy_worker_t* worker) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (worker->clientCopy) {
|
if (worker->clientCopy) {
|
||||||
discord_cleanup(worker->clientCopy);
|
hammy_worker_cleanup_clone(worker->clientCopy);
|
||||||
worker->clientCopy = NULL;
|
worker->clientCopy = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-21
@@ -1,44 +1,90 @@
|
|||||||
#include <string.h>
|
#include <concord/discord.h>
|
||||||
#include <signal.h>
|
|
||||||
#include <concord/log.h>
|
#include <concord/log.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
#include <hammy/bot.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;
|
(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) {
|
static void hammy_on_ready(struct discord* client, const struct discord_ready* event) {
|
||||||
(void)client;
|
hammy_bot_t* bot = (hammy_bot_t*)discord_get_data(client);
|
||||||
log_info("[master] Logged in as %s", event->user->username);
|
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) {
|
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();
|
ccord_global_init();
|
||||||
|
|
||||||
hammy_bot_t* bot = hammy_bot_create();
|
hammy_bot_t* bot = hammy_bot_create();
|
||||||
if (!bot) {
|
if (!bot) {
|
||||||
log_error("[master] Hammy Bot creation returned NULL! Bailing!");
|
fprintf(stderr, "could not create bot\n");
|
||||||
ccord_global_cleanup();
|
ccord_global_cleanup();
|
||||||
|
|
||||||
return 1;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
hammy_bot_set_on_ready(bot, &on_ready);
|
// Token and logging settings both come out of config.json.
|
||||||
hammy_bot_run(bot); // This will block and exit afterwards
|
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);
|
hammy_bot_destroy(&bot);
|
||||||
|
|
||||||
ccord_global_cleanup();
|
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