Add /q command for converting Q-Code (case-insensitive) to Question/Answer combination

This commit is contained in:
2026-08-31 22:04:07 +02:00
parent c056b1b67a
commit 44d13ab071
7 changed files with 144 additions and 3 deletions
+15
View File
@@ -21,6 +21,9 @@
// Longest code in the morse table is '$' ('...-..-'), seven characters. // Longest code in the morse table is '$' ('...-..-'), seven characters.
#define HAMMY_MORSE_MAX 16 #define HAMMY_MORSE_MAX 16
// The current longest string in the qcodes table is 42; 128 is pretty generous. TODO: Change this if the qcodes table ever changes
#define HAMMY_QCODE_MAX 128
// A country has at most a handful of licence classes, each with at most a // A country has at most a handful of licence classes, each with at most a
// couple of segments covering one exact frequency. 24 is generous. // couple of segments covering one exact frequency. 24 is generous.
#define HAMMY_FREQ_PRIVS_MAX 24 #define HAMMY_FREQ_PRIVS_MAX 24
@@ -33,6 +36,7 @@ struct hammy_refdb_t {
sqlite3_stmt* stExact; sqlite3_stmt* stExact;
sqlite3_stmt* stPrefix; sqlite3_stmt* stPrefix;
sqlite3_stmt* stMorse; sqlite3_stmt* stMorse;
sqlite3_stmt* stQCode;
sqlite3_stmt* stFreqMain; sqlite3_stmt* stFreqMain;
sqlite3_stmt* stFreqIaru; sqlite3_stmt* stFreqIaru;
sqlite3_stmt* stFreqNearest; sqlite3_stmt* stFreqNearest;
@@ -41,6 +45,10 @@ struct hammy_refdb_t {
sqlite3_stmt* stCountryList; sqlite3_stmt* stCountryList;
char morseCode[HAMMY_MORSE_MAX]; // Scratch for the last hammy_refdb_get_morse() hit char morseCode[HAMMY_MORSE_MAX]; // Scratch for the last hammy_refdb_get_morse() hit
char qcodeQuestion[HAMMY_QCODE_MAX]; // Scratch for the last hammy_refdb_get_qcode() hit
char qcodeAnswer[HAMMY_QCODE_MAX]; // Scratch for the last hammy_refdb_get_qcode() hit
char version[64]; char version[64];
}; };
@@ -146,6 +154,13 @@ bool hammy_refdb_dxcc(hammy_refdb_t* db, const char* callsign, hammy_dxcc_t* out
// the NEXT call on the same handle - copy it if it has to outlive that. // the NEXT call on the same handle - copy it if it has to outlive that.
bool hammy_refdb_get_morse(hammy_refdb_t* db, char c, const char** out); bool hammy_refdb_get_morse(hammy_refdb_t* db, char c, const char** out);
// Looks up a QSO code in the qcodes table. Case-insensitive; non-ASCII bytes
// never match. Returns false if not found, leaving *out untouched.
//
// On a hit *out points at the storage owned by refdb and is only valid until
// the NEXT call on the same handle - copy it if it has to outlive that.
bool hammy_refdb_get_qcode(hammy_refdb_t* db, const char* code, const char** outQuestion, const char** outAnswer);
// What band is freq_hz in, and who may transmit there. // What band is freq_hz in, and who may transmit there.
// //
// country is an ISO 3166-1 alpha-2 code; NULL or empty means "US". Always // country is an ISO 3166-1 alpha-2 code; NULL or empty means "US". Always
+14
View File
@@ -0,0 +1,14 @@
#ifndef HAMMY_UTILS_H
#define HAMMY_UTILS_H
#include <ctype.h>
#include <stdio.h>
#include <stdint.h>
// Converts in-place to uppercase
void hammy_to_uppercase(char* str);
// Converts in-place to lowercase
void hammy_to_lowercase(char* str);
#endif
-2
View File
@@ -7,8 +7,6 @@
#include <hammy/job.h> #include <hammy/job.h>
#include <hammy/refdb.h> #include <hammy/refdb.h>
#include <dlibc/vector.h>
// The reply goes out as an embed description, which Discord caps at 4096 // The reply goes out as an embed description, which Discord caps at 4096
// characters, so the two halves get separate budgets that still leave room for // characters, so the two halves get separate budgets that still leave room for
// the labels between them. Capping at all is what keeps an unbounded, // the labels between them. Capping at all is what keeps an unbounded,
+39
View File
@@ -0,0 +1,39 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <hammy/command.h>
#include <hammy/job.h>
#include <hammy/refdb.h>
#include <hammy/utils.h>
#define HAMMY_QCODE_ECHO_MAX 16
#define HAMMY_QCODE_CODE_MAX 3072
#define HAMMY_QCODE_TRUNCATED " ... (truncated)"
void hammy_cmd_q(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) {
// Get the qcode argument from the job
const char* qcode = hammy_job_get_arg(job, "q-code");
if (!qcode) {
hammy_job_respond(job, client, "Error", "No text provided for Q-Code conversion.", true);
return;
}
char body[HAMMY_QCODE_CODE_MAX + sizeof(HAMMY_QCODE_TRUNCATED)]; // Generally, since the bottom pointers can only point to max 128-character strings (set as preprocesor header)
// So yeah - if we ever change the above for some reason to stupid values... yeah. Technically "unsafe" but yeah.
const char* questionStr = NULL;
const char* answerStr = NULL;
if (!hammy_refdb_get_qcode(refdb, qcode, &questionStr, &answerStr)) {
hammy_job_respond(job, client, "Q-Code Not Found!", "Failed to find the Q-Code specified. Please check your query!", true);
return;
}
hammy_to_uppercase(qcode);
snprintf(body, sizeof(body), "Q-Code: `%s`\nQuestion: `%s`\nAnswer: `%s`", qcode, (questionStr ? questionStr : "Not Specified"), (answerStr ? answerStr : "Not Specified"));
hammy_job_respond(job, client, "Q-Code Conversion", body, false);
}
+13 -1
View File
@@ -9,6 +9,7 @@
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); void hammy_cmd_ping(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb);
void hammy_cmd_morse(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); void hammy_cmd_morse(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb);
void hammy_cmd_freq(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); void hammy_cmd_freq(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb);
void hammy_cmd_q(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb);
// TODO: Temporary - move to a proper options system; e.g. a vector of heap-allocated options or something. For now, just point at static storage. // TODO: Temporary - move to a proper options system; e.g. a vector of heap-allocated options or something. For now, just point at static storage.
static struct discord_application_command_option morse_opts[] = { static struct discord_application_command_option morse_opts[] = {
@@ -33,6 +34,16 @@ static struct discord_application_command_options freq_opts_struct = {
.array = freq_opts .array = freq_opts
}; };
static struct discord_application_command_option q_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = "q-code",
.description = "Q-Code to convert to Question/Answer", .required = true },
};
static struct discord_application_command_options q_opts_struct = {
.size = 1,
.array = q_opts
};
// 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
// may be created with a NULL destructor hook. // may be created with a NULL destructor hook.
@@ -41,7 +52,8 @@ static struct discord_application_command_options freq_opts_struct = {
static const hammy_command_t hammy_builtin_commands[] = { static const hammy_command_t hammy_builtin_commands[] = {
{ "ping", "Check whether Hammy is alive and how fast it is responding", NULL, &hammy_cmd_ping, true }, { "ping", "Check whether Hammy is alive and how fast it is responding", NULL, &hammy_cmd_ping, true },
{ "morse", "Text to and from Morse code", &morse_opts_struct, &hammy_cmd_morse, true }, { "morse", "Text to and from Morse code", &morse_opts_struct, &hammy_cmd_morse, true },
{ "freq", "Band, segment and who can transmit", &freq_opts_struct, &hammy_cmd_freq, true } { "freq", "Band, segment and who can transmit", &freq_opts_struct, &hammy_cmd_freq, true },
{ "q", "Convert Q-Code to the corresponding question and answer", &q_opts_struct, &hammy_cmd_q, true }
// Tier 0 commands go here as they land. All pure computation, so instant: // Tier 0 commands go here as they land. All pure computation, so instant:
// { "grid", "Maidenhead locator conversions", &grid_opts, &hammy_cmd_grid, true }, // { "grid", "Maidenhead locator conversions", &grid_opts, &hammy_cmd_grid, true },
+50
View File
@@ -86,6 +86,10 @@ static const char SQL_COUNTRY_LIST[] =
static const char SQL_COUNTRY_KNOWN[] = static const char SQL_COUNTRY_KNOWN[] =
"SELECT 1 FROM license_classes WHERE country = ?1 LIMIT 1"; "SELECT 1 FROM license_classes WHERE country = ?1 LIMIT 1";
// Resolve Q-Code to question/answer
static const char SQL_QCODE[] =
"SELECT question, answer FROM qcodes WHERE code = UPPER(?1) LIMIT 1";
// Authorizer: the connection may read, and nothing else. // Authorizer: the connection may read, and nothing else.
// //
// SQLITE_OPEN_READONLY protects the MAIN database file. It does not stop // SQLITE_OPEN_READONLY protects the MAIN database file. It does not stop
@@ -138,6 +142,7 @@ static const hammy_stmt_def_t HAMMY_STATEMENTS[] = {
HAMMY_STMT(stExact, SQL_EXACT), HAMMY_STMT(stExact, SQL_EXACT),
HAMMY_STMT(stPrefix, SQL_PREFIX), HAMMY_STMT(stPrefix, SQL_PREFIX),
HAMMY_STMT(stMorse, SQL_MORSE), HAMMY_STMT(stMorse, SQL_MORSE),
HAMMY_STMT(stQCode, SQL_QCODE),
HAMMY_STMT(stFreqMain, SQL_FREQ_MAIN), HAMMY_STMT(stFreqMain, SQL_FREQ_MAIN),
HAMMY_STMT(stFreqIaru, SQL_FREQ_IARU), HAMMY_STMT(stFreqIaru, SQL_FREQ_IARU),
HAMMY_STMT(stFreqNearest, SQL_FREQ_NEAREST), HAMMY_STMT(stFreqNearest, SQL_FREQ_NEAREST),
@@ -446,6 +451,51 @@ bool hammy_refdb_get_morse(hammy_refdb_t* db, char c, const char** out) {
return hit; return hit;
} }
bool hammy_refdb_get_qcode(hammy_refdb_t* db, const char* code, const char** outQuestion, const char** outAnswer) {
if (!db || !code || !outQuestion || !outAnswer) { return false; }
sqlite3_stmt* const needed[] = { db->stQCode };
if (!stmts_ready("qcodes", needed, 1)) { return false; }
// Read the get_morse comment, I ain't writing this again (entire string edition)
for (const char* p = code; *p != '\0'; p++) {
if ((unsigned char)*p & 0x80u) { return false; }
}
// Query the string
sqlite3_reset(db->stQCode);
sqlite3_clear_bindings(db->stQCode);
sqlite3_bind_text(db->stQCode, 1, code, -1, SQLITE_TRANSIENT); // -1 tells SQLite to figure out the length itself (strlen() call - requires NULL term)
bool hit = false;
if (sqlite3_step(db->stQCode) == SQLITE_ROW) {
if (sqlite3_column_count(db->stQCode) < 2) { // Error
sqlite3_reset(db->stQCode);
return false;
}
const unsigned char* questionStr = sqlite3_column_text(db->stQCode, 0);
const unsigned char* answerStr = sqlite3_column_text(db->stQCode, 1);
if (questionStr) {
snprintf(db->qcodeQuestion, sizeof(db->qcodeQuestion), "%s", (const char*)questionStr);
*outQuestion = db->qcodeQuestion;
hit = true;
}
if (answerStr) {
snprintf(db->qcodeAnswer, sizeof(db->qcodeAnswer), "%s", (const char*)answerStr);
*outAnswer = db->qcodeAnswer;
hit = true;
}
}
sqlite3_reset(db->stQCode);
return hit;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Frequency parsing // Frequency parsing
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+13
View File
@@ -0,0 +1,13 @@
#include <hammy/utils.h>
void hammy_to_uppercase(char* str) {
for (size_t i = 0; str[i] != '\0'; i++) {
str[i] = toupper((unsigned char)str[i]);
}
}
void hammy_to_lowercase(char* str) {
for (size_t i = 0; str[i] != '\0'; i++) {
str[i] = tolower((unsigned char)str[i]);
}
}