diff --git a/include/hammy/refdb.h b/include/hammy/refdb.h index d92ccae..5c08786 100644 --- a/include/hammy/refdb.h +++ b/include/hammy/refdb.h @@ -21,6 +21,9 @@ // Longest code in the morse table is '$' ('...-..-'), seven characters. #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 // couple of segments covering one exact frequency. 24 is generous. #define HAMMY_FREQ_PRIVS_MAX 24 @@ -33,6 +36,7 @@ struct hammy_refdb_t { sqlite3_stmt* stExact; sqlite3_stmt* stPrefix; sqlite3_stmt* stMorse; + sqlite3_stmt* stQCode; sqlite3_stmt* stFreqMain; sqlite3_stmt* stFreqIaru; sqlite3_stmt* stFreqNearest; @@ -41,6 +45,10 @@ struct hammy_refdb_t { sqlite3_stmt* stCountryList; 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]; }; @@ -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. 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. // // country is an ISO 3166-1 alpha-2 code; NULL or empty means "US". Always diff --git a/include/hammy/utils.h b/include/hammy/utils.h new file mode 100644 index 0000000..6159c15 --- /dev/null +++ b/include/hammy/utils.h @@ -0,0 +1,14 @@ +#ifndef HAMMY_UTILS_H +#define HAMMY_UTILS_H + +#include +#include +#include + +// Converts in-place to uppercase +void hammy_to_uppercase(char* str); + +// Converts in-place to lowercase +void hammy_to_lowercase(char* str); + +#endif diff --git a/src/commands/morse.c b/src/commands/morse.c index d249d5f..12792de 100644 --- a/src/commands/morse.c +++ b/src/commands/morse.c @@ -7,8 +7,6 @@ #include #include -#include - // 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 // the labels between them. Capping at all is what keeps an unbounded, diff --git a/src/commands/q.c b/src/commands/q.c new file mode 100644 index 0000000..664384b --- /dev/null +++ b/src/commands/q.c @@ -0,0 +1,39 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#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); +} diff --git a/src/hammy/command.c b/src/hammy/command.c index 098f0f4..39068d1 100644 --- a/src/hammy/command.c +++ b/src/hammy/command.c @@ -9,6 +9,7 @@ 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_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. static struct discord_application_command_option morse_opts[] = { @@ -33,6 +34,16 @@ static struct discord_application_command_options freq_opts_struct = { .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. // Copying an entry into the bot's vector is a plain struct copy, and the vector // 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[] = { { "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 }, - { "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: // { "grid", "Maidenhead locator conversions", &grid_opts, &hammy_cmd_grid, true }, diff --git a/src/hammy/refdb.c b/src/hammy/refdb.c index 70271cb..738e094 100644 --- a/src/hammy/refdb.c +++ b/src/hammy/refdb.c @@ -86,6 +86,10 @@ static const char SQL_COUNTRY_LIST[] = static const char SQL_COUNTRY_KNOWN[] = "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. // // 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(stPrefix, SQL_PREFIX), HAMMY_STMT(stMorse, SQL_MORSE), + HAMMY_STMT(stQCode, SQL_QCODE), HAMMY_STMT(stFreqMain, SQL_FREQ_MAIN), HAMMY_STMT(stFreqIaru, SQL_FREQ_IARU), 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; } +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 // --------------------------------------------------------------------------- diff --git a/src/hammy/utils.c b/src/hammy/utils.c new file mode 100644 index 0000000..cecdfd0 --- /dev/null +++ b/src/hammy/utils.c @@ -0,0 +1,13 @@ +#include + +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]); + } +}