embeds in responses

This commit is contained in:
2026-08-29 20:34:39 +02:00
parent 57ff0d0378
commit faa73838c0
9 changed files with 1301 additions and 36 deletions
+2 -1
View File
@@ -198,7 +198,7 @@ else()
-Wshadow -Wshadow
-Wconversion -Wconversion
-Wsign-conversion -Wsign-conversion
-Wcast-qual #-Wcast-qual
-Wcast-align -Wcast-align
-Wstrict-prototypes -Wstrict-prototypes
-Wmissing-prototypes -Wmissing-prototypes
@@ -215,6 +215,7 @@ else()
-Wundef -Wundef
-Winit-self -Winit-self
-Wmissing-include-dirs -Wmissing-include-dirs
-Wno-discarded-qualifiers # Temp
) )
if(CMAKE_C_COMPILER_ID MATCHES "GNU") if(CMAKE_C_COMPILER_ID MATCHES "GNU")
+95
View File
@@ -0,0 +1,95 @@
#ifndef HAMMY_EMBEDS_H
#define HAMMY_EMBEDS_H
#include <concord/discord.h>
// Constructs a generic error embed and writes it into out. Returns true on success and false on failure.
// You must allocate space for exactly one embed in the out array. This function cannot check this so please make sure yourself.
static inline bool hammy_embeds_genericerror(struct discord *client, struct discord_embed *out) {
if (!out || !client) { return false; }
static struct discord_embed_footer footer = { .text = "Hammy Bot" };
out[0] = (struct discord_embed){
.title = "An Error Occurred",
.description = "An error occurred while processing your request.",
.color = 0xFF0000,
.timestamp = discord_timestamp(client),
.footer = &footer,
};
return true;
}
// Constructs a custom error embed and writes it into out. Returns true on success and false on failure.
// You must allocate space for exactly one embed in the out array. This function cannot check this so please make sure yourself.
static inline bool hammy_embeds_customerror(struct discord *client,
struct discord_embed *out,
struct discord_embed_fields *fieldsOut,
char *title,
char *description,
struct discord_embed_field *fields,
int fieldCount)
{
if (!out || !client || !title || !description) { return false; }
if (fieldCount < 0 || (fieldCount > 0 && !fields)) { return false; }
if (fieldCount > 0 && !fieldsOut) { return false; }
static struct discord_embed_footer footer = { .text = "Hammy Bot" };
if (fieldCount > 0) {
*fieldsOut = (struct discord_embed_fields){
.size = fieldCount,
.array = fields,
};
}
out[0] = (struct discord_embed){
.title = title,
.description = description,
.color = 0xFF0000,
.timestamp = discord_timestamp(client),
.footer = &footer,
.fields = (fieldCount > 0) ? fieldsOut : NULL,
};
return true;
}
// Constructs a custom generic embed and writes it into out. Returns true on success and false on failure.
// You must allocate space for exactly one embed in the out array. This function cannot check this so please make sure yourself.
static inline bool hammy_embeds_customembed(struct discord *client,
struct discord_embed *out,
struct discord_embed_fields *fieldsOut,
char *title,
char *description,
struct discord_embed_field *fields,
int fieldCount,
int color)
{
if (!out || !client || !title || !description) { return false; }
if (fieldCount < 0 || (fieldCount > 0 && !fields)) { return false; }
if (fieldCount > 0 && !fieldsOut) { return false; }
static struct discord_embed_footer footer = { .text = "Hammy Bot" };
if (fieldCount > 0) {
*fieldsOut = (struct discord_embed_fields){
.size = fieldCount,
.array = fields,
};
}
out[0] = (struct discord_embed){
.title = title,
.description = description,
.color = color,
.timestamp = discord_timestamp(client),
.footer = &footer,
.fields = (fieldCount > 0) ? fieldsOut : NULL,
};
return true;
}
#endif
+2 -2
View File
@@ -56,10 +56,10 @@ void hammy_job_run(hammy_job_t* job, struct discord* client);
// Edits the (already deferred) interaction response with a plain text body. // Edits the (already deferred) interaction response with a plain text body.
// 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* title, const char* content, bool isError);
// Sends a fresh interaction response with a plain text body. // Sends a fresh interaction response with a plain text body.
// Only valid on the instant path, where nothing has been sent yet. // 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); void hammy_job_respond(const hammy_job_t* job, struct discord* client, const char* title, const char* content, bool isError);
#endif #endif
+1 -1
View File
@@ -26,5 +26,5 @@ void hammy_cmd_ping(const hammy_job_t* job, struct discord* client) {
"Pong!\nGateway: `%d ms`\nHandled in: `%" PRId64 " ms`", "Pong!\nGateway: `%d ms`\nHandled in: `%" PRId64 " ms`",
gatewayMs, handledMs); gatewayMs, handledMs);
hammy_job_respond(job, client, body); hammy_job_respond(job, client, "Pong!", body, false);
} }
+21 -6
View File
@@ -9,6 +9,7 @@
#include <hammy/command.h> #include <hammy/command.h>
#include <hammy/job.h> #include <hammy/job.h>
#include <hammy/pool.h> #include <hammy/pool.h>
#include <hammy/embeds.h>
static void hammy_bot_attach(struct discord* client, hammy_bot_t* bot) { static void hammy_bot_attach(struct discord* client, hammy_bot_t* bot) {
discord_set_data(client, bot); discord_set_data(client, bot);
@@ -36,12 +37,26 @@ static void hammy_bot_on_interaction(struct discord* client, const struct discor
struct discord_interaction_response params = { struct discord_interaction_response params = {
.type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, .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, &params, NULL); struct discord_embed embed[1];
CCORDcode code;
if (hammy_embeds_customerror(client, embed, NULL, "Unknown Command", "I don't know that command!", NULL, 0)) {
params.data = &(struct discord_interaction_callback_data){
.embeds = &(struct discord_embeds){
.size = 1,
.array = embed
}
};
code = discord_create_interaction_response(client, event->id, event->token, &params, NULL);
} else {
params.data = &(struct discord_interaction_callback_data){
.content = "I don't know that command!"
};
code = discord_create_interaction_response(client, event->id, event->token, &params, NULL);
}
if (code != CCORD_OK) { if (code != CCORD_OK) {
log_warn("[job] Failed to send interaction error response for interaction %" PRIu64 ": %d", event->id, code); log_warn("[job] Failed to send interaction error response for interaction %" PRIu64 ": %d", event->id, code);
@@ -76,14 +91,14 @@ static void hammy_bot_on_interaction(struct discord* client, const struct discor
break; break;
case HAMMY_PUSH_FULL: case HAMMY_PUSH_FULL:
log_warn("[bot] Queue pool full. Cannot queue job for interaction '%s'.", name ? name : "(null)"); 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_reply(job, client, "Bot is Busy", "I am a bit busy right now! Please try again later.", true);
hammy_job_destroy(&job); hammy_job_destroy(&job);
break; break;
case HAMMY_PUSH_SHUTDOWN: case HAMMY_PUSH_SHUTDOWN:
// Also covers the window before on_ready starts the pool, since // Also covers the window before on_ready starts the pool, since
// hammy_pool_push() reports a NULL pool the same way. // 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)"); 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_reply(job, client, "Bot is Offline", "Hammy is not accepting commands right now. Please try again shortly.", true);
hammy_job_destroy(&job); hammy_job_destroy(&job);
break; break;
} }
+55 -21
View File
@@ -7,6 +7,7 @@
#include <hammy/bot.h> #include <hammy/bot.h>
#include <hammy/command.h> #include <hammy/command.h>
#include <hammy/job.h> #include <hammy/job.h>
#include <hammy/embeds.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.
static char* hammy_strdup(const char* src) { static char* hammy_strdup(const char* src) {
@@ -125,34 +126,67 @@ int64_t hammy_job_age_ms(const hammy_job_t* job, struct discord* client) {
return (int64_t)discord_timestamp(client) - job->queuedAt; return (int64_t)discord_timestamp(client) - job->queuedAt;
} }
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* title, const char* content, bool isError) {
if (!job || !client || !content) { return; } if (!job || !client || !content || !title) { return; }
// TODO: Embeds if (hammy_embeds_customembed(client, NULL, NULL, title, content, NULL, 0, isError ? 0xFF0000 : 0x00FF00)) {
struct discord_edit_original_interaction_response params = { struct discord_interaction_response params = {
.content = (char*)content .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE,
}; .data = &(struct discord_interaction_callback_data){
.content = (char*)content
}
};
CCORDcode code = discord_edit_original_interaction_response(client, job->appId, job->token, &params, NULL); CCORDcode code = discord_create_interaction_response(client, job->id, job->token, &params, NULL);
if (code != CCORD_OK) { if (code != CCORD_OK) {
log_warn("[job] Failed to edit response for interaction %" PRIu64 ": %d", job->id, code); log_warn("[job] Failed to send interaction response for interaction %" PRIu64 ": %d", job->id, code);
}
} else {
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, &params, NULL);
if (code != CCORD_OK) {
log_warn("[job] Failed to send interaction response for interaction %" PRIu64 ": %d", job->id, code);
}
} }
} }
void hammy_job_respond(const hammy_job_t* job, struct discord* client, const char* content) { void hammy_job_respond(const hammy_job_t* job, struct discord* client, const char* title, const char* content, bool isError) {
if (!job || !client || !content) { return; } if (!job || !client || !content || !title) { return; }
struct discord_interaction_response params = { struct discord_embed embed[1];
.type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, if (hammy_embeds_customembed(client, embed, NULL, title, content, NULL, 0, isError ? 0xFF0000 : 0x00FF00)) {
.data = &(struct discord_interaction_callback_data){ struct discord_interaction_response params = {
.content = (char*)content .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE,
.data = &(struct discord_interaction_callback_data){
.embeds = &(struct discord_embeds){
.size = 1,
.array = embed
}
}
};
CCORDcode code = discord_create_interaction_response(client, job->id, job->token, &params, NULL);
if (code != CCORD_OK) {
log_warn("[job] Failed to send interaction response for interaction %" PRIu64 ": %d", job->id, code);
} }
}; } else {
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, &params, NULL); CCORDcode code = discord_create_interaction_response(client, job->id, job->token, &params, NULL);
if (code != CCORD_OK) {
if (code != CCORD_OK) { log_warn("[job] Failed to send interaction response for interaction %" PRIu64 ": %d", job->id, code);
log_warn("[job] Failed to send interaction response for interaction %" PRIu64 ": %d", job->id, code); }
} }
} }
@@ -163,7 +197,7 @@ void hammy_job_run(hammy_job_t* job, struct discord* client) {
const hammy_command_t* command = hammy_bot_find_command(job->bot, job->command); const hammy_command_t* command = hammy_bot_find_command(job->bot, job->command);
if (!command || !command->handler) { if (!command || !command->handler) {
log_warn("[job] No handler found for command '%s'.", job->command ? job->command : "unknown"); 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!"); hammy_job_reply(job, client, "Unknown Command", "I don't know that command!", true);
return; return;
} }
+1 -1
View File
@@ -134,7 +134,7 @@ static void hammy_pool_stop(hammy_pool_t* pool, bool drain) {
// The job's bot back-reference carries the original client, which is // The job's bot back-reference carries the original client, which is
// the one this thread is allowed to serialise through. // the one this thread is allowed to serialise through.
if (job->bot && job->bot->client) { 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_reply(job, job->bot->client, "Bot is Shutting Down", "Hammy is shutting down, so this command was dropped. Please try again once it is back.", true);
} }
hammy_job_destroy(&job); hammy_job_destroy(&job);
+2 -2
View File
@@ -48,12 +48,12 @@ static void* hammy_worker_main(void* arg) {
pthread_mutex_unlock(&pool->lock); pthread_mutex_unlock(&pool->lock);
// From here, the worker owns the job and is responsible for destroying it. // From here, the worker owns the job and is responsible for destroying it.
log_info("[worker %d] Processing job %llu from user %llu", worker->id, job->id, job->user); log_info("[worker %d] Processing job %lu from user %lu", worker->id, job->id, job->user);
int64_t age = hammy_job_age_ms(job, worker->clientCopy); int64_t age = hammy_job_age_ms(job, worker->clientCopy);
if (age > HAMMY_JOB_MAX_AGE_MS) { if (age > HAMMY_JOB_MAX_AGE_MS) {
log_warn("[worker %d] Dropping stale job '%s' (age %lld ms)", worker->id, job->command ? job->command : "unknown", (long long)age); log_warn("[worker %d] Dropping stale job '%s' (age %lld ms)", worker->id, job->command ? job->command : "unknown", (long long)age);
hammy_job_reply(job, worker->clientCopy, "Sorry, your command took too long to process and was dropped. Please try again."); hammy_job_reply(job, worker->clientCopy, "Command Timeout", "Sorry, your command took too long to process and was dropped. Please try again.", true);
} else { } else {
hammy_job_run(job, worker->clientCopy); hammy_job_run(job, worker->clientCopy);
} }
+1122 -2
View File
File diff suppressed because it is too large Load Diff